gsheet-lvt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3140 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +1246 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { basename } from "path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
8
|
+
|
|
9
|
+
// src/commands/account/add.ts
|
|
10
|
+
import * as readline from "readline";
|
|
11
|
+
|
|
12
|
+
// src/auth/oauth-flow.ts
|
|
13
|
+
import { OAuth2Client } from "google-auth-library";
|
|
14
|
+
import http from "http";
|
|
15
|
+
|
|
16
|
+
// src/config/constants.ts
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import * as os from "os";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
var __dirname = path.dirname(__filename);
|
|
23
|
+
var packageJsonPath = findPackageJsonPath(__dirname);
|
|
24
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
25
|
+
var APP_INFO = {
|
|
26
|
+
name: "gsheet",
|
|
27
|
+
packageName: packageJson.name,
|
|
28
|
+
display_name: "Google Sheets CLI",
|
|
29
|
+
version: packageJson.version
|
|
30
|
+
};
|
|
31
|
+
var configDirectoryByOS = {
|
|
32
|
+
["linux" /* Linux */]: (homeDir) => path.join(homeDir, ".config", APP_INFO.name),
|
|
33
|
+
["wsl" /* Wsl */]: (homeDir) => path.join(homeDir, ".config", APP_INFO.name),
|
|
34
|
+
["mac" /* Mac */]: (homeDir) => path.join(homeDir, "Library", "Preferences", APP_INFO.name),
|
|
35
|
+
["windows" /* Windows */]: (homeDir) => path.join(homeDir, "AppData", "Roaming", APP_INFO.name)
|
|
36
|
+
};
|
|
37
|
+
function getUserOS() {
|
|
38
|
+
const platform3 = os.platform();
|
|
39
|
+
if (platform3 === "linux") {
|
|
40
|
+
try {
|
|
41
|
+
const release2 = os.release().toLowerCase();
|
|
42
|
+
if (release2.includes("microsoft") || release2.includes("wsl")) {
|
|
43
|
+
return "wsl" /* Wsl */;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
return "linux" /* Linux */;
|
|
48
|
+
}
|
|
49
|
+
if (platform3 === "darwin") return "mac" /* Mac */;
|
|
50
|
+
if (platform3 === "win32") return "windows" /* Windows */;
|
|
51
|
+
throw new Error(`Unsupported OS: ${platform3}`);
|
|
52
|
+
}
|
|
53
|
+
function getConfigDirectory() {
|
|
54
|
+
const userOS = getUserOS();
|
|
55
|
+
const homeDir = os.homedir();
|
|
56
|
+
return configDirectoryByOS[userOS](homeDir);
|
|
57
|
+
}
|
|
58
|
+
var CONFIG_PATHS = {
|
|
59
|
+
configDir: getConfigDirectory(),
|
|
60
|
+
userMetadataFile: path.join(getConfigDirectory(), "user_metadata.json"),
|
|
61
|
+
defaultConfigFile: path.join(getConfigDirectory(), "config.json")
|
|
62
|
+
};
|
|
63
|
+
var OAUTH_SCOPES = {
|
|
64
|
+
SPREADSHEETS: "https://www.googleapis.com/auth/spreadsheets",
|
|
65
|
+
DRIVE_READONLY: "https://www.googleapis.com/auth/drive.readonly",
|
|
66
|
+
USERINFO_EMAIL: "https://www.googleapis.com/auth/userinfo.email"
|
|
67
|
+
};
|
|
68
|
+
var GOOGLE_API_URLS = {
|
|
69
|
+
USERINFO: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
70
|
+
SHEETS_CREATE: "https://sheets.google.com"
|
|
71
|
+
};
|
|
72
|
+
var GOOGLE_CLOUD_CONSOLE_URLS = {
|
|
73
|
+
CREDENTIALS: "https://console.cloud.google.com/apis/credentials",
|
|
74
|
+
CONSENT_SCREEN: "https://console.cloud.google.com/apis/credentials/consent",
|
|
75
|
+
SCOPES: "https://console.cloud.google.com/auth/scopes",
|
|
76
|
+
TEST_USERS: "https://console.cloud.google.com/auth/audience",
|
|
77
|
+
ENABLE_SHEETS_API: "https://console.cloud.google.com/apis/library/sheets.googleapis.com",
|
|
78
|
+
ENABLE_DRIVE_API: "https://console.cloud.google.com/apis/library/drive.googleapis.com"
|
|
79
|
+
};
|
|
80
|
+
var OAUTH_CONFIG = {
|
|
81
|
+
REDIRECT_HOST: "127.0.0.1",
|
|
82
|
+
REDIRECT_PATH: "/callback",
|
|
83
|
+
ACCESS_TYPE: "offline",
|
|
84
|
+
PROMPT: "consent"
|
|
85
|
+
};
|
|
86
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
87
|
+
function findPackageJsonPath(startDir) {
|
|
88
|
+
let currentDir = startDir;
|
|
89
|
+
while (true) {
|
|
90
|
+
const candidate = path.join(currentDir, "package.json");
|
|
91
|
+
if (existsSync(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
const parentDir = path.dirname(currentDir);
|
|
95
|
+
if (parentDir === currentDir) {
|
|
96
|
+
throw new Error("Could not find package.json");
|
|
97
|
+
}
|
|
98
|
+
currentDir = parentDir;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/utils/logger.ts
|
|
103
|
+
import chalk from "chalk";
|
|
104
|
+
var Logger = class {
|
|
105
|
+
static error(message, error) {
|
|
106
|
+
if (error === void 0) {
|
|
107
|
+
console.error(chalk.red(`\u274C ${message}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const errorText = error instanceof Error ? error.message : "Unknown error";
|
|
111
|
+
console.error(chalk.red(`\u274C ${message}: ${errorText}`));
|
|
112
|
+
}
|
|
113
|
+
static success(message) {
|
|
114
|
+
console.log(chalk.green(`\u2705 ${message}`));
|
|
115
|
+
}
|
|
116
|
+
static warning(message) {
|
|
117
|
+
console.log(chalk.yellow(`\u26A0\uFE0F ${message}`));
|
|
118
|
+
}
|
|
119
|
+
static info(message) {
|
|
120
|
+
console.log(`${message}`);
|
|
121
|
+
}
|
|
122
|
+
static dim(message) {
|
|
123
|
+
console.log(chalk.dim(message));
|
|
124
|
+
}
|
|
125
|
+
static plain(message) {
|
|
126
|
+
console.log(message);
|
|
127
|
+
}
|
|
128
|
+
static json(data) {
|
|
129
|
+
console.log(JSON.stringify(data, null, 2));
|
|
130
|
+
}
|
|
131
|
+
static bold(message) {
|
|
132
|
+
console.log(chalk.bold(message));
|
|
133
|
+
}
|
|
134
|
+
static loading(message) {
|
|
135
|
+
console.log(`\u{1F504} ${message}`);
|
|
136
|
+
}
|
|
137
|
+
static link(url, prefix) {
|
|
138
|
+
const linkText = prefix ? `${prefix} ${url}` : url;
|
|
139
|
+
console.log(chalk.dim(`\u{1F517} ${linkText}`));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/auth/oauth-scopes.ts
|
|
144
|
+
var DRIVE_SCOPES = /* @__PURE__ */ new Set([OAUTH_SCOPES.DRIVE_READONLY, "https://www.googleapis.com/auth/drive"]);
|
|
145
|
+
var REQUIRED_OAUTH_SCOPES = [OAUTH_SCOPES.SPREADSHEETS, OAUTH_SCOPES.DRIVE_READONLY];
|
|
146
|
+
function getMissingOAuthScopes(grantedScopes) {
|
|
147
|
+
return REQUIRED_OAUTH_SCOPES.filter((scope) => {
|
|
148
|
+
if (scope === OAUTH_SCOPES.DRIVE_READONLY) {
|
|
149
|
+
return !grantedScopes.some((grantedScope) => DRIVE_SCOPES.has(grantedScope));
|
|
150
|
+
}
|
|
151
|
+
return !grantedScopes.includes(scope);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function assertRequiredOAuthScopes(grantedScopes) {
|
|
155
|
+
const missingScopes = getMissingOAuthScopes(grantedScopes);
|
|
156
|
+
if (missingScopes.length === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(
|
|
160
|
+
[
|
|
161
|
+
"Google returned an access token without required scopes.",
|
|
162
|
+
`Missing scopes: ${missingScopes.join(", ")}`,
|
|
163
|
+
`Granted scopes: ${grantedScopes.length > 0 ? grantedScopes.join(", ") : "none"}`,
|
|
164
|
+
"Fix: in Google Cloud Console, add the missing scopes to the OAuth consent screen, publish/save the consent screen, then run `gsheet account reauth` again."
|
|
165
|
+
].join("\n")
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/auth/oauth-flow.ts
|
|
170
|
+
async function performOAuthFlow(clientId, clientSecret, options = {}) {
|
|
171
|
+
const port = await getRandomAvailablePort();
|
|
172
|
+
const redirectUri = `http://${OAUTH_CONFIG.REDIRECT_HOST}:${port}${OAUTH_CONFIG.REDIRECT_PATH}`;
|
|
173
|
+
const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
|
|
174
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
175
|
+
access_type: OAUTH_CONFIG.ACCESS_TYPE,
|
|
176
|
+
scope: [OAUTH_SCOPES.SPREADSHEETS, OAUTH_SCOPES.DRIVE_READONLY, OAUTH_SCOPES.USERINFO_EMAIL],
|
|
177
|
+
prompt: OAUTH_CONFIG.PROMPT,
|
|
178
|
+
include_granted_scopes: true,
|
|
179
|
+
login_hint: options.loginHint
|
|
180
|
+
});
|
|
181
|
+
if (options.onAuthUrl) {
|
|
182
|
+
options.onAuthUrl(authUrl);
|
|
183
|
+
} else {
|
|
184
|
+
Logger.info("Opening browser for authentication...");
|
|
185
|
+
Logger.info(`Visit: ${authUrl}`);
|
|
186
|
+
}
|
|
187
|
+
const authCode = await startCallbackServer(port);
|
|
188
|
+
const { tokens } = await oauth2Client.getToken(authCode);
|
|
189
|
+
oauth2Client.setCredentials(tokens);
|
|
190
|
+
if (!tokens.access_token) {
|
|
191
|
+
throw new Error("No access token received. Try re-authenticating.");
|
|
192
|
+
}
|
|
193
|
+
const tokenInfo = await oauth2Client.getTokenInfo(tokens.access_token);
|
|
194
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
195
|
+
const userInfo = await fetch(GOOGLE_API_URLS.USERINFO, {
|
|
196
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
197
|
+
});
|
|
198
|
+
const userData = await userInfo.json();
|
|
199
|
+
if (!tokens.refresh_token) {
|
|
200
|
+
throw new Error("No refresh token received. Try revoking app access and re-authenticating.");
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
email: userData.email,
|
|
204
|
+
credentials: {
|
|
205
|
+
client_id: clientId,
|
|
206
|
+
client_secret: clientSecret,
|
|
207
|
+
refresh_token: tokens.refresh_token,
|
|
208
|
+
access_token: tokens.access_token ?? void 0,
|
|
209
|
+
expiry_date: tokens.expiry_date ?? void 0
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function getRandomAvailablePort() {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const server = http.createServer();
|
|
216
|
+
server.listen(0, () => {
|
|
217
|
+
const address = server.address();
|
|
218
|
+
if (address && typeof address !== "string") {
|
|
219
|
+
const port = address.port;
|
|
220
|
+
server.close(() => resolve(port));
|
|
221
|
+
} else {
|
|
222
|
+
reject(new Error("Failed to get port"));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function startCallbackServer(port) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const server = http.createServer((req, res) => {
|
|
230
|
+
if (req.url?.startsWith(OAUTH_CONFIG.REDIRECT_PATH)) {
|
|
231
|
+
const url = new URL(req.url, `http://${OAUTH_CONFIG.REDIRECT_HOST}:${port}`);
|
|
232
|
+
const code = url.searchParams.get("code");
|
|
233
|
+
if (code) {
|
|
234
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
235
|
+
res.end(`
|
|
236
|
+
<html>
|
|
237
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
238
|
+
<h1 style="color: #10b981;">Authentication Successful</h1>
|
|
239
|
+
<p>You can close this window and return to the terminal.</p>
|
|
240
|
+
</body>
|
|
241
|
+
</html>
|
|
242
|
+
`);
|
|
243
|
+
server.close();
|
|
244
|
+
resolve(code);
|
|
245
|
+
} else {
|
|
246
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
247
|
+
res.end(`
|
|
248
|
+
<html>
|
|
249
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
250
|
+
<h1 style="color: #ef4444;">\u2717 Authentication Failed</h1>
|
|
251
|
+
<p>No authorization code received.</p>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
`);
|
|
255
|
+
server.close();
|
|
256
|
+
reject(new Error("No authorization code received"));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
server.listen(port, OAUTH_CONFIG.REDIRECT_HOST);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/cli/define.ts
|
|
265
|
+
function defineCommand(definition) {
|
|
266
|
+
return { kind: "command", ...definition };
|
|
267
|
+
}
|
|
268
|
+
function defineSubCommand(definition) {
|
|
269
|
+
return { kind: "subcommand", ...definition };
|
|
270
|
+
}
|
|
271
|
+
var flag = {
|
|
272
|
+
string(name, description, options) {
|
|
273
|
+
return { name, description, type: "string" /* String */, ...options };
|
|
274
|
+
},
|
|
275
|
+
boolean(name, description, options) {
|
|
276
|
+
return { name, description, type: "boolean" /* Boolean */, ...options };
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var argument = {
|
|
280
|
+
string(name, description, options) {
|
|
281
|
+
return { name, description, type: "string", ...options };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/config/config-manager.ts
|
|
286
|
+
import * as fs2 from "fs";
|
|
287
|
+
import { OAuth2Client as OAuth2Client3 } from "google-auth-library";
|
|
288
|
+
|
|
289
|
+
// src/auth/token-refresh.ts
|
|
290
|
+
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
291
|
+
async function refreshTokenIfNeeded(credentials) {
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
const expiryDate = credentials.expiry_date || 0;
|
|
294
|
+
if (now >= expiryDate - TOKEN_REFRESH_THRESHOLD_MS) {
|
|
295
|
+
return await refreshToken(credentials);
|
|
296
|
+
}
|
|
297
|
+
return credentials;
|
|
298
|
+
}
|
|
299
|
+
async function refreshToken(credentials) {
|
|
300
|
+
const oauth2Client = new OAuth2Client2(credentials.client_id, credentials.client_secret);
|
|
301
|
+
oauth2Client.setCredentials({
|
|
302
|
+
refresh_token: credentials.refresh_token
|
|
303
|
+
});
|
|
304
|
+
const { credentials: newTokens } = await oauth2Client.refreshAccessToken();
|
|
305
|
+
const accessToken = newTokens.access_token || credentials.access_token;
|
|
306
|
+
if (!accessToken) {
|
|
307
|
+
throw new Error("No access token available after refresh. Run `gsheet account reauth`.");
|
|
308
|
+
}
|
|
309
|
+
const tokenInfo = await oauth2Client.getTokenInfo(accessToken);
|
|
310
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
311
|
+
return {
|
|
312
|
+
client_id: credentials.client_id,
|
|
313
|
+
client_secret: credentials.client_secret,
|
|
314
|
+
refresh_token: credentials.refresh_token,
|
|
315
|
+
access_token: accessToken,
|
|
316
|
+
expiry_date: newTokens.expiry_date || credentials.expiry_date
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/utils/json.ts
|
|
321
|
+
import * as fs from "fs";
|
|
322
|
+
function readJson(filePath) {
|
|
323
|
+
if (!fs.existsSync(filePath)) {
|
|
324
|
+
throw new Error(`File not found: ${filePath}`);
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const rawData = fs.readFileSync(filePath, "utf-8");
|
|
328
|
+
return JSON.parse(rawData);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Failed to parse JSON file: ${filePath}. Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function writeJson(filePath, data, pretty = true) {
|
|
336
|
+
try {
|
|
337
|
+
const jsonString = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
338
|
+
fs.writeFileSync(filePath, jsonString, "utf-8");
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Failed to write JSON file: ${filePath}. Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/config/types.ts
|
|
347
|
+
import { z } from "zod";
|
|
348
|
+
var oauthCredentialsSchema = z.object({
|
|
349
|
+
client_id: z.string(),
|
|
350
|
+
client_secret: z.string(),
|
|
351
|
+
refresh_token: z.string(),
|
|
352
|
+
access_token: z.string().optional(),
|
|
353
|
+
expiry_date: z.number().optional()
|
|
354
|
+
});
|
|
355
|
+
var spreadsheetConfigSchema = z.object({
|
|
356
|
+
spreadsheet_id: z.string(),
|
|
357
|
+
activeSheet: z.string().optional()
|
|
358
|
+
});
|
|
359
|
+
var accountSchema = z.object({
|
|
360
|
+
email: z.string(),
|
|
361
|
+
oauth: oauthCredentialsSchema,
|
|
362
|
+
activeSpreadsheet: z.string().optional(),
|
|
363
|
+
spreadsheets: z.record(z.string(), spreadsheetConfigSchema)
|
|
364
|
+
});
|
|
365
|
+
var userMetadataSchema = z.object({
|
|
366
|
+
config_path: z.string(),
|
|
367
|
+
activeAccount: z.string().optional(),
|
|
368
|
+
accounts: z.record(z.string(), accountSchema)
|
|
369
|
+
});
|
|
370
|
+
var sheetsConfigSchema = z.object({
|
|
371
|
+
$schema: z.string().optional(),
|
|
372
|
+
settings: z.object({
|
|
373
|
+
max_results: z.number().default(50),
|
|
374
|
+
default_columns: z.string().default("A:Z"),
|
|
375
|
+
completion_installed: z.boolean().optional()
|
|
376
|
+
}).optional()
|
|
377
|
+
});
|
|
378
|
+
var sheetDataSchema = z.object({
|
|
379
|
+
title: z.string(),
|
|
380
|
+
index: z.number()
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// src/config/config-manager.ts
|
|
384
|
+
var ConfigManager = class {
|
|
385
|
+
userMetadata = null;
|
|
386
|
+
config = null;
|
|
387
|
+
constructor() {
|
|
388
|
+
this.ensureConfigDirectory();
|
|
389
|
+
this.initializeUserMetadata();
|
|
390
|
+
}
|
|
391
|
+
ensureConfigDirectory() {
|
|
392
|
+
if (!fs2.existsSync(CONFIG_PATHS.configDir)) {
|
|
393
|
+
fs2.mkdirSync(CONFIG_PATHS.configDir, { recursive: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
initializeUserMetadata() {
|
|
397
|
+
if (!fs2.existsSync(CONFIG_PATHS.userMetadataFile)) {
|
|
398
|
+
this.createDefaultUserMetadata();
|
|
399
|
+
}
|
|
400
|
+
this.loadUserMetadata();
|
|
401
|
+
}
|
|
402
|
+
createDefaultUserMetadata() {
|
|
403
|
+
const defaultMetadata = {
|
|
404
|
+
config_path: CONFIG_PATHS.defaultConfigFile,
|
|
405
|
+
accounts: {}
|
|
406
|
+
};
|
|
407
|
+
writeJson(CONFIG_PATHS.userMetadataFile, defaultMetadata);
|
|
408
|
+
}
|
|
409
|
+
loadUserMetadata() {
|
|
410
|
+
try {
|
|
411
|
+
const data = readJson(CONFIG_PATHS.userMetadataFile);
|
|
412
|
+
const validated = userMetadataSchema.parse(data);
|
|
413
|
+
this.userMetadata = validated;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw new Error(`Failed to load user metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
saveUserMetadata() {
|
|
419
|
+
if (!this.userMetadata) {
|
|
420
|
+
throw new Error("User metadata not loaded");
|
|
421
|
+
}
|
|
422
|
+
writeJson(CONFIG_PATHS.userMetadataFile, this.userMetadata);
|
|
423
|
+
}
|
|
424
|
+
getConfigPath() {
|
|
425
|
+
if (!this.userMetadata) {
|
|
426
|
+
throw new Error("User metadata not loaded");
|
|
427
|
+
}
|
|
428
|
+
return this.userMetadata.config_path;
|
|
429
|
+
}
|
|
430
|
+
loadConfig() {
|
|
431
|
+
if (this.config) {
|
|
432
|
+
return this.config;
|
|
433
|
+
}
|
|
434
|
+
const configPath = this.getConfigPath();
|
|
435
|
+
if (!fs2.existsSync(configPath)) {
|
|
436
|
+
this.createDefaultConfig();
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const data = readJson(configPath);
|
|
440
|
+
const validated = sheetsConfigSchema.parse(data);
|
|
441
|
+
this.config = validated;
|
|
442
|
+
return this.config;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw new Error(`Failed to load config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
createDefaultConfig() {
|
|
448
|
+
const defaultConfig = {
|
|
449
|
+
settings: {
|
|
450
|
+
max_results: 50,
|
|
451
|
+
default_columns: "A:Z"
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const configPath = this.getConfigPath();
|
|
455
|
+
writeJson(configPath, defaultConfig);
|
|
456
|
+
}
|
|
457
|
+
saveConfig() {
|
|
458
|
+
if (!this.config) {
|
|
459
|
+
throw new Error("No config to save");
|
|
460
|
+
}
|
|
461
|
+
const configPath = this.getConfigPath();
|
|
462
|
+
writeJson(configPath, this.config);
|
|
463
|
+
}
|
|
464
|
+
async addAccount(email, credentials) {
|
|
465
|
+
if (!this.userMetadata) {
|
|
466
|
+
throw new Error("User metadata not loaded");
|
|
467
|
+
}
|
|
468
|
+
if (this.userMetadata.accounts[email]) {
|
|
469
|
+
throw new Error(`Account '${email}' already exists`);
|
|
470
|
+
}
|
|
471
|
+
const account = {
|
|
472
|
+
email,
|
|
473
|
+
oauth: credentials,
|
|
474
|
+
spreadsheets: {}
|
|
475
|
+
};
|
|
476
|
+
this.userMetadata.accounts[email] = account;
|
|
477
|
+
this.saveUserMetadata();
|
|
478
|
+
}
|
|
479
|
+
async removeAccount(email) {
|
|
480
|
+
if (!this.userMetadata) {
|
|
481
|
+
throw new Error("User metadata not loaded");
|
|
482
|
+
}
|
|
483
|
+
if (!this.userMetadata.accounts[email]) {
|
|
484
|
+
throw new Error(`Account '${email}' not found`);
|
|
485
|
+
}
|
|
486
|
+
delete this.userMetadata.accounts[email];
|
|
487
|
+
if (this.userMetadata.activeAccount === email) {
|
|
488
|
+
this.userMetadata.activeAccount = void 0;
|
|
489
|
+
}
|
|
490
|
+
this.saveUserMetadata();
|
|
491
|
+
}
|
|
492
|
+
getAllAccounts() {
|
|
493
|
+
if (!this.userMetadata) {
|
|
494
|
+
throw new Error("User metadata not loaded");
|
|
495
|
+
}
|
|
496
|
+
return Object.values(this.userMetadata.accounts);
|
|
497
|
+
}
|
|
498
|
+
getAccount(email) {
|
|
499
|
+
if (!this.userMetadata) {
|
|
500
|
+
throw new Error("User metadata not loaded");
|
|
501
|
+
}
|
|
502
|
+
return this.userMetadata.accounts[email] || null;
|
|
503
|
+
}
|
|
504
|
+
setActiveAccount(email) {
|
|
505
|
+
if (!this.userMetadata) {
|
|
506
|
+
throw new Error("User metadata not loaded");
|
|
507
|
+
}
|
|
508
|
+
if (!this.userMetadata.accounts[email]) {
|
|
509
|
+
throw new Error(`Account '${email}' not found`);
|
|
510
|
+
}
|
|
511
|
+
this.userMetadata.activeAccount = email;
|
|
512
|
+
this.saveUserMetadata();
|
|
513
|
+
}
|
|
514
|
+
getActiveAccount() {
|
|
515
|
+
if (!this.userMetadata?.activeAccount) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return this.getAccount(this.userMetadata.activeAccount);
|
|
519
|
+
}
|
|
520
|
+
getActiveAccountEmail() {
|
|
521
|
+
return this.userMetadata?.activeAccount || null;
|
|
522
|
+
}
|
|
523
|
+
async updateAccountCredentials(email, credentials) {
|
|
524
|
+
if (!this.userMetadata) {
|
|
525
|
+
throw new Error("User metadata not loaded");
|
|
526
|
+
}
|
|
527
|
+
const account = this.userMetadata.accounts[email];
|
|
528
|
+
if (!account) {
|
|
529
|
+
throw new Error(`Account '${email}' not found`);
|
|
530
|
+
}
|
|
531
|
+
account.oauth = credentials;
|
|
532
|
+
this.saveUserMetadata();
|
|
533
|
+
}
|
|
534
|
+
async getRefreshedCredentials(email) {
|
|
535
|
+
const account = this.getAccount(email);
|
|
536
|
+
if (!account) {
|
|
537
|
+
throw new Error(`Account '${email}' not found`);
|
|
538
|
+
}
|
|
539
|
+
const refreshedCredentials = await refreshTokenIfNeeded(account.oauth);
|
|
540
|
+
await assertCredentialsHaveRequiredScopes(refreshedCredentials);
|
|
541
|
+
if (refreshedCredentials !== account.oauth) {
|
|
542
|
+
await this.updateAccountCredentials(email, refreshedCredentials);
|
|
543
|
+
}
|
|
544
|
+
return refreshedCredentials;
|
|
545
|
+
}
|
|
546
|
+
async addSpreadsheet(email, name, spreadsheetId) {
|
|
547
|
+
if (!this.userMetadata) {
|
|
548
|
+
throw new Error("User metadata not loaded");
|
|
549
|
+
}
|
|
550
|
+
const account = this.userMetadata.accounts[email];
|
|
551
|
+
if (!account) {
|
|
552
|
+
throw new Error(`Account '${email}' not found`);
|
|
553
|
+
}
|
|
554
|
+
if (account.spreadsheets[name]) {
|
|
555
|
+
throw new Error(`Spreadsheet '${name}' already exists for account '${email}'`);
|
|
556
|
+
}
|
|
557
|
+
const spreadsheet = {
|
|
558
|
+
spreadsheet_id: spreadsheetId
|
|
559
|
+
};
|
|
560
|
+
account.spreadsheets[name] = spreadsheet;
|
|
561
|
+
this.saveUserMetadata();
|
|
562
|
+
}
|
|
563
|
+
async removeSpreadsheet(email, name) {
|
|
564
|
+
if (!this.userMetadata) {
|
|
565
|
+
throw new Error("User metadata not loaded");
|
|
566
|
+
}
|
|
567
|
+
const account = this.userMetadata.accounts[email];
|
|
568
|
+
if (!account) {
|
|
569
|
+
throw new Error(`Account '${email}' not found`);
|
|
570
|
+
}
|
|
571
|
+
if (!account.spreadsheets[name]) {
|
|
572
|
+
throw new Error(`Spreadsheet '${name}' not found for account '${email}'`);
|
|
573
|
+
}
|
|
574
|
+
delete account.spreadsheets[name];
|
|
575
|
+
if (account.activeSpreadsheet === name) {
|
|
576
|
+
account.activeSpreadsheet = void 0;
|
|
577
|
+
}
|
|
578
|
+
this.saveUserMetadata();
|
|
579
|
+
}
|
|
580
|
+
listSpreadsheets(email) {
|
|
581
|
+
if (!this.userMetadata) {
|
|
582
|
+
throw new Error("User metadata not loaded");
|
|
583
|
+
}
|
|
584
|
+
const account = this.userMetadata.accounts[email];
|
|
585
|
+
if (!account) {
|
|
586
|
+
throw new Error(`Account '${email}' not found`);
|
|
587
|
+
}
|
|
588
|
+
return Object.entries(account.spreadsheets).map(([name, spreadsheet]) => ({
|
|
589
|
+
name,
|
|
590
|
+
spreadsheetId: spreadsheet.spreadsheet_id,
|
|
591
|
+
activeSheet: spreadsheet.activeSheet
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
getSpreadsheet(email, name) {
|
|
595
|
+
if (!this.userMetadata) {
|
|
596
|
+
throw new Error("User metadata not loaded");
|
|
597
|
+
}
|
|
598
|
+
const account = this.userMetadata.accounts[email];
|
|
599
|
+
if (!account) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return account.spreadsheets[name] || null;
|
|
603
|
+
}
|
|
604
|
+
getSpreadsheetById(email, id) {
|
|
605
|
+
if (!this.userMetadata) {
|
|
606
|
+
throw new Error("User metadata not loaded");
|
|
607
|
+
}
|
|
608
|
+
const account = this.userMetadata.accounts[email];
|
|
609
|
+
if (!account) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
return Object.values(account.spreadsheets).find((s) => s.spreadsheet_id === id) || null;
|
|
613
|
+
}
|
|
614
|
+
setActiveSpreadsheet(email, name) {
|
|
615
|
+
if (!this.userMetadata) {
|
|
616
|
+
throw new Error("User metadata not loaded");
|
|
617
|
+
}
|
|
618
|
+
const account = this.userMetadata.accounts[email];
|
|
619
|
+
if (!account) {
|
|
620
|
+
throw new Error(`Account '${email}' not found`);
|
|
621
|
+
}
|
|
622
|
+
if (!account.spreadsheets[name]) {
|
|
623
|
+
throw new Error(`Spreadsheet '${name}' not found for account '${email}'`);
|
|
624
|
+
}
|
|
625
|
+
account.activeSpreadsheet = name;
|
|
626
|
+
this.saveUserMetadata();
|
|
627
|
+
}
|
|
628
|
+
getActiveSpreadsheet(email) {
|
|
629
|
+
if (!this.userMetadata) {
|
|
630
|
+
throw new Error("User metadata not loaded");
|
|
631
|
+
}
|
|
632
|
+
const account = this.userMetadata.accounts[email];
|
|
633
|
+
if (!account || !account.activeSpreadsheet) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return account.spreadsheets[account.activeSpreadsheet] || null;
|
|
637
|
+
}
|
|
638
|
+
getActiveSpreadsheetName(email) {
|
|
639
|
+
if (!this.userMetadata) {
|
|
640
|
+
throw new Error("User metadata not loaded");
|
|
641
|
+
}
|
|
642
|
+
const account = this.userMetadata.accounts[email];
|
|
643
|
+
return account?.activeSpreadsheet || null;
|
|
644
|
+
}
|
|
645
|
+
setActiveSheet(email, spreadsheetName, sheetName) {
|
|
646
|
+
if (!this.userMetadata) {
|
|
647
|
+
throw new Error("User metadata not loaded");
|
|
648
|
+
}
|
|
649
|
+
const account = this.userMetadata.accounts[email];
|
|
650
|
+
if (!account) {
|
|
651
|
+
throw new Error(`Account '${email}' not found`);
|
|
652
|
+
}
|
|
653
|
+
const spreadsheet = account.spreadsheets[spreadsheetName];
|
|
654
|
+
if (!spreadsheet) {
|
|
655
|
+
throw new Error(`Spreadsheet '${spreadsheetName}' not found for account '${email}'`);
|
|
656
|
+
}
|
|
657
|
+
spreadsheet.activeSheet = sheetName;
|
|
658
|
+
this.saveUserMetadata();
|
|
659
|
+
}
|
|
660
|
+
getActiveSheetName(email, spreadsheetName) {
|
|
661
|
+
if (!this.userMetadata) {
|
|
662
|
+
throw new Error("User metadata not loaded");
|
|
663
|
+
}
|
|
664
|
+
const account = this.userMetadata.accounts[email];
|
|
665
|
+
if (!account) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const spreadsheet = account.spreadsheets[spreadsheetName];
|
|
669
|
+
return spreadsheet?.activeSheet || null;
|
|
670
|
+
}
|
|
671
|
+
markCompletionInstalled() {
|
|
672
|
+
const config = this.loadConfig();
|
|
673
|
+
if (!config.settings) {
|
|
674
|
+
config.settings = {
|
|
675
|
+
max_results: 50,
|
|
676
|
+
default_columns: "A:Z"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
config.settings.completion_installed = true;
|
|
680
|
+
this.saveConfig();
|
|
681
|
+
}
|
|
682
|
+
isCompletionInstalled() {
|
|
683
|
+
const config = this.loadConfig();
|
|
684
|
+
return config.settings?.completion_installed === true;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
async function assertCredentialsHaveRequiredScopes(credentials) {
|
|
688
|
+
if (!credentials.access_token) {
|
|
689
|
+
throw new Error("No access token available. Run `gsheet account reauth`.");
|
|
690
|
+
}
|
|
691
|
+
const oauth2Client = new OAuth2Client3(credentials.client_id, credentials.client_secret);
|
|
692
|
+
const tokenInfo = await oauth2Client.getTokenInfo(credentials.access_token);
|
|
693
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/commands/account/add.ts
|
|
697
|
+
async function promptInput(question) {
|
|
698
|
+
const rl = readline.createInterface({
|
|
699
|
+
input: process.stdin,
|
|
700
|
+
output: process.stdout
|
|
701
|
+
});
|
|
702
|
+
return new Promise((resolve) => {
|
|
703
|
+
rl.question(question, (answer) => {
|
|
704
|
+
rl.close();
|
|
705
|
+
resolve(answer.trim());
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
var addAccountCommand = defineSubCommand({
|
|
710
|
+
name: "add",
|
|
711
|
+
description: "Add a Google account via OAuth",
|
|
712
|
+
errorMessage: "Failed to add account",
|
|
713
|
+
action: async () => {
|
|
714
|
+
Logger.bold("=".repeat(70));
|
|
715
|
+
Logger.bold(" GOOGLE CLOUD CONSOLE SETUP");
|
|
716
|
+
Logger.bold("=".repeat(70));
|
|
717
|
+
Logger.plain("");
|
|
718
|
+
Logger.info("Follow these steps to get OAuth credentials:\n");
|
|
719
|
+
Logger.bold("STEP 1: Create or Select a Project");
|
|
720
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.CREDENTIALS}`);
|
|
721
|
+
Logger.info(" - Go to Google Cloud Console");
|
|
722
|
+
Logger.info(" - Create a new project OR select an existing one");
|
|
723
|
+
Logger.info(" - Note: May require setting up billing (free tier available)");
|
|
724
|
+
Logger.plain("");
|
|
725
|
+
Logger.bold("STEP 2: Enable Required APIs");
|
|
726
|
+
Logger.info(" a) Enable Google Sheets API:");
|
|
727
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.ENABLE_SHEETS_API}`);
|
|
728
|
+
Logger.info(" b) Enable Google Drive API:");
|
|
729
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.ENABLE_DRIVE_API}`);
|
|
730
|
+
Logger.plain("");
|
|
731
|
+
Logger.bold("STEP 3: Configure OAuth Consent Screen");
|
|
732
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.CONSENT_SCREEN}`);
|
|
733
|
+
Logger.info(" - User Type: External");
|
|
734
|
+
Logger.info(" - App name: gsheet (or any name)");
|
|
735
|
+
Logger.info(" - User support email: your email");
|
|
736
|
+
Logger.info(" - Developer contact: your email");
|
|
737
|
+
Logger.info(' - Click "SAVE AND CONTINUE"');
|
|
738
|
+
Logger.plain("");
|
|
739
|
+
Logger.bold("STEP 4: Add Scopes");
|
|
740
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.SCOPES}`);
|
|
741
|
+
Logger.info(' - Click "ADD OR REMOVE SCOPES"');
|
|
742
|
+
Logger.info(" - Search and add:");
|
|
743
|
+
Logger.info(" \u2192 .../auth/spreadsheets");
|
|
744
|
+
Logger.info(" \u2192 .../auth/drive.readonly");
|
|
745
|
+
Logger.info(" \u2192 .../auth/userinfo.email");
|
|
746
|
+
Logger.info(' - Click "UPDATE" then "SAVE AND CONTINUE"');
|
|
747
|
+
Logger.plain("");
|
|
748
|
+
Logger.bold("STEP 5: Add Test Users");
|
|
749
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.TEST_USERS}`);
|
|
750
|
+
Logger.info(' - Click "ADD USERS"');
|
|
751
|
+
Logger.info(" - Add your email address");
|
|
752
|
+
Logger.info(' - Click "SAVE AND CONTINUE"');
|
|
753
|
+
Logger.plain("");
|
|
754
|
+
Logger.bold("STEP 6: Create OAuth 2.0 Client ID");
|
|
755
|
+
Logger.link(` ${GOOGLE_CLOUD_CONSOLE_URLS.CREDENTIALS}`);
|
|
756
|
+
Logger.info(' - Click "CREATE CREDENTIALS" \u2192 "OAuth client ID"');
|
|
757
|
+
Logger.info(" - Application type: Desktop app");
|
|
758
|
+
Logger.info(" - Name: gsheet");
|
|
759
|
+
Logger.info(' - Click "CREATE"');
|
|
760
|
+
Logger.info(" - Copy the Client ID and Client Secret");
|
|
761
|
+
Logger.plain("");
|
|
762
|
+
Logger.bold("=".repeat(70));
|
|
763
|
+
Logger.plain("");
|
|
764
|
+
const clientId = await promptInput("Enter OAuth Client ID: ");
|
|
765
|
+
if (!clientId) {
|
|
766
|
+
Logger.error("Client ID is required");
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
const clientSecret = await promptInput("Enter OAuth Client Secret: ");
|
|
770
|
+
if (!clientSecret) {
|
|
771
|
+
Logger.error("Client Secret is required");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
Logger.info("\nStarting OAuth authentication flow...");
|
|
775
|
+
const result = await performOAuthFlow(clientId, clientSecret);
|
|
776
|
+
const configManager = new ConfigManager();
|
|
777
|
+
await configManager.addAccount(result.email, result.credentials);
|
|
778
|
+
const accounts = configManager.getAllAccounts();
|
|
779
|
+
if (accounts.length === 1) {
|
|
780
|
+
configManager.setActiveAccount(result.email);
|
|
781
|
+
Logger.success(`Account '${result.email}' added and set as active!`);
|
|
782
|
+
} else {
|
|
783
|
+
Logger.success(`Account '${result.email}' added successfully!`);
|
|
784
|
+
Logger.info("Switch to this account: gsheet account select");
|
|
785
|
+
}
|
|
786
|
+
process.exit(0);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// src/commands/account/list.ts
|
|
791
|
+
var listAccountsCommand = defineSubCommand({
|
|
792
|
+
name: "list",
|
|
793
|
+
description: "List all configured Google accounts",
|
|
794
|
+
errorMessage: "Failed to list accounts",
|
|
795
|
+
action: () => {
|
|
796
|
+
const configManager = new ConfigManager();
|
|
797
|
+
const accounts = configManager.getAllAccounts();
|
|
798
|
+
const activeAccountEmail = configManager.getActiveAccountEmail();
|
|
799
|
+
if (accounts.length === 0) {
|
|
800
|
+
Logger.info("No accounts configured.");
|
|
801
|
+
Logger.info("Add one with: gsheet account add");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
Logger.info("Configured accounts:");
|
|
805
|
+
accounts.forEach((account) => {
|
|
806
|
+
const isActive = account.email === activeAccountEmail;
|
|
807
|
+
const spreadsheetCount = Object.keys(account.spreadsheets).length;
|
|
808
|
+
const prefix = isActive ? "->" : " ";
|
|
809
|
+
Logger.info(`${prefix} ${account.email} (${spreadsheetCount} spreadsheet${spreadsheetCount !== 1 ? "s" : ""})`);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// src/commands/account/reauth.ts
|
|
815
|
+
var reauthAccountCommand = defineSubCommand({
|
|
816
|
+
name: "reauth",
|
|
817
|
+
description: "Re-authenticate the active account",
|
|
818
|
+
errorMessage: "Failed to re-authenticate account",
|
|
819
|
+
action: async () => {
|
|
820
|
+
const configManager = new ConfigManager();
|
|
821
|
+
const activeAccount = configManager.getActiveAccount();
|
|
822
|
+
if (!activeAccount) {
|
|
823
|
+
Logger.error("No active account set.");
|
|
824
|
+
Logger.info("Use: gsheet account switch <email>");
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
Logger.info(`Re-authenticating account: ${activeAccount.email}`);
|
|
828
|
+
Logger.info("Opening browser for authentication...\n");
|
|
829
|
+
const result = await performOAuthFlow(activeAccount.oauth.client_id, activeAccount.oauth.client_secret, {
|
|
830
|
+
loginHint: activeAccount.email
|
|
831
|
+
});
|
|
832
|
+
if (result.email !== activeAccount.email) {
|
|
833
|
+
Logger.error(`Authentication email mismatch. Expected ${activeAccount.email}, got ${result.email}`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
await configManager.updateAccountCredentials(result.email, result.credentials);
|
|
837
|
+
Logger.success(`Account '${result.email}' re-authenticated successfully!`);
|
|
838
|
+
process.exit(0);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// src/commands/account/remove.ts
|
|
843
|
+
import inquirer from "inquirer";
|
|
844
|
+
var removeAccountCommand = defineSubCommand({
|
|
845
|
+
name: "remove",
|
|
846
|
+
description: "Remove a Google account",
|
|
847
|
+
arguments: [argument.string("email", "Account email to remove (optional - interactive if not provided)")],
|
|
848
|
+
errorMessage: "Failed to remove account",
|
|
849
|
+
action: async ({ args }) => {
|
|
850
|
+
const configManager = new ConfigManager();
|
|
851
|
+
const accounts = configManager.getAllAccounts();
|
|
852
|
+
if (accounts.length === 0) {
|
|
853
|
+
Logger.warning("No accounts configured.");
|
|
854
|
+
Logger.info("Use: gsheet account add");
|
|
855
|
+
process.exit(0);
|
|
856
|
+
}
|
|
857
|
+
let selectedEmail = args.email;
|
|
858
|
+
if (!selectedEmail) {
|
|
859
|
+
const choices = accounts.map((acc) => ({
|
|
860
|
+
name: acc.email,
|
|
861
|
+
value: acc.email
|
|
862
|
+
}));
|
|
863
|
+
const answer = await inquirer.prompt([
|
|
864
|
+
{
|
|
865
|
+
type: "list",
|
|
866
|
+
name: "email",
|
|
867
|
+
message: "Select account to remove:",
|
|
868
|
+
choices
|
|
869
|
+
}
|
|
870
|
+
]);
|
|
871
|
+
selectedEmail = answer.email;
|
|
872
|
+
}
|
|
873
|
+
if (!selectedEmail) {
|
|
874
|
+
Logger.error("No account selected");
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
const account = configManager.getAccount(selectedEmail);
|
|
878
|
+
if (!account) {
|
|
879
|
+
Logger.error(`Account '${selectedEmail}' not found`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
const spreadsheetCount = Object.keys(account.spreadsheets).length;
|
|
883
|
+
if (spreadsheetCount > 0) {
|
|
884
|
+
Logger.warning(
|
|
885
|
+
`This will remove ${spreadsheetCount} spreadsheet${spreadsheetCount !== 1 ? "s" : ""} associated with this account.`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const confirmation = await inquirer.prompt([
|
|
889
|
+
{
|
|
890
|
+
type: "confirm",
|
|
891
|
+
name: "confirmed",
|
|
892
|
+
message: `Are you sure you want to remove account '${selectedEmail}'?`,
|
|
893
|
+
default: false
|
|
894
|
+
}
|
|
895
|
+
]);
|
|
896
|
+
if (!confirmation.confirmed) {
|
|
897
|
+
Logger.info("Removal cancelled");
|
|
898
|
+
process.exit(0);
|
|
899
|
+
}
|
|
900
|
+
await configManager.removeAccount(selectedEmail);
|
|
901
|
+
Logger.success(`Account '${selectedEmail}' removed successfully`);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// src/commands/account/select.ts
|
|
906
|
+
import inquirer2 from "inquirer";
|
|
907
|
+
var selectAccountCommand = defineSubCommand({
|
|
908
|
+
name: "select",
|
|
909
|
+
description: "Select active Google account",
|
|
910
|
+
arguments: [argument.string("email", "Account email to select (optional - interactive if not provided)")],
|
|
911
|
+
errorMessage: "Failed to select account",
|
|
912
|
+
action: async ({ args }) => {
|
|
913
|
+
const configManager = new ConfigManager();
|
|
914
|
+
const accounts = configManager.getAllAccounts();
|
|
915
|
+
if (accounts.length === 0) {
|
|
916
|
+
Logger.warning("No accounts configured.");
|
|
917
|
+
Logger.info("Use: gsheet account add");
|
|
918
|
+
process.exit(0);
|
|
919
|
+
}
|
|
920
|
+
let selectedEmail = args.email;
|
|
921
|
+
if (!selectedEmail) {
|
|
922
|
+
const activeAccount = configManager.getActiveAccount();
|
|
923
|
+
const choices = accounts.map((acc) => ({
|
|
924
|
+
name: acc.email === activeAccount?.email ? `${acc.email} (current)` : acc.email,
|
|
925
|
+
value: acc.email
|
|
926
|
+
}));
|
|
927
|
+
const answer = await inquirer2.prompt([
|
|
928
|
+
{
|
|
929
|
+
type: "list",
|
|
930
|
+
name: "email",
|
|
931
|
+
message: "Select account:",
|
|
932
|
+
choices
|
|
933
|
+
}
|
|
934
|
+
]);
|
|
935
|
+
selectedEmail = answer.email;
|
|
936
|
+
}
|
|
937
|
+
if (!selectedEmail) {
|
|
938
|
+
Logger.error("No account selected");
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
configManager.setActiveAccount(selectedEmail);
|
|
942
|
+
Logger.success(`Selected account: ${selectedEmail}`);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// src/commands/completion.ts
|
|
947
|
+
import { accessSync, constants, existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
948
|
+
import { homedir as homedir2 } from "os";
|
|
949
|
+
import { join as join2 } from "path";
|
|
950
|
+
|
|
951
|
+
// src/cli/completion/shared.ts
|
|
952
|
+
var globalOptions = [
|
|
953
|
+
{ description: "Show help", long: "help", names: ["-h", "--help"], short: "h" },
|
|
954
|
+
{ description: "Show version", long: "version", names: ["-V", "--version"], short: "V" }
|
|
955
|
+
];
|
|
956
|
+
function isVisibleCompletionCommand(command) {
|
|
957
|
+
return command.visible;
|
|
958
|
+
}
|
|
959
|
+
function getCompletionBinNames(binName) {
|
|
960
|
+
return [.../* @__PURE__ */ new Set([binName, APP_INFO.name, "gs"])];
|
|
961
|
+
}
|
|
962
|
+
function getRootCommands(commands) {
|
|
963
|
+
const roots = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const command of commands) {
|
|
965
|
+
const parts = command.name.split(" ");
|
|
966
|
+
const root = parts[0];
|
|
967
|
+
if (!root || roots.has(root)) continue;
|
|
968
|
+
roots.set(root, {
|
|
969
|
+
name: root,
|
|
970
|
+
description: command.description
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return [...roots.values()];
|
|
974
|
+
}
|
|
975
|
+
function getSubcommandGroups(commands) {
|
|
976
|
+
const groups = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const command of commands) {
|
|
978
|
+
const [root, subcommand] = command.name.split(" ");
|
|
979
|
+
if (!root || !subcommand) continue;
|
|
980
|
+
const group = groups.get(root) ?? [];
|
|
981
|
+
if (!group.some((item) => item.name === subcommand)) {
|
|
982
|
+
group.push({ name: subcommand, description: command.description });
|
|
983
|
+
}
|
|
984
|
+
groups.set(root, group);
|
|
985
|
+
}
|
|
986
|
+
return groups;
|
|
987
|
+
}
|
|
988
|
+
function getOptionGroups(commands) {
|
|
989
|
+
const groups = /* @__PURE__ */ new Map();
|
|
990
|
+
for (const command of commands) {
|
|
991
|
+
const options = command.options.filter((option) => option.visible);
|
|
992
|
+
if (options.length === 0) continue;
|
|
993
|
+
groups.set(
|
|
994
|
+
command.name,
|
|
995
|
+
options.map((option) => ({
|
|
996
|
+
description: option.description,
|
|
997
|
+
long: option.allNotations.find((notation) => notation.startsWith("--"))?.replace(/^--/, ""),
|
|
998
|
+
names: option.allNotations,
|
|
999
|
+
short: option.allNotations.find((notation) => /^-[^-]/.test(notation))?.replace(/^-/, "")
|
|
1000
|
+
}))
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
return groups;
|
|
1004
|
+
}
|
|
1005
|
+
function commandKey(command) {
|
|
1006
|
+
return command.replace(/\W+/g, "_");
|
|
1007
|
+
}
|
|
1008
|
+
function optionWords(items) {
|
|
1009
|
+
return items.flatMap((item) => item.names).join(" ");
|
|
1010
|
+
}
|
|
1011
|
+
function getFunctionName(binName) {
|
|
1012
|
+
return `_${commandKey(binName)}_completion`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/cli/completion/bash.ts
|
|
1016
|
+
function getBashCompletionScript(binNames, roots, subcommands, _options) {
|
|
1017
|
+
const functionName = getFunctionName(binNames[0] ?? APP_INFO.name);
|
|
1018
|
+
return `${functionName}() {
|
|
1019
|
+
local cur root subcommand
|
|
1020
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1021
|
+
root="\${COMP_WORDS[1]}"
|
|
1022
|
+
subcommand="\${COMP_WORDS[2]}"
|
|
1023
|
+
COMPREPLY=()
|
|
1024
|
+
|
|
1025
|
+
if [[ "$cur" == -* ]]; then
|
|
1026
|
+
COMPREPLY=($(compgen -W "${optionWords(globalOptions)}" -- "$cur"))
|
|
1027
|
+
return
|
|
1028
|
+
fi
|
|
1029
|
+
|
|
1030
|
+
case "$COMP_CWORD" in
|
|
1031
|
+
1)
|
|
1032
|
+
COMPREPLY=($(compgen -W "${roots.map((item) => item.name).join(" ")}" -- "$cur"))
|
|
1033
|
+
;;
|
|
1034
|
+
2)
|
|
1035
|
+
case "\${COMP_WORDS[1]}" in
|
|
1036
|
+
${formatBashSubcommandCases(subcommands)}
|
|
1037
|
+
esac
|
|
1038
|
+
;;
|
|
1039
|
+
esac
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
${binNames.map((binName) => `complete -F ${functionName} ${binName}`).join("\n")}`;
|
|
1043
|
+
}
|
|
1044
|
+
function formatBashSubcommandCases(groups) {
|
|
1045
|
+
return [...groups.entries()].map(
|
|
1046
|
+
([root, items]) => ` ${root})
|
|
1047
|
+
COMPREPLY=($(compgen -W "${items.map((item) => item.name).join(" ")}" -- "$cur"))
|
|
1048
|
+
;;`
|
|
1049
|
+
).join("\n");
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/cli/completion/fish.ts
|
|
1053
|
+
function getFishCompletionScript(binNames, roots, subcommands, options) {
|
|
1054
|
+
return binNames.map((binName) => getFishCompletionForBin(binName, roots, subcommands, options)).join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
function getFishCompletionForBin(binName, roots, subcommands, options) {
|
|
1057
|
+
const rootNames = roots.map((item) => item.name).join(" ");
|
|
1058
|
+
return `function __${commandKey(binName)}_seen_command
|
|
1059
|
+
set -l tokens (commandline -opc)
|
|
1060
|
+
for token in $tokens[2..-1]
|
|
1061
|
+
switch $token
|
|
1062
|
+
case ${rootNames}
|
|
1063
|
+
return 0
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
return 1
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
function __${commandKey(binName)}_using_command
|
|
1070
|
+
set -l tokens (commandline -opc)
|
|
1071
|
+
test (count $tokens) -ge 2; and test "$tokens[2]" = "$argv[1]"
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
function __${commandKey(binName)}_using_subcommand
|
|
1075
|
+
set -l tokens (commandline -opc)
|
|
1076
|
+
test (count $tokens) -ge 3; and test "$tokens[2]" = "$argv[1]"; and test "$tokens[3]" = "$argv[2]"
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
complete -c ${binName} -f
|
|
1080
|
+
${formatFishRootCompletions(binName, roots)}
|
|
1081
|
+
${formatFishSubcommandCompletions(binName, subcommands)}
|
|
1082
|
+
${formatFishOptionCompletions(binName, subcommands, options)}
|
|
1083
|
+
${formatFishGlobalOptionCompletions(binName)}`;
|
|
1084
|
+
}
|
|
1085
|
+
function formatFishRootCompletions(binName, roots) {
|
|
1086
|
+
return roots.map(
|
|
1087
|
+
(item) => `complete -c ${binName} -f -n "not __${commandKey(binName)}_seen_command" -a ${quoteFish(item.name)} -d ${quoteFish(item.description ?? "")}`
|
|
1088
|
+
).join("\n");
|
|
1089
|
+
}
|
|
1090
|
+
function formatFishSubcommandCompletions(binName, groups) {
|
|
1091
|
+
return [...groups.entries()].flatMap(
|
|
1092
|
+
([root, items]) => items.map(
|
|
1093
|
+
(item) => `complete -c ${binName} -f -n "__${commandKey(binName)}_using_command ${quoteFish(root)}" -a ${quoteFish(item.name)} -d ${quoteFish(item.description ?? "")}`
|
|
1094
|
+
)
|
|
1095
|
+
).join("\n");
|
|
1096
|
+
}
|
|
1097
|
+
function formatFishOptionCompletions(binName, subcommands, options) {
|
|
1098
|
+
return [...options.entries()].flatMap(([command, items]) => {
|
|
1099
|
+
const parts = command.split(" ");
|
|
1100
|
+
const condition = parts.length === 1 ? `__${commandKey(binName)}_using_command ${quoteFish(parts[0] ?? "")}` : `__${commandKey(binName)}_using_subcommand ${quoteFish(parts[0] ?? "")} ${quoteFish(parts[1] ?? "")}`;
|
|
1101
|
+
if (parts.length > 1 && !(subcommands.get(parts[0] ?? "") ?? []).some((item) => item.name === parts[1])) {
|
|
1102
|
+
return [];
|
|
1103
|
+
}
|
|
1104
|
+
return items.map((item) => {
|
|
1105
|
+
const flags = [item.short ? `-s ${quoteFish(item.short)}` : "", item.long ? `-l ${quoteFish(item.long)}` : ""].filter(Boolean).join(" ");
|
|
1106
|
+
return `complete -c ${binName} -f -n "${condition}" ${flags} -d ${quoteFish(item.description ?? "")}`;
|
|
1107
|
+
});
|
|
1108
|
+
}).join("\n");
|
|
1109
|
+
}
|
|
1110
|
+
function formatFishGlobalOptionCompletions(binName) {
|
|
1111
|
+
return globalOptions.map((item) => {
|
|
1112
|
+
const flags = [item.short ? `-s ${quoteFish(item.short)}` : "", item.long ? `-l ${quoteFish(item.long)}` : ""].filter(Boolean).join(" ");
|
|
1113
|
+
return `complete -c ${binName} -f ${flags} -d ${quoteFish(item.description ?? "")}`;
|
|
1114
|
+
}).join("\n");
|
|
1115
|
+
}
|
|
1116
|
+
function quoteFish(value) {
|
|
1117
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/cli/completion/zsh.ts
|
|
1121
|
+
function getZshCompletionScript(binNames, roots, subcommands, options) {
|
|
1122
|
+
const functionName = getFunctionName(binNames[0] ?? APP_INFO.name);
|
|
1123
|
+
return `#compdef ${binNames.join(" ")}
|
|
1124
|
+
|
|
1125
|
+
${functionName}() {
|
|
1126
|
+
local -a commands
|
|
1127
|
+
commands=(
|
|
1128
|
+
${formatZshItems(roots)}
|
|
1129
|
+
)
|
|
1130
|
+
local -a global_options
|
|
1131
|
+
global_options=(
|
|
1132
|
+
${formatZshOptionItems(globalOptions)}
|
|
1133
|
+
)
|
|
1134
|
+
${formatSubcommandArrays(subcommands)}
|
|
1135
|
+
${formatZshOptionArrays(options)}
|
|
1136
|
+
|
|
1137
|
+
if [[ "$words[CURRENT]" == -* ]]; then
|
|
1138
|
+
_describe '${binNames[0]} options' global_options
|
|
1139
|
+
return
|
|
1140
|
+
fi
|
|
1141
|
+
|
|
1142
|
+
if (( CURRENT == 2 )); then
|
|
1143
|
+
_describe '${binNames[0]} commands' commands
|
|
1144
|
+
return
|
|
1145
|
+
fi
|
|
1146
|
+
|
|
1147
|
+
case $words[2] in
|
|
1148
|
+
${formatZshCompletionCases(binNames[0] ?? APP_INFO.name, subcommands, options)}
|
|
1149
|
+
esac
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
compdef ${functionName} ${binNames.join(" ")}`;
|
|
1153
|
+
}
|
|
1154
|
+
function formatZshItems(items) {
|
|
1155
|
+
return items.map((item) => ` '${escapeZshItem(item)}'`).join("\n");
|
|
1156
|
+
}
|
|
1157
|
+
function formatZshOptionItems(items) {
|
|
1158
|
+
return items.flatMap(
|
|
1159
|
+
(item) => item.names.map((name) => ({
|
|
1160
|
+
description: item.description,
|
|
1161
|
+
name
|
|
1162
|
+
}))
|
|
1163
|
+
).map((item) => ` '${escapeZshItem(item)}'`).join("\n");
|
|
1164
|
+
}
|
|
1165
|
+
function formatSubcommandArrays(groups) {
|
|
1166
|
+
return [...groups.entries()].map(
|
|
1167
|
+
([root, items]) => `
|
|
1168
|
+
local -a ${commandKey(root)}_commands
|
|
1169
|
+
${commandKey(root)}_commands=(
|
|
1170
|
+
${formatZshItems(items)}
|
|
1171
|
+
)`
|
|
1172
|
+
).join("\n");
|
|
1173
|
+
}
|
|
1174
|
+
function formatZshOptionArrays(groups) {
|
|
1175
|
+
return [...groups.entries()].map(
|
|
1176
|
+
([command, items]) => `
|
|
1177
|
+
local -a ${commandKey(command)}_options
|
|
1178
|
+
${commandKey(command)}_options=(
|
|
1179
|
+
${formatZshOptionItems(items)}
|
|
1180
|
+
)`
|
|
1181
|
+
).join("\n");
|
|
1182
|
+
}
|
|
1183
|
+
function formatZshCompletionCases(binName, subcommands, options) {
|
|
1184
|
+
const rootCommands = new Set(
|
|
1185
|
+
[...subcommands.keys(), ...[...options.keys()].map((command) => command.split(" ")[0])].filter(
|
|
1186
|
+
(command) => Boolean(command)
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
return [...rootCommands].map((root) => {
|
|
1190
|
+
const rootOptions = options.get(root);
|
|
1191
|
+
if (rootOptions) {
|
|
1192
|
+
return ` ${root})
|
|
1193
|
+
_describe '${binName} ${root} options' ${commandKey(root)}_options
|
|
1194
|
+
;;`;
|
|
1195
|
+
}
|
|
1196
|
+
const subcommandCases = (subcommands.get(root) ?? []).map((item) => {
|
|
1197
|
+
const key = `${root} ${item.name}`;
|
|
1198
|
+
if (!options.has(key)) return "";
|
|
1199
|
+
return ` ${item.name})
|
|
1200
|
+
_describe '${binName} ${root} ${item.name} options' ${commandKey(key)}_options
|
|
1201
|
+
;;`;
|
|
1202
|
+
}).filter(Boolean).join("\n");
|
|
1203
|
+
return ` ${root})
|
|
1204
|
+
if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
|
|
1205
|
+
_describe '${binName} ${root} commands' ${commandKey(root)}_commands
|
|
1206
|
+
return
|
|
1207
|
+
fi
|
|
1208
|
+
case $words[3] in
|
|
1209
|
+
${subcommandCases}
|
|
1210
|
+
esac
|
|
1211
|
+
;;`;
|
|
1212
|
+
}).join("\n");
|
|
1213
|
+
}
|
|
1214
|
+
function escapeZshItem(item) {
|
|
1215
|
+
const text = item.description ? `${item.name}:${item.description}` : item.name;
|
|
1216
|
+
return text.replace(/'/g, "'\\''");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/commands/completion.ts
|
|
1220
|
+
var completionShells = ["zsh" /* Zsh */, "bash" /* Bash */, "fish" /* Fish */];
|
|
1221
|
+
var completionCommand = defineCommand({
|
|
1222
|
+
name: "completion",
|
|
1223
|
+
description: "Generate shell completion scripts",
|
|
1224
|
+
subcommands: completionShells.map(
|
|
1225
|
+
(shell) => defineSubCommand({
|
|
1226
|
+
name: shell,
|
|
1227
|
+
description: `Generate ${shell} completion script`,
|
|
1228
|
+
action: async () => {
|
|
1229
|
+
}
|
|
1230
|
+
})
|
|
1231
|
+
)
|
|
1232
|
+
});
|
|
1233
|
+
var completionScriptGenerators = {
|
|
1234
|
+
["bash" /* Bash */]: getBashCompletionScript,
|
|
1235
|
+
["fish" /* Fish */]: getFishCompletionScript,
|
|
1236
|
+
["zsh" /* Zsh */]: getZshCompletionScript
|
|
1237
|
+
};
|
|
1238
|
+
var shellMatchers = [
|
|
1239
|
+
{ shell: "zsh" /* Zsh */, matches: (value) => value.includes("zsh" /* Zsh */) },
|
|
1240
|
+
{ shell: "bash" /* Bash */, matches: (value) => value.includes("bash" /* Bash */) },
|
|
1241
|
+
{ shell: "fish" /* Fish */, matches: (value) => value.includes("fish" /* Fish */) }
|
|
1242
|
+
];
|
|
1243
|
+
var silentCompletionInstallers = {
|
|
1244
|
+
["bash" /* Bash */]: installBashCompletionSilent,
|
|
1245
|
+
["fish" /* Fish */]: installFishCompletionSilent,
|
|
1246
|
+
["zsh" /* Zsh */]: async () => {
|
|
1247
|
+
await installZshCompletionSilent();
|
|
1248
|
+
await clearZshCompletionCache();
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
var completionProgram;
|
|
1252
|
+
function createCompletionCommand(program2) {
|
|
1253
|
+
completionProgram = program2;
|
|
1254
|
+
program2.command(completionCommand.name, completionCommand.description).action(async () => {
|
|
1255
|
+
Logger.info(`Available shells: ${completionShells.join(", ")}`);
|
|
1256
|
+
Logger.info(`Usage: ${APP_INFO.name} completion <shell>`);
|
|
1257
|
+
});
|
|
1258
|
+
for (const shellCommand of completionCommand.subcommands) {
|
|
1259
|
+
program2.command(`${completionCommand.name} ${shellCommand.name}`, shellCommand.description).action(async ({ program: program3 }) => {
|
|
1260
|
+
if (!isCompletionShell(shellCommand.name)) {
|
|
1261
|
+
throw new Error(`Unsupported shell: ${shellCommand.name}`);
|
|
1262
|
+
}
|
|
1263
|
+
console.log(await getCompletionScript(program3, shellCommand.name));
|
|
1264
|
+
return 0;
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function detectShell() {
|
|
1269
|
+
const shell = process.env.SHELL || "";
|
|
1270
|
+
return shellMatchers.find((matcher) => matcher.matches(shell))?.shell ?? "zsh" /* Zsh */;
|
|
1271
|
+
}
|
|
1272
|
+
async function reinstallCompletionSilently() {
|
|
1273
|
+
const configManager = new ConfigManager();
|
|
1274
|
+
if (!configManager.isCompletionInstalled()) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
const shell = detectShell();
|
|
1278
|
+
try {
|
|
1279
|
+
await silentCompletionInstallers[shell]();
|
|
1280
|
+
return true;
|
|
1281
|
+
} catch {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
async function installZshCompletionSilent() {
|
|
1286
|
+
const homeDir = homedir2();
|
|
1287
|
+
const possibleDirs = [
|
|
1288
|
+
join2(homeDir, ".oh-my-zsh", "completions"),
|
|
1289
|
+
join2(homeDir, ".zsh", "completions"),
|
|
1290
|
+
join2(homeDir, ".config", "zsh", "completions"),
|
|
1291
|
+
join2(homeDir, ".local", "share", "zsh", "site-functions"),
|
|
1292
|
+
"/usr/local/share/zsh/site-functions"
|
|
1293
|
+
];
|
|
1294
|
+
let targetDir = null;
|
|
1295
|
+
for (const dir of possibleDirs) {
|
|
1296
|
+
if (existsSync4(dir)) {
|
|
1297
|
+
try {
|
|
1298
|
+
accessSync(dir, constants.W_OK);
|
|
1299
|
+
targetDir = dir;
|
|
1300
|
+
break;
|
|
1301
|
+
} catch {
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!targetDir) {
|
|
1306
|
+
targetDir = join2(homeDir, ".zsh", "completions");
|
|
1307
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1308
|
+
}
|
|
1309
|
+
const completionFile = join2(targetDir, `_${APP_INFO.name}`);
|
|
1310
|
+
writeFileSync2(completionFile, await getCurrentCompletionScript("zsh" /* Zsh */));
|
|
1311
|
+
}
|
|
1312
|
+
async function installBashCompletionSilent() {
|
|
1313
|
+
const homeDir = homedir2();
|
|
1314
|
+
const possibleDirs = [
|
|
1315
|
+
join2(homeDir, ".bash_completion.d"),
|
|
1316
|
+
join2(homeDir, ".local", "share", "bash-completion", "completions"),
|
|
1317
|
+
"/usr/local/etc/bash_completion.d",
|
|
1318
|
+
"/etc/bash_completion.d"
|
|
1319
|
+
];
|
|
1320
|
+
let targetDir = null;
|
|
1321
|
+
for (const dir of possibleDirs) {
|
|
1322
|
+
if (existsSync4(dir)) {
|
|
1323
|
+
try {
|
|
1324
|
+
accessSync(dir, constants.W_OK);
|
|
1325
|
+
targetDir = dir;
|
|
1326
|
+
break;
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!targetDir) {
|
|
1332
|
+
targetDir = join2(homeDir, ".bash_completion.d");
|
|
1333
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1334
|
+
}
|
|
1335
|
+
const completionFile = join2(targetDir, APP_INFO.name);
|
|
1336
|
+
writeFileSync2(completionFile, await getCurrentCompletionScript("bash" /* Bash */));
|
|
1337
|
+
}
|
|
1338
|
+
async function installFishCompletionSilent() {
|
|
1339
|
+
const homeDir = homedir2();
|
|
1340
|
+
const targetDir = join2(homeDir, ".config", "fish", "completions");
|
|
1341
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1342
|
+
const completionFile = join2(targetDir, `${APP_INFO.name}.fish`);
|
|
1343
|
+
writeFileSync2(completionFile, await getCurrentCompletionScript("fish" /* Fish */));
|
|
1344
|
+
}
|
|
1345
|
+
async function clearZshCompletionCache() {
|
|
1346
|
+
const homeDir = homedir2();
|
|
1347
|
+
const zshCacheFile = join2(homeDir, ".zcompdump");
|
|
1348
|
+
try {
|
|
1349
|
+
if (existsSync4(zshCacheFile)) {
|
|
1350
|
+
const fs3 = await import("fs");
|
|
1351
|
+
fs3.unlinkSync(zshCacheFile);
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function isCompletionShell(value) {
|
|
1357
|
+
return completionShells.includes(value);
|
|
1358
|
+
}
|
|
1359
|
+
async function getCurrentCompletionScript(shell) {
|
|
1360
|
+
if (!completionProgram) {
|
|
1361
|
+
throw new Error("Completion program not initialized");
|
|
1362
|
+
}
|
|
1363
|
+
return getCompletionScript(completionProgram, shell);
|
|
1364
|
+
}
|
|
1365
|
+
async function getCompletionScript(program2, shell) {
|
|
1366
|
+
const commands = (await program2.getAllCommands()).filter(isVisibleCompletionCommand);
|
|
1367
|
+
const roots = getRootCommands(commands);
|
|
1368
|
+
const subcommands = getSubcommandGroups(commands);
|
|
1369
|
+
subcommands.set(
|
|
1370
|
+
completionCommand.name,
|
|
1371
|
+
completionShells.map((shell2) => ({ name: shell2, description: `Generate ${shell2} completion` }))
|
|
1372
|
+
);
|
|
1373
|
+
const options = getOptionGroups(commands);
|
|
1374
|
+
return completionScriptGenerators[shell](getCompletionBinNames(program2.getBin()), roots, subcommands, options);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/core/google-sheets.service.ts
|
|
1378
|
+
import { OAuth2Client as OAuth2Client4 } from "google-auth-library";
|
|
1379
|
+
import { GoogleSpreadsheet } from "google-spreadsheet";
|
|
1380
|
+
var GoogleSheetsService = class {
|
|
1381
|
+
constructor(config) {
|
|
1382
|
+
this.config = config;
|
|
1383
|
+
this.auth = new OAuth2Client4(config.oauthCredentials.client_id, config.oauthCredentials.client_secret);
|
|
1384
|
+
this.auth.setCredentials({
|
|
1385
|
+
access_token: config.oauthCredentials.access_token,
|
|
1386
|
+
refresh_token: config.oauthCredentials.refresh_token,
|
|
1387
|
+
expiry_date: config.oauthCredentials.expiry_date
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
config;
|
|
1391
|
+
doc = null;
|
|
1392
|
+
auth;
|
|
1393
|
+
async ensureConnection() {
|
|
1394
|
+
if (!this.doc) {
|
|
1395
|
+
this.doc = new GoogleSpreadsheet(this.config.spreadsheetId, this.auth);
|
|
1396
|
+
await this.doc.loadInfo();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
async getSheetInfo() {
|
|
1400
|
+
await this.ensureConnection();
|
|
1401
|
+
if (!this.doc) {
|
|
1402
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
title: this.doc.title,
|
|
1406
|
+
sheets: this.doc.sheetsByIndex.map((sheet, index) => ({
|
|
1407
|
+
title: sheet.title,
|
|
1408
|
+
index,
|
|
1409
|
+
sheetId: sheet.sheetId
|
|
1410
|
+
}))
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
async getSheetData(sheetName, includeFormulas = false) {
|
|
1414
|
+
await this.ensureConnection();
|
|
1415
|
+
if (!this.doc) {
|
|
1416
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1417
|
+
}
|
|
1418
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1419
|
+
if (!sheet) {
|
|
1420
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1421
|
+
}
|
|
1422
|
+
await sheet.loadCells();
|
|
1423
|
+
let lastRow = 0;
|
|
1424
|
+
let lastCol = 0;
|
|
1425
|
+
for (let row = 0; row < sheet.rowCount; row++) {
|
|
1426
|
+
for (let col = 0; col < sheet.columnCount; col++) {
|
|
1427
|
+
const cell = sheet.getCell(row, col);
|
|
1428
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
1429
|
+
if (value !== "") {
|
|
1430
|
+
lastRow = Math.max(lastRow, row);
|
|
1431
|
+
lastCol = Math.max(lastCol, col);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (lastRow === 0 && lastCol === 0) {
|
|
1436
|
+
const firstCell = sheet.getCell(0, 0);
|
|
1437
|
+
const firstValue = includeFormulas && firstCell.formula ? firstCell.formula : firstCell.formattedValue ?? "";
|
|
1438
|
+
if (firstValue === "") {
|
|
1439
|
+
return [];
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const data = [];
|
|
1443
|
+
for (let row = 0; row <= lastRow; row++) {
|
|
1444
|
+
const rowData = [];
|
|
1445
|
+
for (let col = 0; col <= lastCol; col++) {
|
|
1446
|
+
const cell = sheet.getCell(row, col);
|
|
1447
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
1448
|
+
rowData.push(value);
|
|
1449
|
+
}
|
|
1450
|
+
data.push(rowData);
|
|
1451
|
+
}
|
|
1452
|
+
return data;
|
|
1453
|
+
}
|
|
1454
|
+
async addSheet(sheetName) {
|
|
1455
|
+
await this.ensureConnection();
|
|
1456
|
+
if (!this.doc) {
|
|
1457
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1458
|
+
}
|
|
1459
|
+
await this.doc.addSheet({ title: sheetName });
|
|
1460
|
+
}
|
|
1461
|
+
async removeSheet(sheetName) {
|
|
1462
|
+
await this.ensureConnection();
|
|
1463
|
+
if (!this.doc) {
|
|
1464
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1465
|
+
}
|
|
1466
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1467
|
+
if (!sheet) {
|
|
1468
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1469
|
+
}
|
|
1470
|
+
await sheet.delete();
|
|
1471
|
+
}
|
|
1472
|
+
async renameSheet(oldName, newName) {
|
|
1473
|
+
await this.ensureConnection();
|
|
1474
|
+
if (!this.doc) {
|
|
1475
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1476
|
+
}
|
|
1477
|
+
const sheet = this.doc.sheetsByTitle[oldName];
|
|
1478
|
+
if (!sheet) {
|
|
1479
|
+
throw new Error(`Sheet '${oldName}' not found`);
|
|
1480
|
+
}
|
|
1481
|
+
await sheet.updateProperties({ title: newName });
|
|
1482
|
+
}
|
|
1483
|
+
async copySheet(sheetName, newSheetName) {
|
|
1484
|
+
await this.ensureConnection();
|
|
1485
|
+
if (!this.doc) {
|
|
1486
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1487
|
+
}
|
|
1488
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1489
|
+
if (!sheet) {
|
|
1490
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1491
|
+
}
|
|
1492
|
+
await sheet.duplicate({ title: newSheetName });
|
|
1493
|
+
}
|
|
1494
|
+
async writeCell(sheetName, cell, value) {
|
|
1495
|
+
await this.ensureConnection();
|
|
1496
|
+
if (!this.doc) {
|
|
1497
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1498
|
+
}
|
|
1499
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1500
|
+
if (!sheet) {
|
|
1501
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1502
|
+
}
|
|
1503
|
+
await sheet.loadCells(cell);
|
|
1504
|
+
const targetCell = sheet.getCellByA1(cell);
|
|
1505
|
+
targetCell.value = value;
|
|
1506
|
+
await sheet.saveUpdatedCells();
|
|
1507
|
+
}
|
|
1508
|
+
async writeCellRange(sheetName, range, values, noPreserve) {
|
|
1509
|
+
await this.ensureConnection();
|
|
1510
|
+
if (!this.doc) {
|
|
1511
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1512
|
+
}
|
|
1513
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1514
|
+
if (!sheet) {
|
|
1515
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1516
|
+
}
|
|
1517
|
+
await sheet.loadCells(range);
|
|
1518
|
+
const [start, end] = range.split(":");
|
|
1519
|
+
const startCell = sheet.getCellByA1(start);
|
|
1520
|
+
const endCell = sheet.getCellByA1(end);
|
|
1521
|
+
let valueRowIndex = 0;
|
|
1522
|
+
for (let row = startCell.rowIndex; row <= endCell.rowIndex; row++) {
|
|
1523
|
+
let valueColIndex = 0;
|
|
1524
|
+
for (let col = startCell.columnIndex; col <= endCell.columnIndex; col++) {
|
|
1525
|
+
const cell = sheet.getCell(row, col);
|
|
1526
|
+
const cellWithRawData = cell;
|
|
1527
|
+
const hasDataValidation = cellWithRawData._rawData?.dataValidation !== void 0;
|
|
1528
|
+
const hasFormula = cellWithRawData._rawData?.userEnteredValue?.formulaValue !== void 0;
|
|
1529
|
+
const isCellEmpty = !cell.value || cell.value === "";
|
|
1530
|
+
if (values[valueRowIndex] && values[valueRowIndex][valueColIndex] !== void 0) {
|
|
1531
|
+
const newValue = values[valueRowIndex][valueColIndex];
|
|
1532
|
+
const isNewValueEmpty = newValue === "" || newValue === null;
|
|
1533
|
+
if (noPreserve) {
|
|
1534
|
+
cell.value = newValue;
|
|
1535
|
+
} else {
|
|
1536
|
+
if (!(hasDataValidation && isCellEmpty && isNewValueEmpty) && !hasFormula) {
|
|
1537
|
+
cell.value = newValue;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
valueColIndex++;
|
|
1542
|
+
}
|
|
1543
|
+
valueRowIndex++;
|
|
1544
|
+
}
|
|
1545
|
+
await sheet.saveUpdatedCells();
|
|
1546
|
+
}
|
|
1547
|
+
async appendRow(sheetName, values) {
|
|
1548
|
+
await this.ensureConnection();
|
|
1549
|
+
if (!this.doc) {
|
|
1550
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1551
|
+
}
|
|
1552
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1553
|
+
if (!sheet) {
|
|
1554
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1555
|
+
}
|
|
1556
|
+
await sheet.addRow(values);
|
|
1557
|
+
}
|
|
1558
|
+
async getSheetDataRange(sheetName, range, includeFormulas = false) {
|
|
1559
|
+
await this.ensureConnection();
|
|
1560
|
+
if (!this.doc) {
|
|
1561
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1562
|
+
}
|
|
1563
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1564
|
+
if (!sheet) {
|
|
1565
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1566
|
+
}
|
|
1567
|
+
await sheet.loadCells(range);
|
|
1568
|
+
const [start, end] = range.split(":");
|
|
1569
|
+
const startCell = sheet.getCellByA1(start);
|
|
1570
|
+
const endCell = sheet.getCellByA1(end);
|
|
1571
|
+
const data = [];
|
|
1572
|
+
for (let row = startCell.rowIndex; row <= endCell.rowIndex; row++) {
|
|
1573
|
+
const rowData = [];
|
|
1574
|
+
for (let col = startCell.columnIndex; col <= endCell.columnIndex; col++) {
|
|
1575
|
+
const cell = sheet.getCell(row, col);
|
|
1576
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
1577
|
+
rowData.push(value);
|
|
1578
|
+
}
|
|
1579
|
+
data.push(rowData);
|
|
1580
|
+
}
|
|
1581
|
+
return data;
|
|
1582
|
+
}
|
|
1583
|
+
async insertRows(sheetName, range, inheritFromBefore = false) {
|
|
1584
|
+
await this.ensureConnection();
|
|
1585
|
+
if (!this.doc) {
|
|
1586
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1587
|
+
}
|
|
1588
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1589
|
+
if (!sheet) {
|
|
1590
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1591
|
+
}
|
|
1592
|
+
await sheet.insertDimension("ROWS", range, inheritFromBefore);
|
|
1593
|
+
}
|
|
1594
|
+
async deleteRows(sheetName, range) {
|
|
1595
|
+
await this.ensureConnection();
|
|
1596
|
+
if (!this.doc) {
|
|
1597
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1598
|
+
}
|
|
1599
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1600
|
+
if (!sheet) {
|
|
1601
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1602
|
+
}
|
|
1603
|
+
await sheet._makeSingleUpdateRequest("deleteDimension", {
|
|
1604
|
+
range: {
|
|
1605
|
+
sheetId: sheet.sheetId,
|
|
1606
|
+
dimension: "ROWS",
|
|
1607
|
+
startIndex: range.startIndex,
|
|
1608
|
+
endIndex: range.endIndex
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
async getRowFormulas(sheetName, rowIndex) {
|
|
1613
|
+
await this.ensureConnection();
|
|
1614
|
+
if (!this.doc) {
|
|
1615
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1616
|
+
}
|
|
1617
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1618
|
+
if (!sheet) {
|
|
1619
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1620
|
+
}
|
|
1621
|
+
await sheet.loadCells(`A${rowIndex + 1}:${rowIndex + 1}`);
|
|
1622
|
+
const formulas = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (let col = 0; col < sheet.columnCount; col++) {
|
|
1624
|
+
const cell = sheet.getCell(rowIndex, col);
|
|
1625
|
+
if (cell.formula) {
|
|
1626
|
+
formulas.set(col, cell.formula);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return formulas;
|
|
1630
|
+
}
|
|
1631
|
+
async copyRowFormulas(sheetName, sourceRowIndex, targetRowIndex) {
|
|
1632
|
+
await this.ensureConnection();
|
|
1633
|
+
if (!this.doc) {
|
|
1634
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1635
|
+
}
|
|
1636
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1637
|
+
if (!sheet) {
|
|
1638
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1639
|
+
}
|
|
1640
|
+
const formulas = await this.getRowFormulas(sheetName, sourceRowIndex);
|
|
1641
|
+
if (formulas.size === 0) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
await sheet.loadCells(`A${targetRowIndex + 1}:${targetRowIndex + 1}`);
|
|
1645
|
+
const rowDiff = targetRowIndex - sourceRowIndex;
|
|
1646
|
+
for (const [col, formula] of formulas) {
|
|
1647
|
+
const cell = sheet.getCell(targetRowIndex, col);
|
|
1648
|
+
const adjustedFormula = this.adjustFormulaReferences(formula, rowDiff);
|
|
1649
|
+
cell.formula = adjustedFormula;
|
|
1650
|
+
}
|
|
1651
|
+
await sheet.saveUpdatedCells();
|
|
1652
|
+
}
|
|
1653
|
+
async copyRowFormulasBulk(sheetName, sourceRowIndex, startTargetRowIndex, count) {
|
|
1654
|
+
await this.ensureConnection();
|
|
1655
|
+
if (!this.doc) {
|
|
1656
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
1657
|
+
}
|
|
1658
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
1659
|
+
if (!sheet) {
|
|
1660
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
1661
|
+
}
|
|
1662
|
+
const formulas = await this.getRowFormulas(sheetName, sourceRowIndex);
|
|
1663
|
+
if (formulas.size === 0) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const endTargetRowIndex = startTargetRowIndex + count - 1;
|
|
1667
|
+
await sheet.loadCells(`A${startTargetRowIndex + 1}:${endTargetRowIndex + 1}`);
|
|
1668
|
+
for (let i = 0; i < count; i++) {
|
|
1669
|
+
const targetRowIndex = startTargetRowIndex + i;
|
|
1670
|
+
const rowDiff = targetRowIndex - sourceRowIndex;
|
|
1671
|
+
for (const [col, formula] of formulas) {
|
|
1672
|
+
const cell = sheet.getCell(targetRowIndex, col);
|
|
1673
|
+
const adjustedFormula = this.adjustFormulaReferences(formula, rowDiff);
|
|
1674
|
+
cell.formula = adjustedFormula;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
await sheet.saveUpdatedCells();
|
|
1678
|
+
}
|
|
1679
|
+
adjustFormulaReferences(formula, rowDiff) {
|
|
1680
|
+
return formula.replace(/([A-Z]+)(\d+)/g, (_match, colLetter, rowNum) => {
|
|
1681
|
+
const newRow = parseInt(rowNum, 10) + rowDiff;
|
|
1682
|
+
return `${colLetter}${newRow}`;
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
// src/core/command-helpers.ts
|
|
1688
|
+
async function getGoogleSheetsService() {
|
|
1689
|
+
const configManager = new ConfigManager();
|
|
1690
|
+
const activeAccount = configManager.getActiveAccount();
|
|
1691
|
+
if (!activeAccount) {
|
|
1692
|
+
Logger.error("No active account set.");
|
|
1693
|
+
Logger.info("Use: gsheet account add");
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
}
|
|
1696
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
1697
|
+
if (!activeSpreadsheetName) {
|
|
1698
|
+
Logger.error("No active spreadsheet set.");
|
|
1699
|
+
Logger.info("Use: gsheet spreadsheet select");
|
|
1700
|
+
process.exit(1);
|
|
1701
|
+
}
|
|
1702
|
+
const spreadsheet = configManager.getSpreadsheet(activeAccount.email, activeSpreadsheetName);
|
|
1703
|
+
if (!spreadsheet) {
|
|
1704
|
+
Logger.error(`Spreadsheet '${activeSpreadsheetName}' not found. Use "gsheet spreadsheet add" to add one.`);
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
const refreshedCredentials = await configManager.getRefreshedCredentials(activeAccount.email);
|
|
1708
|
+
return new GoogleSheetsService({
|
|
1709
|
+
spreadsheetId: spreadsheet.spreadsheet_id,
|
|
1710
|
+
oauthCredentials: refreshedCredentials
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
function getActiveSheetName(sheetName) {
|
|
1714
|
+
if (sheetName) {
|
|
1715
|
+
return sheetName;
|
|
1716
|
+
}
|
|
1717
|
+
const configManager = new ConfigManager();
|
|
1718
|
+
const activeAccount = configManager.getActiveAccount();
|
|
1719
|
+
if (!activeAccount) {
|
|
1720
|
+
Logger.error("No active account set.");
|
|
1721
|
+
Logger.info("Use: gsheet account add");
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1724
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
1725
|
+
if (!activeSpreadsheetName) {
|
|
1726
|
+
Logger.error("No active spreadsheet set.");
|
|
1727
|
+
Logger.info("Use: gsheet spreadsheet select");
|
|
1728
|
+
process.exit(1);
|
|
1729
|
+
}
|
|
1730
|
+
const activeSheetName = configManager.getActiveSheetName(activeAccount.email, activeSpreadsheetName);
|
|
1731
|
+
if (!activeSheetName) {
|
|
1732
|
+
Logger.error("No active sheet set.");
|
|
1733
|
+
Logger.info("Use: gsheet sheet select");
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
return activeSheetName;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/commands/sheet/data_operations/append.ts
|
|
1740
|
+
var appendCommand = defineSubCommand({
|
|
1741
|
+
name: "append",
|
|
1742
|
+
description: "Append a new row to the end of the sheet",
|
|
1743
|
+
flags: [
|
|
1744
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
1745
|
+
flag.string("--value", "Values to append (comma-separated)", { alias: "-v", required: true })
|
|
1746
|
+
],
|
|
1747
|
+
errorMessage: "Failed to append row",
|
|
1748
|
+
action: async ({ options }) => {
|
|
1749
|
+
const sheetsService = await getGoogleSheetsService();
|
|
1750
|
+
const sheetName = getActiveSheetName(options.name);
|
|
1751
|
+
const values = options.value.split(",").map((v) => v.trim());
|
|
1752
|
+
Logger.loading(`Appending row to '${sheetName}'...`);
|
|
1753
|
+
await sheetsService.appendRow(sheetName, values);
|
|
1754
|
+
Logger.success(`Row appended to '${sheetName}' successfully`);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
// src/utils/cell.ts
|
|
1759
|
+
function columnLetterToNumber(colLetter) {
|
|
1760
|
+
let result = 0;
|
|
1761
|
+
const letters = colLetter.toUpperCase();
|
|
1762
|
+
for (let i = 0; i < letters.length; i++) {
|
|
1763
|
+
result = result * 26 + (letters.charCodeAt(i) - 64);
|
|
1764
|
+
}
|
|
1765
|
+
return result - 1;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/commands/sheet/data_operations/write.ts
|
|
1769
|
+
var writeCommand = defineSubCommand({
|
|
1770
|
+
name: "write",
|
|
1771
|
+
description: "Write to a specific cell or range of cells",
|
|
1772
|
+
flags: [
|
|
1773
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
1774
|
+
flag.string("--cell", "Cell address (e.g., A1) - required if --range not provided", { alias: "-c" }),
|
|
1775
|
+
flag.string("--range", "Range (e.g., A1:B2) - required if --cell not provided", { alias: "-r" }),
|
|
1776
|
+
flag.string("--value", "Value to write (use , for columns, ; for rows)", { alias: "-v", required: true }),
|
|
1777
|
+
flag.boolean("--no-preserve", "Overwrite cells with formulas or data validation")
|
|
1778
|
+
],
|
|
1779
|
+
errorMessage: "Failed to write to sheet",
|
|
1780
|
+
action: async ({ options }) => {
|
|
1781
|
+
if (!options.cell && !options.range) {
|
|
1782
|
+
Logger.error("Either --cell or --range must be specified");
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
if (options.cell && options.range) {
|
|
1786
|
+
Logger.error("Cannot use both --cell and --range at the same time");
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
const sheetsService = await getGoogleSheetsService();
|
|
1790
|
+
const sheetName = getActiveSheetName(options.name);
|
|
1791
|
+
if (options.cell) {
|
|
1792
|
+
Logger.loading(`Writing to cell ${options.cell}...`);
|
|
1793
|
+
await sheetsService.writeCell(sheetName, options.cell, options.value);
|
|
1794
|
+
Logger.success(`Cell ${options.cell} updated successfully`);
|
|
1795
|
+
} else if (options.range) {
|
|
1796
|
+
let values;
|
|
1797
|
+
if (options.value.trim().startsWith("[")) {
|
|
1798
|
+
try {
|
|
1799
|
+
values = JSON.parse(options.value);
|
|
1800
|
+
if (!Array.isArray(values) || !Array.isArray(values[0])) {
|
|
1801
|
+
throw new Error("Value must be a 2D array");
|
|
1802
|
+
}
|
|
1803
|
+
} catch (_error) {
|
|
1804
|
+
Logger.error('Invalid JSON array format. Expected 2D array like [["a","b"],["c","d"]]');
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
}
|
|
1807
|
+
} else {
|
|
1808
|
+
const rows = options.value.split(";").map((row) => row.trim());
|
|
1809
|
+
values = rows.map(
|
|
1810
|
+
(row) => row.split(",").map((cell) => {
|
|
1811
|
+
const trimmed = cell.trim();
|
|
1812
|
+
const numericValue = trimmed.replace(",", ".");
|
|
1813
|
+
if (!Number.isNaN(Number(numericValue)) && numericValue !== "") {
|
|
1814
|
+
return Number(numericValue);
|
|
1815
|
+
}
|
|
1816
|
+
return trimmed;
|
|
1817
|
+
})
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
const rangeParts = options.range.split(":");
|
|
1821
|
+
if (rangeParts.length === 2) {
|
|
1822
|
+
const [startCell, endCell] = rangeParts;
|
|
1823
|
+
const startMatch = startCell.match(/^([A-Z]+)(\d+)$/);
|
|
1824
|
+
const endMatch = endCell.match(/^([A-Z]+)(\d+)$/);
|
|
1825
|
+
if (startMatch && endMatch) {
|
|
1826
|
+
const startRow = parseInt(startMatch[2], 10);
|
|
1827
|
+
const endRow = parseInt(endMatch[2], 10);
|
|
1828
|
+
const startCol = startMatch[1];
|
|
1829
|
+
const endCol = endMatch[1];
|
|
1830
|
+
const expectedRows = endRow - startRow + 1;
|
|
1831
|
+
const expectedCols = columnLetterToNumber(endCol) - columnLetterToNumber(startCol) + 1;
|
|
1832
|
+
const actualRows = values.length;
|
|
1833
|
+
const actualCols = Math.max(...values.map((row) => row.length));
|
|
1834
|
+
if (actualRows !== expectedRows || actualCols !== expectedCols) {
|
|
1835
|
+
Logger.error(
|
|
1836
|
+
`Dimension mismatch: Range ${options.range} expects ${expectedRows}x${expectedCols} but got ${actualRows}x${actualCols} values`
|
|
1837
|
+
);
|
|
1838
|
+
Logger.info(`Tip: Provide ${expectedRows} rows with ${expectedCols} columns each`);
|
|
1839
|
+
process.exit(1);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const noPreserve = options.preserve === false;
|
|
1844
|
+
Logger.loading(`Writing to range ${options.range}...`);
|
|
1845
|
+
await sheetsService.writeCellRange(sheetName, options.range, values, noPreserve);
|
|
1846
|
+
Logger.success(`Range ${options.range} updated successfully`);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// src/commands/sheet/import_export/export.ts
|
|
1852
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1853
|
+
|
|
1854
|
+
// src/utils/formatters.ts
|
|
1855
|
+
function formatAsMarkdown(data) {
|
|
1856
|
+
if (data.length === 0) return "";
|
|
1857
|
+
const [headers, ...rows] = data;
|
|
1858
|
+
const colWidths = headers.map((_, colIndex) => Math.max(...data.map((row) => (row[colIndex] || "").length)));
|
|
1859
|
+
const separator = `| ${colWidths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
1860
|
+
const formatRow = (row) => `| ${row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" | ")} |`;
|
|
1861
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)].join("\n");
|
|
1862
|
+
}
|
|
1863
|
+
function formatAsCSV(data) {
|
|
1864
|
+
return data.map(
|
|
1865
|
+
(row) => row.map((cell) => {
|
|
1866
|
+
const value = cell || "";
|
|
1867
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
1868
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1869
|
+
}
|
|
1870
|
+
return value;
|
|
1871
|
+
}).join(",")
|
|
1872
|
+
).join("\n");
|
|
1873
|
+
}
|
|
1874
|
+
function formatAsJSON(data) {
|
|
1875
|
+
if (data.length === 0) return "[]";
|
|
1876
|
+
const [headers, ...rows] = data;
|
|
1877
|
+
const jsonData = rows.map((row) => {
|
|
1878
|
+
const obj = {};
|
|
1879
|
+
headers.forEach((header, index) => {
|
|
1880
|
+
obj[header] = row[index] || "";
|
|
1881
|
+
});
|
|
1882
|
+
return obj;
|
|
1883
|
+
});
|
|
1884
|
+
return JSON.stringify(jsonData, null, 2);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/commands/sheet/import_export/export.ts
|
|
1888
|
+
var ExportFormat = /* @__PURE__ */ ((ExportFormat2) => {
|
|
1889
|
+
ExportFormat2["Csv"] = "csv";
|
|
1890
|
+
ExportFormat2["Json"] = "json";
|
|
1891
|
+
return ExportFormat2;
|
|
1892
|
+
})(ExportFormat || {});
|
|
1893
|
+
var exportFormats = Object.values(ExportFormat);
|
|
1894
|
+
var exportFormatters = {
|
|
1895
|
+
["csv" /* Csv */]: formatAsCSV,
|
|
1896
|
+
["json" /* Json */]: formatAsJSON
|
|
1897
|
+
};
|
|
1898
|
+
var exportCommand = defineSubCommand({
|
|
1899
|
+
name: "export",
|
|
1900
|
+
description: "Export sheet data to JSON or CSV format",
|
|
1901
|
+
flags: [
|
|
1902
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
1903
|
+
flag.string("--range", "Range to export (optional)", { alias: "-r" }),
|
|
1904
|
+
flag.string("--format", "Export format", { alias: "-f", required: true }),
|
|
1905
|
+
flag.string("--output", "Output file path", { alias: "-o" })
|
|
1906
|
+
],
|
|
1907
|
+
errorMessage: "Failed to export data",
|
|
1908
|
+
action: async ({ options }) => {
|
|
1909
|
+
if (!isExportFormat(options.format)) {
|
|
1910
|
+
Logger.error(`Invalid format '${options.format}'. Valid formats: ${exportFormats.join(", ")}`);
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
}
|
|
1913
|
+
const sheetsService = await getGoogleSheetsService();
|
|
1914
|
+
const sheetName = getActiveSheetName(options.name);
|
|
1915
|
+
Logger.loading(`Exporting data from '${sheetName}'${options.range ? ` (range: ${options.range})` : ""}...`);
|
|
1916
|
+
let data;
|
|
1917
|
+
if (options.range) {
|
|
1918
|
+
data = await sheetsService.getSheetDataRange(sheetName, options.range);
|
|
1919
|
+
} else {
|
|
1920
|
+
data = await sheetsService.getSheetData(sheetName);
|
|
1921
|
+
}
|
|
1922
|
+
if (data.length === 0) {
|
|
1923
|
+
Logger.warning("No data to export");
|
|
1924
|
+
process.exit(0);
|
|
1925
|
+
}
|
|
1926
|
+
const output = exportFormatters[options.format](data);
|
|
1927
|
+
if (options.output) {
|
|
1928
|
+
writeFileSync3(options.output, output, "utf-8");
|
|
1929
|
+
Logger.success(`Data exported to ${options.output}`);
|
|
1930
|
+
} else {
|
|
1931
|
+
Logger.success("Exported data:\n");
|
|
1932
|
+
Logger.plain(output);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
function isExportFormat(value) {
|
|
1937
|
+
return exportFormats.includes(value);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/commands/sheet/import_export/import.ts
|
|
1941
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1942
|
+
|
|
1943
|
+
// src/utils/csv.ts
|
|
1944
|
+
function parseCSV(content) {
|
|
1945
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
1946
|
+
const result = [];
|
|
1947
|
+
for (const line of lines) {
|
|
1948
|
+
const row = [];
|
|
1949
|
+
let current = "";
|
|
1950
|
+
let inQuotes = false;
|
|
1951
|
+
for (let i = 0; i < line.length; i++) {
|
|
1952
|
+
const char = line[i];
|
|
1953
|
+
if (char === '"') {
|
|
1954
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
1955
|
+
current += '"';
|
|
1956
|
+
i++;
|
|
1957
|
+
} else {
|
|
1958
|
+
inQuotes = !inQuotes;
|
|
1959
|
+
}
|
|
1960
|
+
} else if (char === "," && !inQuotes) {
|
|
1961
|
+
row.push(current.trim());
|
|
1962
|
+
current = "";
|
|
1963
|
+
} else {
|
|
1964
|
+
current += char;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
row.push(current.trim());
|
|
1968
|
+
result.push(row);
|
|
1969
|
+
}
|
|
1970
|
+
return result;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/commands/sheet/import_export/import.ts
|
|
1974
|
+
var importCommand = defineSubCommand({
|
|
1975
|
+
name: "import",
|
|
1976
|
+
description: "Import CSV file to a sheet",
|
|
1977
|
+
flags: [
|
|
1978
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
1979
|
+
flag.string("--file", "CSV file path", { alias: "-f", required: true }),
|
|
1980
|
+
flag.boolean("--skip-header", "Skip header row when importing")
|
|
1981
|
+
],
|
|
1982
|
+
errorMessage: "Failed to import data",
|
|
1983
|
+
action: async ({ options }) => {
|
|
1984
|
+
const sheetsService = await getGoogleSheetsService();
|
|
1985
|
+
const sheetName = getActiveSheetName(options.name);
|
|
1986
|
+
Logger.loading(`Reading CSV file '${options.file}'...`);
|
|
1987
|
+
const csvContent = readFileSync3(options.file, "utf-8");
|
|
1988
|
+
const data = parseCSV(csvContent);
|
|
1989
|
+
if (data.length === 0) {
|
|
1990
|
+
Logger.warning("CSV file is empty");
|
|
1991
|
+
process.exit(0);
|
|
1992
|
+
}
|
|
1993
|
+
const dataToImport = options.skipHeader ? data.slice(1) : data;
|
|
1994
|
+
if (dataToImport.length === 0) {
|
|
1995
|
+
Logger.warning("No data to import after skipping header");
|
|
1996
|
+
process.exit(0);
|
|
1997
|
+
}
|
|
1998
|
+
Logger.loading(`Importing ${dataToImport.length} rows to '${sheetName}'...`);
|
|
1999
|
+
if (options.skipHeader) {
|
|
2000
|
+
if (dataToImport.length > 0) {
|
|
2001
|
+
const firstRowRange = `A1:${String.fromCharCode(65 + dataToImport[0].length - 1)}1`;
|
|
2002
|
+
await sheetsService.writeCellRange(sheetName, firstRowRange, [dataToImport[0]]);
|
|
2003
|
+
for (let i = 1; i < dataToImport.length; i++) {
|
|
2004
|
+
await sheetsService.appendRow(sheetName, dataToImport[i]);
|
|
2005
|
+
if ((i + 1) % 10 === 0) {
|
|
2006
|
+
Logger.loading(`Imported ${i + 1}/${dataToImport.length} rows...`);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
} else {
|
|
2011
|
+
if (data.length > 0) {
|
|
2012
|
+
const headerRange = `A1:${String.fromCharCode(65 + data[0].length - 1)}1`;
|
|
2013
|
+
await sheetsService.writeCellRange(sheetName, headerRange, [data[0]]);
|
|
2014
|
+
for (let i = 1; i < data.length; i++) {
|
|
2015
|
+
await sheetsService.appendRow(sheetName, data[i]);
|
|
2016
|
+
if ((i + 1) % 10 === 0) {
|
|
2017
|
+
Logger.loading(`Imported ${i + 1}/${data.length} rows...`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
Logger.success(`Successfully imported ${dataToImport.length} rows to '${sheetName}'`);
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
// src/commands/sheet/list.ts
|
|
2027
|
+
var listCommand = defineSubCommand({
|
|
2028
|
+
name: "list",
|
|
2029
|
+
description: "List all sheets in a spreadsheet",
|
|
2030
|
+
flags: [flag.string("--output", "Output format", { alias: "-o" })],
|
|
2031
|
+
errorMessage: "Failed to list sheets",
|
|
2032
|
+
action: async ({ options }) => {
|
|
2033
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2034
|
+
if (options.output !== "json") {
|
|
2035
|
+
Logger.loading("Fetching spreadsheet info...");
|
|
2036
|
+
}
|
|
2037
|
+
const info = await sheetsService.getSheetInfo();
|
|
2038
|
+
if (options.output === "json") {
|
|
2039
|
+
Logger.json(info);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
Logger.success(`Connected to spreadsheet: ${info.title}`);
|
|
2043
|
+
Logger.bold(`
|
|
2044
|
+
\u{1F4CB} Sheets (${info.sheets.length}):
|
|
2045
|
+
`);
|
|
2046
|
+
info.sheets.forEach((sheet) => {
|
|
2047
|
+
Logger.plain(` ${sheet.index + 1}. ${sheet.title}`);
|
|
2048
|
+
Logger.dim(` ID: ${sheet.sheetId}`);
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
// src/commands/sheet/read.ts
|
|
2054
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
2055
|
+
var OutputFormat = /* @__PURE__ */ ((OutputFormat2) => {
|
|
2056
|
+
OutputFormat2["Csv"] = "csv";
|
|
2057
|
+
OutputFormat2["Json"] = "json";
|
|
2058
|
+
OutputFormat2["Markdown"] = "markdown";
|
|
2059
|
+
return OutputFormat2;
|
|
2060
|
+
})(OutputFormat || {});
|
|
2061
|
+
var outputFormats = Object.values(OutputFormat);
|
|
2062
|
+
var outputFormatters = {
|
|
2063
|
+
["csv" /* Csv */]: formatAsCSV,
|
|
2064
|
+
["json" /* Json */]: formatAsJSON,
|
|
2065
|
+
["markdown" /* Markdown */]: formatAsMarkdown
|
|
2066
|
+
};
|
|
2067
|
+
var readCommand = defineSubCommand({
|
|
2068
|
+
name: "read",
|
|
2069
|
+
description: "Read the complete content of a sheet",
|
|
2070
|
+
flags: [
|
|
2071
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
2072
|
+
flag.string("--output", "Output format", { alias: "-o" }),
|
|
2073
|
+
flag.boolean("--formulas", "Include formulas instead of values", { alias: "-f" }),
|
|
2074
|
+
flag.string("--export", "Export to file", { alias: "-e" }),
|
|
2075
|
+
flag.string("--range", "Range to read (e.g., A1:B10)", { alias: "-r" })
|
|
2076
|
+
],
|
|
2077
|
+
errorMessage: "Failed to read sheet",
|
|
2078
|
+
action: async ({ options }) => {
|
|
2079
|
+
const outputFormat = options.output ?? "markdown" /* Markdown */;
|
|
2080
|
+
if (!isOutputFormat(outputFormat)) {
|
|
2081
|
+
Logger.error(`Invalid output format '${outputFormat}'. Valid formats: ${outputFormats.join(", ")}`);
|
|
2082
|
+
process.exit(1);
|
|
2083
|
+
}
|
|
2084
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2085
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2086
|
+
if (outputFormat !== "json" /* Json */ || options.export) {
|
|
2087
|
+
Logger.loading(`Reading sheet '${sheetName}'...`);
|
|
2088
|
+
}
|
|
2089
|
+
const includeFormulas = options.formulas ?? false;
|
|
2090
|
+
const data = options.range ? await sheetsService.getSheetDataRange(sheetName, options.range, includeFormulas) : await sheetsService.getSheetData(sheetName, includeFormulas);
|
|
2091
|
+
if (data.length === 0) {
|
|
2092
|
+
Logger.warning("Sheet is empty");
|
|
2093
|
+
process.exit(0);
|
|
2094
|
+
}
|
|
2095
|
+
const output = outputFormatters[outputFormat](data);
|
|
2096
|
+
if (options.export) {
|
|
2097
|
+
writeFileSync4(options.export, output, "utf-8");
|
|
2098
|
+
Logger.success(`Content exported to ${options.export}`);
|
|
2099
|
+
} else if (outputFormat === "json" /* Json */) {
|
|
2100
|
+
Logger.plain(output);
|
|
2101
|
+
} else {
|
|
2102
|
+
Logger.success(`Content of sheet '${sheetName}':
|
|
2103
|
+
`);
|
|
2104
|
+
Logger.plain(output);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
function isOutputFormat(value) {
|
|
2109
|
+
return outputFormats.includes(value);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/utils/validators.ts
|
|
2113
|
+
function validatePositiveInteger(value, fieldName) {
|
|
2114
|
+
const num = parseInt(value, 10);
|
|
2115
|
+
if (Number.isNaN(num) || num < 1) {
|
|
2116
|
+
throw new Error(`${fieldName} must be a positive integer`);
|
|
2117
|
+
}
|
|
2118
|
+
return num;
|
|
2119
|
+
}
|
|
2120
|
+
function validateRequired(value, fieldName) {
|
|
2121
|
+
if (!value || !value.trim()) {
|
|
2122
|
+
throw new Error(`${fieldName} is required`);
|
|
2123
|
+
}
|
|
2124
|
+
return value.trim();
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/commands/sheet/row_operations/add.ts
|
|
2128
|
+
var rowAddCommand = defineSubCommand({
|
|
2129
|
+
name: "row-add",
|
|
2130
|
+
description: "Add a row to the sheet",
|
|
2131
|
+
flags: [
|
|
2132
|
+
flag.string("--row", "Row number (1-indexed)", { alias: "-r", required: true }),
|
|
2133
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
2134
|
+
flag.boolean("--above", "Insert row above the specified row"),
|
|
2135
|
+
flag.boolean("--below", "Insert row below the specified row"),
|
|
2136
|
+
flag.boolean("--formulas", "Copy formatting, formulas, and data validation from adjacent row", { alias: "-f" }),
|
|
2137
|
+
flag.string("--count", "Number of rows to add (default: 1)", { alias: "-c" })
|
|
2138
|
+
],
|
|
2139
|
+
errorMessage: "Failed to add row",
|
|
2140
|
+
action: async ({ options }) => {
|
|
2141
|
+
const rowValue = validateRequired(options.row, "Row number");
|
|
2142
|
+
const rowNumber = validatePositiveInteger(rowValue, "Row number");
|
|
2143
|
+
if (options.above && options.below) {
|
|
2144
|
+
Logger.error("Cannot use both --above and --below. Choose one");
|
|
2145
|
+
process.exit(1);
|
|
2146
|
+
}
|
|
2147
|
+
const count = options.count ? validatePositiveInteger(options.count, "Count") : 1;
|
|
2148
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2149
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2150
|
+
let insertPosition;
|
|
2151
|
+
let inheritFromBefore;
|
|
2152
|
+
let sourceRowForFormulas;
|
|
2153
|
+
if (options.below) {
|
|
2154
|
+
insertPosition = rowNumber;
|
|
2155
|
+
sourceRowForFormulas = rowNumber - 1;
|
|
2156
|
+
inheritFromBefore = false;
|
|
2157
|
+
} else {
|
|
2158
|
+
insertPosition = rowNumber - 1;
|
|
2159
|
+
sourceRowForFormulas = rowNumber - 1;
|
|
2160
|
+
inheritFromBefore = !!options.formulas;
|
|
2161
|
+
}
|
|
2162
|
+
const rowWord = count === 1 ? "row" : "rows";
|
|
2163
|
+
Logger.loading(`Adding ${count} ${rowWord} ${options.below ? "below" : "above"} row ${rowNumber}...`);
|
|
2164
|
+
await sheetsService.insertRows(
|
|
2165
|
+
sheetName,
|
|
2166
|
+
{
|
|
2167
|
+
startIndex: insertPosition,
|
|
2168
|
+
endIndex: insertPosition + count
|
|
2169
|
+
},
|
|
2170
|
+
inheritFromBefore
|
|
2171
|
+
);
|
|
2172
|
+
if (options.formulas) {
|
|
2173
|
+
Logger.loading("Copying formulas from adjacent row...");
|
|
2174
|
+
await sheetsService.copyRowFormulasBulk(sheetName, sourceRowForFormulas, insertPosition, count);
|
|
2175
|
+
}
|
|
2176
|
+
Logger.success(`${count} ${rowWord} added successfully ${options.below ? "below" : "above"} row ${rowNumber}`);
|
|
2177
|
+
if (options.formulas) {
|
|
2178
|
+
Logger.info(`Formatting, formulas, and dropdowns copied from row ${sourceRowForFormulas + 1}`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
// src/commands/sheet/row_operations/remove.ts
|
|
2184
|
+
var rowRemoveCommand = defineSubCommand({
|
|
2185
|
+
name: "row-remove",
|
|
2186
|
+
description: "Remove a row from the sheet",
|
|
2187
|
+
flags: [
|
|
2188
|
+
flag.string("--row", "Row number (1-indexed)", { alias: "-r", required: true }),
|
|
2189
|
+
flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" }),
|
|
2190
|
+
flag.boolean("--above", "Remove rows above the specified row"),
|
|
2191
|
+
flag.boolean("--below", "Remove rows below the specified row"),
|
|
2192
|
+
flag.string("--count", "Number of rows to remove (default: 1)", { alias: "-c" })
|
|
2193
|
+
],
|
|
2194
|
+
errorMessage: "Failed to remove row",
|
|
2195
|
+
action: async ({ options }) => {
|
|
2196
|
+
const rowValue = validateRequired(options.row, "Row number");
|
|
2197
|
+
const rowNumber = validatePositiveInteger(rowValue, "Row number");
|
|
2198
|
+
if (options.above && options.below) {
|
|
2199
|
+
Logger.error("Cannot use both --above and --below. Choose one");
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
const count = options.count ? validatePositiveInteger(options.count, "Count") : 1;
|
|
2203
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2204
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2205
|
+
let startIndex;
|
|
2206
|
+
let endIndex;
|
|
2207
|
+
if (options.above) {
|
|
2208
|
+
startIndex = rowNumber - count - 1;
|
|
2209
|
+
endIndex = rowNumber - 1;
|
|
2210
|
+
if (startIndex < 0) {
|
|
2211
|
+
Logger.error(`Cannot remove ${count} rows above row ${rowNumber} (would go above row 1)`);
|
|
2212
|
+
process.exit(1);
|
|
2213
|
+
}
|
|
2214
|
+
} else if (options.below) {
|
|
2215
|
+
startIndex = rowNumber;
|
|
2216
|
+
endIndex = rowNumber + count;
|
|
2217
|
+
} else {
|
|
2218
|
+
startIndex = rowNumber - 1;
|
|
2219
|
+
endIndex = rowNumber + count - 1;
|
|
2220
|
+
}
|
|
2221
|
+
const rowWord = count === 1 ? "row" : "rows";
|
|
2222
|
+
const direction = options.above ? "above" : options.below ? "below" : "starting from";
|
|
2223
|
+
Logger.loading(`Removing ${count} ${rowWord} ${direction} row ${rowNumber}...`);
|
|
2224
|
+
await sheetsService.deleteRows(sheetName, {
|
|
2225
|
+
startIndex,
|
|
2226
|
+
endIndex
|
|
2227
|
+
});
|
|
2228
|
+
Logger.success(`${count} ${rowWord} removed successfully`);
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
// src/commands/sheet/sheet_operations/active.ts
|
|
2233
|
+
var activeCommand = defineSubCommand({
|
|
2234
|
+
name: "active",
|
|
2235
|
+
description: "Show the currently active sheet",
|
|
2236
|
+
flags: [flag.string("--output", "Output format", { alias: "-o" })],
|
|
2237
|
+
errorMessage: "Failed to get active sheet",
|
|
2238
|
+
action: async ({ options }) => {
|
|
2239
|
+
const configManager = new ConfigManager();
|
|
2240
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2241
|
+
if (!activeAccount) {
|
|
2242
|
+
Logger.error("No active account set.");
|
|
2243
|
+
Logger.info("Use: gsheet account add");
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
2247
|
+
if (!activeSpreadsheetName) {
|
|
2248
|
+
Logger.warning("No active spreadsheet set.");
|
|
2249
|
+
Logger.info("Use: gsheet spreadsheet select");
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const activeSheetName = configManager.getActiveSheetName(activeAccount.email, activeSpreadsheetName);
|
|
2253
|
+
if (!activeSheetName) {
|
|
2254
|
+
Logger.warning("No active sheet set.");
|
|
2255
|
+
Logger.info("Use: gsheet sheet select");
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2259
|
+
const info = await sheetsService.getSheetInfo();
|
|
2260
|
+
const activeSheet = info.sheets.find((sheet) => sheet.title === activeSheetName);
|
|
2261
|
+
if (options.output === "json") {
|
|
2262
|
+
Logger.json({
|
|
2263
|
+
spreadsheetTitle: info.title,
|
|
2264
|
+
spreadsheetName: activeSpreadsheetName,
|
|
2265
|
+
sheet: activeSheet ? {
|
|
2266
|
+
title: activeSheet.title,
|
|
2267
|
+
index: activeSheet.index,
|
|
2268
|
+
sheetId: activeSheet.sheetId
|
|
2269
|
+
} : {
|
|
2270
|
+
title: activeSheetName,
|
|
2271
|
+
index: null,
|
|
2272
|
+
sheetId: null
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
Logger.success(`Active sheet: ${activeSheetName}`);
|
|
2278
|
+
Logger.dim(` Spreadsheet: ${activeSpreadsheetName}`);
|
|
2279
|
+
if (activeSheet) {
|
|
2280
|
+
Logger.dim(` Index: ${activeSheet.index + 1}`);
|
|
2281
|
+
Logger.dim(` ID: ${activeSheet.sheetId}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
// src/commands/sheet/sheet_operations/add.ts
|
|
2287
|
+
var addCommand = defineSubCommand({
|
|
2288
|
+
name: "add",
|
|
2289
|
+
description: "Add a new sheet to the spreadsheet",
|
|
2290
|
+
flags: [flag.string("--name", "Tab name", { alias: "-n", required: true })],
|
|
2291
|
+
errorMessage: "Failed to add sheet",
|
|
2292
|
+
action: async ({ options }) => {
|
|
2293
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2294
|
+
Logger.loading(`Creating sheet '${options.name}'...`);
|
|
2295
|
+
await sheetsService.addSheet(options.name);
|
|
2296
|
+
Logger.success(`Sheet '${options.name}' created successfully`);
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
// src/commands/sheet/sheet_operations/copy.ts
|
|
2301
|
+
var copyCommand = defineSubCommand({
|
|
2302
|
+
name: "copy",
|
|
2303
|
+
description: "Copy a sheet to a new sheet",
|
|
2304
|
+
flags: [
|
|
2305
|
+
flag.string("--name", "Source tab name (uses active if not provided)", { alias: "-n" }),
|
|
2306
|
+
flag.string("--to", "Destination tab name", { required: true })
|
|
2307
|
+
],
|
|
2308
|
+
errorMessage: "Failed to copy sheet",
|
|
2309
|
+
action: async ({ options }) => {
|
|
2310
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2311
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2312
|
+
Logger.loading(`Copying sheet '${sheetName}' to '${options.to}'...`);
|
|
2313
|
+
await sheetsService.copySheet(sheetName, options.to);
|
|
2314
|
+
Logger.success(`Sheet '${sheetName}' copied to '${options.to}' successfully`);
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
// src/commands/sheet/sheet_operations/remove.ts
|
|
2319
|
+
var removeCommand = defineSubCommand({
|
|
2320
|
+
name: "remove",
|
|
2321
|
+
description: "Remove a sheet from the spreadsheet",
|
|
2322
|
+
flags: [flag.string("--name", "Tab name (uses active if not provided)", { alias: "-n" })],
|
|
2323
|
+
errorMessage: "Failed to remove sheet",
|
|
2324
|
+
action: async ({ options }) => {
|
|
2325
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2326
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2327
|
+
Logger.loading(`Removing sheet '${sheetName}'...`);
|
|
2328
|
+
await sheetsService.removeSheet(sheetName);
|
|
2329
|
+
Logger.success(`Sheet '${sheetName}' removed successfully`);
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
// src/commands/sheet/sheet_operations/rename.ts
|
|
2334
|
+
var renameCommand = defineSubCommand({
|
|
2335
|
+
name: "rename",
|
|
2336
|
+
description: "Rename a sheet in the spreadsheet",
|
|
2337
|
+
flags: [
|
|
2338
|
+
flag.string("--name", "Current tab name (uses active if not provided)", { alias: "-n" }),
|
|
2339
|
+
flag.string("--new-name", "New tab name", { required: true })
|
|
2340
|
+
],
|
|
2341
|
+
errorMessage: "Failed to rename sheet",
|
|
2342
|
+
action: async ({ options }) => {
|
|
2343
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2344
|
+
const sheetName = getActiveSheetName(options.name);
|
|
2345
|
+
Logger.loading(`Renaming sheet '${sheetName}' to '${options.newName}'...`);
|
|
2346
|
+
await sheetsService.renameSheet(sheetName, options.newName);
|
|
2347
|
+
Logger.success(`Sheet '${sheetName}' renamed to '${options.newName}' successfully`);
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
// src/commands/sheet/sheet_operations/select.ts
|
|
2352
|
+
import inquirer3 from "inquirer";
|
|
2353
|
+
var selectCommand = defineSubCommand({
|
|
2354
|
+
name: "select",
|
|
2355
|
+
description: "Select a sheet (sets as active)",
|
|
2356
|
+
flags: [flag.string("--name", "Tab name (skips interactive selection)", { alias: "-n" })],
|
|
2357
|
+
errorMessage: "Failed to select sheet",
|
|
2358
|
+
action: async ({ options }) => {
|
|
2359
|
+
const configManager = new ConfigManager();
|
|
2360
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2361
|
+
if (!activeAccount) {
|
|
2362
|
+
Logger.error("No active account set.");
|
|
2363
|
+
Logger.info("Use: gsheet account add");
|
|
2364
|
+
process.exit(1);
|
|
2365
|
+
}
|
|
2366
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
2367
|
+
if (!activeSpreadsheetName) {
|
|
2368
|
+
Logger.error("No active spreadsheet set.");
|
|
2369
|
+
Logger.info("Use: gsheet spreadsheet select");
|
|
2370
|
+
process.exit(1);
|
|
2371
|
+
}
|
|
2372
|
+
let sheetName = options.name;
|
|
2373
|
+
if (!sheetName) {
|
|
2374
|
+
const sheetsService = await getGoogleSheetsService();
|
|
2375
|
+
Logger.loading("Fetching sheets...");
|
|
2376
|
+
const info = await sheetsService.getSheetInfo();
|
|
2377
|
+
if (info.sheets.length === 0) {
|
|
2378
|
+
Logger.warning("No sheets found in spreadsheet.");
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const activeSheet = configManager.getActiveSheetName(activeAccount.email, activeSpreadsheetName);
|
|
2382
|
+
const answer = await inquirer3.prompt([
|
|
2383
|
+
{
|
|
2384
|
+
type: "list",
|
|
2385
|
+
name: "sheet",
|
|
2386
|
+
message: "Select sheet:",
|
|
2387
|
+
choices: info.sheets.map((s) => ({
|
|
2388
|
+
name: s.title === activeSheet ? `${s.title} (current)` : s.title,
|
|
2389
|
+
value: s.title
|
|
2390
|
+
}))
|
|
2391
|
+
}
|
|
2392
|
+
]);
|
|
2393
|
+
sheetName = answer.sheet;
|
|
2394
|
+
}
|
|
2395
|
+
if (!sheetName) {
|
|
2396
|
+
Logger.error("No sheet name provided");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
configManager.setActiveSheet(activeAccount.email, activeSpreadsheetName, sheetName);
|
|
2400
|
+
Logger.success(`Selected sheet: ${sheetName}`);
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
// src/utils/spreadsheet.ts
|
|
2405
|
+
function parseSpreadsheetId(value) {
|
|
2406
|
+
const trimmed = value.trim();
|
|
2407
|
+
if (!trimmed) {
|
|
2408
|
+
throw new Error("Spreadsheet ID or URL is required");
|
|
2409
|
+
}
|
|
2410
|
+
try {
|
|
2411
|
+
const url = new URL(trimmed);
|
|
2412
|
+
const match = url.pathname.match(/\/spreadsheets\/d\/([^/]+)/);
|
|
2413
|
+
if (match?.[1]) {
|
|
2414
|
+
return match[1];
|
|
2415
|
+
}
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
return trimmed;
|
|
2419
|
+
}
|
|
2420
|
+
function getSpreadsheetUrl(spreadsheetId) {
|
|
2421
|
+
return `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// src/commands/spreadsheet/active.ts
|
|
2425
|
+
var activeSpreadsheetCommand = defineSubCommand({
|
|
2426
|
+
name: "active",
|
|
2427
|
+
description: "Show the currently active spreadsheet",
|
|
2428
|
+
flags: [flag.string("--output", "Output format", { alias: "-o" })],
|
|
2429
|
+
errorMessage: "Failed to get active spreadsheet",
|
|
2430
|
+
action: async ({ options }) => {
|
|
2431
|
+
const configManager = new ConfigManager();
|
|
2432
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2433
|
+
if (!activeAccount) {
|
|
2434
|
+
Logger.error("No active account set.");
|
|
2435
|
+
Logger.info("Use: gsheet account add");
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
2439
|
+
if (!activeSpreadsheetName) {
|
|
2440
|
+
Logger.warning("No active spreadsheet set.");
|
|
2441
|
+
Logger.info('Use "gsheet spreadsheet select" to set one.');
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
const activeSpreadsheet = configManager.getSpreadsheet(activeAccount.email, activeSpreadsheetName);
|
|
2445
|
+
if (!activeSpreadsheet) {
|
|
2446
|
+
Logger.error("Active spreadsheet not found.");
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (options.output === "json") {
|
|
2450
|
+
Logger.json({
|
|
2451
|
+
activeAccount: activeAccount.email,
|
|
2452
|
+
name: activeSpreadsheetName,
|
|
2453
|
+
spreadsheetId: activeSpreadsheet.spreadsheet_id,
|
|
2454
|
+
url: getSpreadsheetUrl(activeSpreadsheet.spreadsheet_id),
|
|
2455
|
+
activeSheet: activeSpreadsheet.activeSheet ?? null
|
|
2456
|
+
});
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
Logger.success(`Active spreadsheet: ${activeSpreadsheetName}`);
|
|
2460
|
+
Logger.dim(` ID: ${activeSpreadsheet.spreadsheet_id}`);
|
|
2461
|
+
Logger.dim(` URL: ${getSpreadsheetUrl(activeSpreadsheet.spreadsheet_id)}`);
|
|
2462
|
+
if (activeSpreadsheet.activeSheet) {
|
|
2463
|
+
Logger.dim(` Active sheet: ${activeSpreadsheet.activeSheet}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
// src/commands/spreadsheet/add.ts
|
|
2469
|
+
import inquirer4 from "inquirer";
|
|
2470
|
+
|
|
2471
|
+
// src/core/google-drive.service.ts
|
|
2472
|
+
import { google } from "googleapis";
|
|
2473
|
+
var GoogleDriveService = class {
|
|
2474
|
+
credentials;
|
|
2475
|
+
constructor(oauthCredentials) {
|
|
2476
|
+
this.credentials = oauthCredentials;
|
|
2477
|
+
}
|
|
2478
|
+
async listSpreadsheets() {
|
|
2479
|
+
const oauth2Client = new google.auth.OAuth2(this.credentials.client_id, this.credentials.client_secret);
|
|
2480
|
+
oauth2Client.setCredentials({
|
|
2481
|
+
access_token: this.credentials.access_token,
|
|
2482
|
+
refresh_token: this.credentials.refresh_token,
|
|
2483
|
+
expiry_date: this.credentials.expiry_date
|
|
2484
|
+
});
|
|
2485
|
+
Logger.info("Checking access token...");
|
|
2486
|
+
const tokenInfo = await oauth2Client.getTokenInfo(this.credentials.access_token || "");
|
|
2487
|
+
Logger.info(`Token scopes: ${tokenInfo.scopes?.join(", ") || "none"}`);
|
|
2488
|
+
Logger.info(
|
|
2489
|
+
`Token expires at: ${this.credentials.expiry_date ? new Date(this.credentials.expiry_date).toLocaleString() : "unknown"}`
|
|
2490
|
+
);
|
|
2491
|
+
const drive = google.drive({ version: "v3", auth: oauth2Client });
|
|
2492
|
+
Logger.info("Requesting spreadsheets from Google Drive API...");
|
|
2493
|
+
try {
|
|
2494
|
+
const response = await drive.files.list({
|
|
2495
|
+
q: "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false",
|
|
2496
|
+
fields: "files(id, name, modifiedTime, webViewLink)",
|
|
2497
|
+
orderBy: "modifiedTime desc",
|
|
2498
|
+
pageSize: 100
|
|
2499
|
+
});
|
|
2500
|
+
const files = response.data.files || [];
|
|
2501
|
+
Logger.info(`Found ${files.length} spreadsheet(s)`);
|
|
2502
|
+
return files.map((file) => ({
|
|
2503
|
+
id: file.id || "",
|
|
2504
|
+
name: file.name || "Untitled",
|
|
2505
|
+
modifiedTime: file.modifiedTime || "",
|
|
2506
|
+
webViewLink: file.webViewLink || ""
|
|
2507
|
+
}));
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
Logger.error("Google Drive API error:", error);
|
|
2510
|
+
Logger.info("\nRequired scopes for this operation:");
|
|
2511
|
+
Logger.info(` - ${OAUTH_SCOPES.SPREADSHEETS}`);
|
|
2512
|
+
Logger.info(` - ${OAUTH_SCOPES.DRIVE_READONLY}`);
|
|
2513
|
+
Logger.info("\nTo fix this:");
|
|
2514
|
+
Logger.info(" 1. Add Drive API scope in OAuth Consent Screen");
|
|
2515
|
+
Logger.info(" 2. Run: gsheet account reauth");
|
|
2516
|
+
throw error;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// src/core/spreadsheet-title.ts
|
|
2522
|
+
async function getSpreadsheetTitle(configManager, email, spreadsheetId) {
|
|
2523
|
+
const credentials = await configManager.getRefreshedCredentials(email);
|
|
2524
|
+
const sheetsService = new GoogleSheetsService({
|
|
2525
|
+
spreadsheetId,
|
|
2526
|
+
oauthCredentials: credentials
|
|
2527
|
+
});
|
|
2528
|
+
const info = await sheetsService.getSheetInfo();
|
|
2529
|
+
return info.title;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/commands/spreadsheet/add.ts
|
|
2533
|
+
var addSpreadsheetCommand = defineSubCommand({
|
|
2534
|
+
name: "add",
|
|
2535
|
+
description: "Add a new spreadsheet (interactive by default, use --id for manual)",
|
|
2536
|
+
flags: [
|
|
2537
|
+
flag.string("--id", "Spreadsheet ID or URL (skips interactive selection)", { alias: "-i" }),
|
|
2538
|
+
flag.string("--name", "Local name for the spreadsheet")
|
|
2539
|
+
],
|
|
2540
|
+
errorMessage: "Failed to add spreadsheet",
|
|
2541
|
+
action: async ({ options }) => {
|
|
2542
|
+
const configManager = new ConfigManager();
|
|
2543
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2544
|
+
if (!activeAccount) {
|
|
2545
|
+
Logger.error("No active account set.");
|
|
2546
|
+
Logger.info("Use: gsheet account add");
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
let spreadsheetId;
|
|
2550
|
+
let name;
|
|
2551
|
+
const spreadsheetIdOption = options.id ?? options.i;
|
|
2552
|
+
if (spreadsheetIdOption !== void 0) {
|
|
2553
|
+
spreadsheetId = parseSpreadsheetId(String(spreadsheetIdOption));
|
|
2554
|
+
name = options.name?.trim() || "";
|
|
2555
|
+
if (!name) {
|
|
2556
|
+
const defaultName = await getSpreadsheetTitle(configManager, activeAccount.email, spreadsheetId);
|
|
2557
|
+
const answer = await inquirer4.prompt([
|
|
2558
|
+
{
|
|
2559
|
+
type: "input",
|
|
2560
|
+
name: "name",
|
|
2561
|
+
message: "Enter a local name for this spreadsheet:",
|
|
2562
|
+
default: defaultName,
|
|
2563
|
+
validate: (input) => {
|
|
2564
|
+
if (!input.trim()) {
|
|
2565
|
+
return "Name cannot be empty";
|
|
2566
|
+
}
|
|
2567
|
+
return true;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
]);
|
|
2571
|
+
name = answer.name;
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
Logger.loading("Fetching your spreadsheets from Google Drive...");
|
|
2575
|
+
const credentials = await configManager.getRefreshedCredentials(activeAccount.email);
|
|
2576
|
+
const driveService = new GoogleDriveService(credentials);
|
|
2577
|
+
const spreadsheets2 = await driveService.listSpreadsheets();
|
|
2578
|
+
if (spreadsheets2.length === 0) {
|
|
2579
|
+
Logger.warning("No spreadsheets found in your Google Drive.");
|
|
2580
|
+
Logger.info(`Create one at: ${GOOGLE_API_URLS.SHEETS_CREATE}`);
|
|
2581
|
+
process.exit(0);
|
|
2582
|
+
}
|
|
2583
|
+
const choices = spreadsheets2.map((s) => ({
|
|
2584
|
+
name: `${s.name} (Modified: ${new Date(s.modifiedTime).toLocaleDateString()})`,
|
|
2585
|
+
value: { id: s.id, name: s.name }
|
|
2586
|
+
}));
|
|
2587
|
+
const selection = await inquirer4.prompt([
|
|
2588
|
+
{
|
|
2589
|
+
type: "list",
|
|
2590
|
+
name: "spreadsheet",
|
|
2591
|
+
message: `Select a spreadsheet (${spreadsheets2.length} found):`,
|
|
2592
|
+
choices,
|
|
2593
|
+
pageSize: 15
|
|
2594
|
+
},
|
|
2595
|
+
{
|
|
2596
|
+
type: "input",
|
|
2597
|
+
name: "localName",
|
|
2598
|
+
message: "Enter a local name for this spreadsheet:",
|
|
2599
|
+
default: (answers) => answers.spreadsheet.name,
|
|
2600
|
+
validate: (input) => {
|
|
2601
|
+
if (!input.trim()) {
|
|
2602
|
+
return "Name cannot be empty";
|
|
2603
|
+
}
|
|
2604
|
+
return true;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
]);
|
|
2608
|
+
spreadsheetId = selection.spreadsheet.id;
|
|
2609
|
+
name = selection.localName;
|
|
2610
|
+
}
|
|
2611
|
+
await configManager.addSpreadsheet(activeAccount.email, name, spreadsheetId);
|
|
2612
|
+
const spreadsheets = configManager.listSpreadsheets(activeAccount.email);
|
|
2613
|
+
if (spreadsheets.length === 1) {
|
|
2614
|
+
configManager.setActiveSpreadsheet(activeAccount.email, name);
|
|
2615
|
+
Logger.success(`Spreadsheet '${name}' added and set as active!`);
|
|
2616
|
+
} else {
|
|
2617
|
+
Logger.success(`Spreadsheet '${name}' added successfully!`);
|
|
2618
|
+
Logger.info(`Switch to this spreadsheet: gsheet spreadsheet select ${name}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
// src/commands/spreadsheet/list.ts
|
|
2624
|
+
var listSpreadsheetsCommand = defineSubCommand({
|
|
2625
|
+
name: "list",
|
|
2626
|
+
description: "List all configured spreadsheets",
|
|
2627
|
+
flags: [flag.string("--output", "Output format", { alias: "-o" })],
|
|
2628
|
+
errorMessage: "Failed to list spreadsheets",
|
|
2629
|
+
action: async ({ options }) => {
|
|
2630
|
+
const configManager = new ConfigManager();
|
|
2631
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2632
|
+
if (!activeAccount) {
|
|
2633
|
+
Logger.error("No active account set.");
|
|
2634
|
+
Logger.info("Use: gsheet account add");
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const spreadsheets = configManager.listSpreadsheets(activeAccount.email);
|
|
2638
|
+
const activeSpreadsheetName = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
2639
|
+
if (options.output === "json") {
|
|
2640
|
+
Logger.json({
|
|
2641
|
+
activeAccount: activeAccount.email,
|
|
2642
|
+
activeSpreadsheet: activeSpreadsheetName,
|
|
2643
|
+
spreadsheets: spreadsheets.map((spreadsheet) => ({
|
|
2644
|
+
name: spreadsheet.name,
|
|
2645
|
+
spreadsheetId: spreadsheet.spreadsheetId,
|
|
2646
|
+
url: getSpreadsheetUrl(spreadsheet.spreadsheetId),
|
|
2647
|
+
activeSheet: spreadsheet.activeSheet ?? null,
|
|
2648
|
+
active: spreadsheet.name === activeSpreadsheetName
|
|
2649
|
+
}))
|
|
2650
|
+
});
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (spreadsheets.length === 0) {
|
|
2654
|
+
Logger.warning('No spreadsheets configured. Use "gsheet spreadsheet add" to add one.');
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
Logger.bold(`
|
|
2658
|
+
Spreadsheets for ${activeAccount.email}:`);
|
|
2659
|
+
spreadsheets.forEach((spreadsheet) => {
|
|
2660
|
+
const isActive = spreadsheet.name === activeSpreadsheetName;
|
|
2661
|
+
const marker = isActive ? "* " : " ";
|
|
2662
|
+
Logger.plain(`${marker}${spreadsheet.name}${isActive ? " (active)" : ""}`);
|
|
2663
|
+
Logger.dim(` ID: ${spreadsheet.spreadsheetId}`);
|
|
2664
|
+
Logger.dim(` URL: ${getSpreadsheetUrl(spreadsheet.spreadsheetId)}`);
|
|
2665
|
+
if (spreadsheet.activeSheet) {
|
|
2666
|
+
Logger.dim(` Active sheet: ${spreadsheet.activeSheet}`);
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
if (activeSpreadsheetName) {
|
|
2670
|
+
Logger.plain("");
|
|
2671
|
+
Logger.dim("* = active spreadsheet");
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
// src/commands/spreadsheet/remove.ts
|
|
2677
|
+
import inquirer5 from "inquirer";
|
|
2678
|
+
var removeSpreadsheetCommand = defineSubCommand({
|
|
2679
|
+
name: "remove",
|
|
2680
|
+
description: "Remove a spreadsheet configuration",
|
|
2681
|
+
flags: [flag.string("--id", "Spreadsheet ID or URL (skips interactive selection)", { alias: "-i" })],
|
|
2682
|
+
errorMessage: "Failed to remove spreadsheet",
|
|
2683
|
+
action: async ({ options }) => {
|
|
2684
|
+
const configManager = new ConfigManager();
|
|
2685
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2686
|
+
if (!activeAccount) {
|
|
2687
|
+
Logger.error("No active account set.");
|
|
2688
|
+
Logger.info("Use: gsheet account add");
|
|
2689
|
+
process.exit(1);
|
|
2690
|
+
}
|
|
2691
|
+
const spreadsheetIdOption = options.id ?? options.i;
|
|
2692
|
+
let spreadsheetId = spreadsheetIdOption !== void 0 ? parseSpreadsheetId(String(spreadsheetIdOption)) : void 0;
|
|
2693
|
+
if (!spreadsheetId) {
|
|
2694
|
+
const spreadsheets = configManager.listSpreadsheets(activeAccount.email);
|
|
2695
|
+
if (spreadsheets.length === 0) {
|
|
2696
|
+
Logger.warning("No spreadsheets configured.");
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
const answer = await inquirer5.prompt([
|
|
2700
|
+
{
|
|
2701
|
+
type: "list",
|
|
2702
|
+
name: "spreadsheet",
|
|
2703
|
+
message: "Select spreadsheet to remove:",
|
|
2704
|
+
choices: spreadsheets.map((s) => ({
|
|
2705
|
+
name: s.name,
|
|
2706
|
+
value: s.spreadsheetId
|
|
2707
|
+
}))
|
|
2708
|
+
}
|
|
2709
|
+
]);
|
|
2710
|
+
spreadsheetId = answer.spreadsheet;
|
|
2711
|
+
}
|
|
2712
|
+
if (!spreadsheetId) {
|
|
2713
|
+
Logger.error("No spreadsheet ID provided");
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const spreadsheet = configManager.getSpreadsheetById(activeAccount.email, spreadsheetId);
|
|
2717
|
+
if (!spreadsheet) {
|
|
2718
|
+
Logger.error(`Spreadsheet with ID '${spreadsheetId}' not found`);
|
|
2719
|
+
process.exit(1);
|
|
2720
|
+
}
|
|
2721
|
+
const spreadsheetName = Object.entries(configManager.listSpreadsheets(activeAccount.email)).find(
|
|
2722
|
+
([_, s]) => s.spreadsheetId === spreadsheetId
|
|
2723
|
+
)?.[1]?.name;
|
|
2724
|
+
if (!spreadsheetName) {
|
|
2725
|
+
Logger.error(`Spreadsheet with ID '${spreadsheetId}' not found`);
|
|
2726
|
+
process.exit(1);
|
|
2727
|
+
}
|
|
2728
|
+
const confirm = await inquirer5.prompt([
|
|
2729
|
+
{
|
|
2730
|
+
type: "confirm",
|
|
2731
|
+
name: "confirmed",
|
|
2732
|
+
message: `Are you sure you want to remove '${spreadsheetName}'?`,
|
|
2733
|
+
default: false
|
|
2734
|
+
}
|
|
2735
|
+
]);
|
|
2736
|
+
if (!confirm.confirmed) {
|
|
2737
|
+
Logger.info("Cancelled");
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
await configManager.removeSpreadsheet(activeAccount.email, spreadsheetName);
|
|
2741
|
+
Logger.success(`Spreadsheet '${spreadsheetName}' removed successfully!`);
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
|
|
2745
|
+
// src/commands/spreadsheet/select.ts
|
|
2746
|
+
import inquirer6 from "inquirer";
|
|
2747
|
+
var selectSpreadsheetCommand = defineSubCommand({
|
|
2748
|
+
name: "select",
|
|
2749
|
+
description: "Select a different spreadsheet (sets as active)",
|
|
2750
|
+
flags: [
|
|
2751
|
+
flag.string("--id", "Spreadsheet ID or URL (skips interactive selection)", { alias: "-i" }),
|
|
2752
|
+
flag.boolean("--add", "Add the spreadsheet if it is not configured"),
|
|
2753
|
+
flag.string("--name", "Local name to use with --add")
|
|
2754
|
+
],
|
|
2755
|
+
errorMessage: "Failed to select spreadsheet",
|
|
2756
|
+
action: async ({ options }) => {
|
|
2757
|
+
const configManager = new ConfigManager();
|
|
2758
|
+
const activeAccount = configManager.getActiveAccount();
|
|
2759
|
+
if (!activeAccount) {
|
|
2760
|
+
Logger.error("No active account set.");
|
|
2761
|
+
Logger.info("Use: gsheet account add");
|
|
2762
|
+
process.exit(1);
|
|
2763
|
+
}
|
|
2764
|
+
const spreadsheetIdOption = options.id ?? options.i;
|
|
2765
|
+
let spreadsheetId = spreadsheetIdOption !== void 0 ? parseSpreadsheetId(String(spreadsheetIdOption)) : void 0;
|
|
2766
|
+
if (!spreadsheetId) {
|
|
2767
|
+
const spreadsheets = configManager.listSpreadsheets(activeAccount.email);
|
|
2768
|
+
if (spreadsheets.length === 0) {
|
|
2769
|
+
Logger.warning('No spreadsheets configured. Use "gsheet spreadsheet add" to add one.');
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const activeSpreadsheet = configManager.getActiveSpreadsheetName(activeAccount.email);
|
|
2773
|
+
const answer = await inquirer6.prompt([
|
|
2774
|
+
{
|
|
2775
|
+
type: "list",
|
|
2776
|
+
name: "spreadsheet",
|
|
2777
|
+
message: "Select spreadsheet:",
|
|
2778
|
+
choices: spreadsheets.map((s) => ({
|
|
2779
|
+
name: s.name === activeSpreadsheet ? `${s.name} (current)` : s.name,
|
|
2780
|
+
value: s.spreadsheetId
|
|
2781
|
+
}))
|
|
2782
|
+
}
|
|
2783
|
+
]);
|
|
2784
|
+
spreadsheetId = answer.spreadsheet;
|
|
2785
|
+
}
|
|
2786
|
+
if (!spreadsheetId) {
|
|
2787
|
+
Logger.error("No spreadsheet ID provided");
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
let spreadsheet = configManager.getSpreadsheetById(activeAccount.email, spreadsheetId);
|
|
2791
|
+
if (!spreadsheet) {
|
|
2792
|
+
if (!options.add) {
|
|
2793
|
+
Logger.error(`Spreadsheet with ID '${spreadsheetId}' not found`);
|
|
2794
|
+
Logger.info("Use --add to add and select this spreadsheet.");
|
|
2795
|
+
process.exit(1);
|
|
2796
|
+
}
|
|
2797
|
+
const name = options.name?.trim() || await getSpreadsheetTitle(configManager, activeAccount.email, spreadsheetId);
|
|
2798
|
+
await configManager.addSpreadsheet(activeAccount.email, name, spreadsheetId);
|
|
2799
|
+
spreadsheet = configManager.getSpreadsheetById(activeAccount.email, spreadsheetId);
|
|
2800
|
+
Logger.success(`Added spreadsheet: ${name}`);
|
|
2801
|
+
}
|
|
2802
|
+
const spreadsheetName = Object.entries(configManager.listSpreadsheets(activeAccount.email)).find(
|
|
2803
|
+
([_, s]) => s.spreadsheetId === spreadsheetId
|
|
2804
|
+
)?.[1]?.name;
|
|
2805
|
+
if (!spreadsheetName) {
|
|
2806
|
+
Logger.error(`Spreadsheet with ID '${spreadsheetId}' not found`);
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
}
|
|
2809
|
+
configManager.setActiveSpreadsheet(activeAccount.email, spreadsheetName);
|
|
2810
|
+
Logger.success(`Selected spreadsheet: ${spreadsheetName}`);
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
// src/commands/update.ts
|
|
2815
|
+
import { exec } from "child_process";
|
|
2816
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2817
|
+
import { platform as platform2 } from "os";
|
|
2818
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
2819
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2820
|
+
import { promisify } from "util";
|
|
2821
|
+
var execAsync = promisify(exec);
|
|
2822
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
2823
|
+
var updateCommandsByPackageManager = {
|
|
2824
|
+
["npm" /* Npm */]: `npm update -g ${APP_INFO.packageName}`,
|
|
2825
|
+
["pnpm" /* Pnpm */]: `pnpm update -g ${APP_INFO.packageName}`,
|
|
2826
|
+
["yarn" /* Yarn */]: `yarn global upgrade ${APP_INFO.packageName}`
|
|
2827
|
+
};
|
|
2828
|
+
var updateCommand = defineSubCommand({
|
|
2829
|
+
name: "update",
|
|
2830
|
+
description: `Update ${APP_INFO.name} to latest version`,
|
|
2831
|
+
errorMessage: "Failed to check for updates",
|
|
2832
|
+
action: async () => {
|
|
2833
|
+
Logger.loading("Checking current version...");
|
|
2834
|
+
const currentVersion = getCurrentVersion();
|
|
2835
|
+
if (!currentVersion) {
|
|
2836
|
+
Logger.error("Could not determine current version");
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
Logger.loading("Checking latest version...");
|
|
2840
|
+
const latestVersion = await getLatestVersion();
|
|
2841
|
+
if (!latestVersion) {
|
|
2842
|
+
Logger.error("Could not fetch latest version from npm");
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
Logger.info(`\u{1F4E6} Current version: ${currentVersion}`);
|
|
2846
|
+
Logger.info(`\u{1F4E6} Latest version: ${latestVersion}`);
|
|
2847
|
+
if (currentVersion === latestVersion) {
|
|
2848
|
+
Logger.success(`${APP_INFO.name} is already up to date!`);
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
Logger.loading("Detecting package manager...");
|
|
2852
|
+
const packageManager = await detectPackageManager();
|
|
2853
|
+
if (!packageManager) {
|
|
2854
|
+
Logger.error(`Could not detect how ${APP_INFO.name} was installed`);
|
|
2855
|
+
Logger.dim("Please update manually using your package manager");
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
Logger.info(`\u{1F4E6} Detected package manager: ${packageManager}`);
|
|
2859
|
+
Logger.loading(`Updating ${APP_INFO.name} from ${currentVersion} to ${latestVersion}...`);
|
|
2860
|
+
const updateCommand2 = getUpdateCommand(packageManager);
|
|
2861
|
+
const { stdout, stderr } = await execAsync(updateCommand2);
|
|
2862
|
+
if (stderr && !stderr.includes("npm WARN")) {
|
|
2863
|
+
Logger.error(`Error updating: ${stderr}`);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
Logger.success(`${APP_INFO.name} updated successfully from ${currentVersion} to ${latestVersion}!`);
|
|
2867
|
+
if (stdout) {
|
|
2868
|
+
Logger.dim(stdout);
|
|
2869
|
+
}
|
|
2870
|
+
const completionReinstalled = await reinstallCompletionSilently();
|
|
2871
|
+
if (completionReinstalled) {
|
|
2872
|
+
Logger.dim("\u2728 Shell completion updated");
|
|
2873
|
+
Logger.info("");
|
|
2874
|
+
Logger.info("To activate the updated completion, run:");
|
|
2875
|
+
const currentShell = process.env.SHELL || "";
|
|
2876
|
+
if (currentShell.includes("zsh")) {
|
|
2877
|
+
Logger.info(" exec zsh");
|
|
2878
|
+
} else if (currentShell.includes("bash")) {
|
|
2879
|
+
Logger.info(" exec bash");
|
|
2880
|
+
} else {
|
|
2881
|
+
Logger.info(" # Restart your shell");
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
async function detectPackageManager() {
|
|
2887
|
+
const npmPath = await getGlobalNpmPath();
|
|
2888
|
+
if (!npmPath) {
|
|
2889
|
+
return null;
|
|
2890
|
+
}
|
|
2891
|
+
const possiblePaths = [
|
|
2892
|
+
{ manager: "npm" /* Npm */, patterns: ["/npm/", "\\npm\\", "/node/", "\\node\\"] },
|
|
2893
|
+
{ manager: "yarn" /* Yarn */, patterns: ["/yarn/", "\\yarn\\", "/.yarn/", "\\.yarn\\"] },
|
|
2894
|
+
{ manager: "pnpm" /* Pnpm */, patterns: ["/pnpm/", "\\pnpm\\", "/.pnpm/", "\\.pnpm\\"] }
|
|
2895
|
+
];
|
|
2896
|
+
for (const { manager, patterns } of possiblePaths) {
|
|
2897
|
+
if (patterns.some((pattern) => npmPath.includes(pattern))) {
|
|
2898
|
+
return manager;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
return "npm" /* Npm */;
|
|
2902
|
+
}
|
|
2903
|
+
async function getGlobalNpmPath() {
|
|
2904
|
+
const isWindows = platform2() === "win32";
|
|
2905
|
+
try {
|
|
2906
|
+
const whereCommand = isWindows ? "where" : "which";
|
|
2907
|
+
const { stdout } = await execAsync(`${whereCommand} ${APP_INFO.name}`);
|
|
2908
|
+
const execPath = stdout.trim();
|
|
2909
|
+
if (execPath) {
|
|
2910
|
+
if (!isWindows) {
|
|
2911
|
+
try {
|
|
2912
|
+
const { stdout: realPath } = await execAsync(`readlink -f "${execPath}"`);
|
|
2913
|
+
return realPath.trim() || execPath;
|
|
2914
|
+
} catch {
|
|
2915
|
+
return execPath;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return execPath;
|
|
2919
|
+
}
|
|
2920
|
+
} catch {
|
|
2921
|
+
try {
|
|
2922
|
+
const { stdout } = await execAsync(`npm list -g --depth=0 ${APP_INFO.packageName}`);
|
|
2923
|
+
if (stdout.includes(APP_INFO.packageName)) {
|
|
2924
|
+
return "npm" /* Npm */;
|
|
2925
|
+
}
|
|
2926
|
+
} catch {
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2931
|
+
function getCurrentVersion() {
|
|
2932
|
+
try {
|
|
2933
|
+
const packagePath = join3(__dirname2, "../../package.json");
|
|
2934
|
+
const packageJson2 = JSON.parse(readFileSync4(packagePath, "utf8"));
|
|
2935
|
+
return packageJson2.version;
|
|
2936
|
+
} catch {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
async function getLatestVersion() {
|
|
2941
|
+
try {
|
|
2942
|
+
const { stdout } = await execAsync(`npm view ${APP_INFO.packageName} version`);
|
|
2943
|
+
return stdout.trim();
|
|
2944
|
+
} catch {
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
function getUpdateCommand(packageManager) {
|
|
2949
|
+
return updateCommandsByPackageManager[packageManager];
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// src/cli/catalog.ts
|
|
2953
|
+
var accountCommand = defineCommand({
|
|
2954
|
+
name: "account",
|
|
2955
|
+
description: "Manage Google accounts",
|
|
2956
|
+
subcommands: [
|
|
2957
|
+
addAccountCommand,
|
|
2958
|
+
listAccountsCommand,
|
|
2959
|
+
selectAccountCommand,
|
|
2960
|
+
removeAccountCommand,
|
|
2961
|
+
reauthAccountCommand
|
|
2962
|
+
]
|
|
2963
|
+
});
|
|
2964
|
+
var spreadsheetCommand = defineCommand({
|
|
2965
|
+
name: "spreadsheet",
|
|
2966
|
+
description: "Manage spreadsheet configurations",
|
|
2967
|
+
subcommands: [
|
|
2968
|
+
addSpreadsheetCommand,
|
|
2969
|
+
listSpreadsheetsCommand,
|
|
2970
|
+
removeSpreadsheetCommand,
|
|
2971
|
+
selectSpreadsheetCommand,
|
|
2972
|
+
activeSpreadsheetCommand
|
|
2973
|
+
]
|
|
2974
|
+
});
|
|
2975
|
+
var sheetCommand = defineCommand({
|
|
2976
|
+
name: "sheet",
|
|
2977
|
+
description: "Manage and interact with spreadsheet sheets",
|
|
2978
|
+
subcommands: [
|
|
2979
|
+
listCommand,
|
|
2980
|
+
activeCommand,
|
|
2981
|
+
selectCommand,
|
|
2982
|
+
readCommand,
|
|
2983
|
+
addCommand,
|
|
2984
|
+
removeCommand,
|
|
2985
|
+
renameCommand,
|
|
2986
|
+
copyCommand,
|
|
2987
|
+
writeCommand,
|
|
2988
|
+
appendCommand,
|
|
2989
|
+
importCommand,
|
|
2990
|
+
exportCommand,
|
|
2991
|
+
rowAddCommand,
|
|
2992
|
+
rowRemoveCommand
|
|
2993
|
+
]
|
|
2994
|
+
});
|
|
2995
|
+
var cliCommands = [accountCommand, spreadsheetCommand, sheetCommand, updateCommand];
|
|
2996
|
+
var docsCommands = [...cliCommands, completionCommand];
|
|
2997
|
+
|
|
2998
|
+
// src/utils/error-handler.ts
|
|
2999
|
+
function formatError(error) {
|
|
3000
|
+
if (error instanceof Error) {
|
|
3001
|
+
return error.message;
|
|
3002
|
+
}
|
|
3003
|
+
if (typeof error === "string") {
|
|
3004
|
+
return error;
|
|
3005
|
+
}
|
|
3006
|
+
return String(error);
|
|
3007
|
+
}
|
|
3008
|
+
function handleCommandError(baseMessage) {
|
|
3009
|
+
return (error) => {
|
|
3010
|
+
const errorDetails = formatError(error);
|
|
3011
|
+
if (errorDetails.includes("invalid_grant")) {
|
|
3012
|
+
Logger.error("OAuth token refresh failed: invalid_grant");
|
|
3013
|
+
Logger.info("Your refresh token is expired or invalid");
|
|
3014
|
+
Logger.info("Fix: gsheet account reauth");
|
|
3015
|
+
process.exit(1);
|
|
3016
|
+
}
|
|
3017
|
+
const prefix = typeof baseMessage === "function" ? baseMessage(error) : baseMessage;
|
|
3018
|
+
const fullMessage = `${prefix}: ${errorDetails}`;
|
|
3019
|
+
Logger.error(fullMessage);
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// src/cli/register.ts
|
|
3025
|
+
function registerProgram(program2, commands) {
|
|
3026
|
+
for (const command of commands) {
|
|
3027
|
+
if (command.kind === "command") {
|
|
3028
|
+
registerParentCommand(program2, command);
|
|
3029
|
+
for (const subcommand of command.subcommands) {
|
|
3030
|
+
registerSubCommand(program2, command.name, subcommand);
|
|
3031
|
+
}
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
registerSubCommand(program2, void 0, command);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function registerParentCommand(program2, definition) {
|
|
3038
|
+
const command = program2.command(definition.name, definition.description);
|
|
3039
|
+
applyAliases(command, definition.aliases);
|
|
3040
|
+
command.action(async () => {
|
|
3041
|
+
const commandPromise = async () => {
|
|
3042
|
+
console.log(`Usage: ${APP_INFO.name} ${definition.name} <command>`);
|
|
3043
|
+
console.log("");
|
|
3044
|
+
console.log(`Available commands: ${definition.subcommands.map((subcommand) => subcommand.name).join(", ")}`);
|
|
3045
|
+
};
|
|
3046
|
+
await commandPromise().catch(
|
|
3047
|
+
handleCommandError(resolveErrorMessage(definition.errorMessage, `Failed to execute: ${definition.name}`))
|
|
3048
|
+
);
|
|
3049
|
+
});
|
|
3050
|
+
return command;
|
|
3051
|
+
}
|
|
3052
|
+
function registerSubCommand(program2, parentName, definition) {
|
|
3053
|
+
const commandName = parentName ? `${parentName} ${definition.name}` : definition.name;
|
|
3054
|
+
const command = program2.command(commandName, definition.description);
|
|
3055
|
+
applyAliases(command, definition.aliases);
|
|
3056
|
+
applyArguments(command, definition.arguments);
|
|
3057
|
+
applyFlags(command, definition.flags);
|
|
3058
|
+
command.action(async ({ args, options }) => {
|
|
3059
|
+
const commandPromise = async () => {
|
|
3060
|
+
await definition.action({
|
|
3061
|
+
args,
|
|
3062
|
+
options
|
|
3063
|
+
});
|
|
3064
|
+
};
|
|
3065
|
+
await commandPromise().catch(
|
|
3066
|
+
handleCommandError(resolveErrorMessage(definition.errorMessage, `Failed to execute: ${commandName}`))
|
|
3067
|
+
);
|
|
3068
|
+
});
|
|
3069
|
+
return command;
|
|
3070
|
+
}
|
|
3071
|
+
function applyAliases(command, aliases) {
|
|
3072
|
+
if (!aliases) return;
|
|
3073
|
+
for (const alias of aliases) {
|
|
3074
|
+
command.alias(alias);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function applyArguments(command, args) {
|
|
3078
|
+
if (!args) return;
|
|
3079
|
+
for (const arg of args) {
|
|
3080
|
+
const argString = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
3081
|
+
command.argument(argString, arg.description);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function applyFlags(command, flags) {
|
|
3085
|
+
if (!flags) return;
|
|
3086
|
+
for (const flag2 of flags) {
|
|
3087
|
+
let flagString = flag2.name;
|
|
3088
|
+
if (flag2.alias) {
|
|
3089
|
+
flagString = `${flag2.alias}, ${flagString}`;
|
|
3090
|
+
}
|
|
3091
|
+
if (flag2.type === "string" /* String */) {
|
|
3092
|
+
flagString += " <value>";
|
|
3093
|
+
}
|
|
3094
|
+
command.option(flagString, flag2.description, {
|
|
3095
|
+
required: flag2.required,
|
|
3096
|
+
default: flag2.name.startsWith("--no-") ? true : void 0
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
function resolveErrorMessage(errorMessage, fallback) {
|
|
3101
|
+
if (!errorMessage) return () => fallback;
|
|
3102
|
+
if (typeof errorMessage === "function") return errorMessage;
|
|
3103
|
+
return () => errorMessage;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/cli.ts
|
|
3107
|
+
var program = createProgram(getProgramBin());
|
|
3108
|
+
registerProgram(program, cliCommands);
|
|
3109
|
+
createCompletionCommand(program);
|
|
3110
|
+
try {
|
|
3111
|
+
await program.run(process.argv.slice(2).length === 0 ? ["--help"] : process.argv.slice(2));
|
|
3112
|
+
} catch (error) {
|
|
3113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3114
|
+
console.log(`error: ${message}`);
|
|
3115
|
+
process.exitCode = 1;
|
|
3116
|
+
}
|
|
3117
|
+
function createProgram(binName) {
|
|
3118
|
+
return new (getProgramConstructor())().bin(binName).name(binName).description("Google Sheets CLI - A tool to interact with Google Sheets").version(APP_INFO.version).disableGlobalOption("--no-color").disableGlobalOption("--quiet").disableGlobalOption("--silent").disableGlobalOption("-v");
|
|
3119
|
+
}
|
|
3120
|
+
function getProgramConstructor() {
|
|
3121
|
+
const require2 = createRequire(import.meta.url);
|
|
3122
|
+
const module = require2("@caporal/core");
|
|
3123
|
+
const Program = module.Program ?? module.default?.Program;
|
|
3124
|
+
if (!Program) throw new Error("Caporal Program constructor not found");
|
|
3125
|
+
return Program;
|
|
3126
|
+
}
|
|
3127
|
+
function getProgramBin() {
|
|
3128
|
+
if (process.env.SHEET_CMD_PROG_NAME) return process.env.SHEET_CMD_PROG_NAME;
|
|
3129
|
+
if (isDirectRun() && process.argv[1]) return basename(process.argv[1]);
|
|
3130
|
+
return APP_INFO.name;
|
|
3131
|
+
}
|
|
3132
|
+
function isDirectRun() {
|
|
3133
|
+
if (!process.argv[1]) return false;
|
|
3134
|
+
try {
|
|
3135
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath3(import.meta.url));
|
|
3136
|
+
} catch {
|
|
3137
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
//# sourceMappingURL=cli.js.map
|