@plosson/agentio 0.4.3 → 0.4.4
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/package.json +1 -1
- package/src/auth/oauth.ts +10 -2
- package/src/commands/gsheets.ts +365 -0
- package/src/index.ts +2 -0
- package/src/services/gsheets/client.ts +362 -0
- package/src/types/config.ts +2 -1
- package/src/types/gsheets.ts +81 -0
- package/src/utils/output.ts +81 -0
package/package.json
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -44,7 +44,14 @@ const GTASKS_SCOPES = [
|
|
|
44
44
|
'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
|
|
45
45
|
];
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const GSHEETS_SCOPES = [
|
|
48
|
+
'https://www.googleapis.com/auth/spreadsheets', // full access to spreadsheets
|
|
49
|
+
'https://www.googleapis.com/auth/drive.file', // create/access files created by this app
|
|
50
|
+
'https://www.googleapis.com/auth/drive.readonly', // list spreadsheets, export, copy
|
|
51
|
+
'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const SCOPES: Record<'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-full' | 'gcal' | 'gtasks' | 'gsheets', string[]> = {
|
|
48
55
|
gmail: GMAIL_SCOPES,
|
|
49
56
|
gchat: GCHAT_SCOPES,
|
|
50
57
|
gdocs: GDOCS_SCOPES,
|
|
@@ -52,9 +59,10 @@ const SCOPES: Record<'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-f
|
|
|
52
59
|
'gdrive-full': GDRIVE_FULL_SCOPES,
|
|
53
60
|
gcal: GCAL_SCOPES,
|
|
54
61
|
gtasks: GTASKS_SCOPES,
|
|
62
|
+
gsheets: GSHEETS_SCOPES,
|
|
55
63
|
};
|
|
56
64
|
|
|
57
|
-
export type OAuthService = 'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-full' | 'gcal' | 'gtasks';
|
|
65
|
+
export type OAuthService = 'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-full' | 'gcal' | 'gtasks' | 'gsheets';
|
|
58
66
|
|
|
59
67
|
export async function performOAuthFlow(
|
|
60
68
|
service: OAuthService
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { createGoogleAuth } from '../auth/token-manager';
|
|
5
|
+
import { setCredentials } from '../auth/token-store';
|
|
6
|
+
import { setProfile } from '../config/config-manager';
|
|
7
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
8
|
+
import { createClientGetter } from '../utils/client-factory';
|
|
9
|
+
import { performOAuthFlow } from '../auth/oauth';
|
|
10
|
+
import { GSheetsClient } from '../services/gsheets/client';
|
|
11
|
+
import {
|
|
12
|
+
printGSheetsList,
|
|
13
|
+
printGSheetsMetadata,
|
|
14
|
+
printGSheetsValues,
|
|
15
|
+
printGSheetsUpdateResult,
|
|
16
|
+
printGSheetsAppendResult,
|
|
17
|
+
printGSheetsClearResult,
|
|
18
|
+
printGSheetsCreated,
|
|
19
|
+
} from '../utils/output';
|
|
20
|
+
import { CliError, handleError } from '../utils/errors';
|
|
21
|
+
import type { GSheetsCredentials } from '../types/gsheets';
|
|
22
|
+
|
|
23
|
+
const getGSheetsClient = createClientGetter<GSheetsCredentials, GSheetsClient>({
|
|
24
|
+
service: 'gsheets',
|
|
25
|
+
createClient: (credentials) => new GSheetsClient(credentials),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse values from CLI arguments or JSON
|
|
30
|
+
* Supports two formats:
|
|
31
|
+
* - Simple: comma-separated rows, pipe-separated cells (e.g., "a|b|c,d|e|f")
|
|
32
|
+
* - JSON: 2D array (e.g., '[["a","b"],["c","d"]]')
|
|
33
|
+
*/
|
|
34
|
+
function parseValues(valueArgs: string[], valuesJson?: string): unknown[][] {
|
|
35
|
+
if (valuesJson) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(valuesJson);
|
|
38
|
+
if (!Array.isArray(parsed)) {
|
|
39
|
+
throw new Error('JSON must be a 2D array');
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new CliError('INVALID_PARAMS', `Invalid JSON values: ${err instanceof Error ? err.message : err}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (valueArgs.length === 0) {
|
|
48
|
+
throw new CliError('INVALID_PARAMS', 'No values provided', 'Provide values as args or via --values-json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse simple format: comma-separated rows, pipe-separated cells
|
|
52
|
+
const rawValues = valueArgs.join(' ');
|
|
53
|
+
const rows = rawValues.split(',');
|
|
54
|
+
return rows.map((row) => {
|
|
55
|
+
const cells = row.trim().split('|');
|
|
56
|
+
return cells.map((cell) => cell.trim());
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerGSheetsCommands(program: Command): void {
|
|
61
|
+
const gsheets = program.command('gsheets').description('Google Sheets operations');
|
|
62
|
+
|
|
63
|
+
gsheets
|
|
64
|
+
.command('list')
|
|
65
|
+
.description('List recent spreadsheets')
|
|
66
|
+
.option('--profile <name>', 'Profile name')
|
|
67
|
+
.option('--limit <n>', 'Number of spreadsheets', '10')
|
|
68
|
+
.option('--query <query>', 'Drive search query filter')
|
|
69
|
+
.addHelpText(
|
|
70
|
+
'after',
|
|
71
|
+
`
|
|
72
|
+
Query Syntax Examples:
|
|
73
|
+
|
|
74
|
+
Name search:
|
|
75
|
+
--query "name contains 'budget'" Spreadsheets with "budget" in name
|
|
76
|
+
--query "name = 'Q1 Report'" Exact name match
|
|
77
|
+
|
|
78
|
+
Ownership:
|
|
79
|
+
--query "'me' in owners" Spreadsheets you own
|
|
80
|
+
--query "'user@example.com' in owners"
|
|
81
|
+
|
|
82
|
+
Date filters:
|
|
83
|
+
--query "modifiedTime > '2024-01-01'" Modified after date
|
|
84
|
+
--query "createdTime > '2024-01-01'" Created after date
|
|
85
|
+
|
|
86
|
+
Combined:
|
|
87
|
+
--query "name contains 'sales' and modifiedTime > '2024-01-01'"
|
|
88
|
+
`
|
|
89
|
+
)
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
try {
|
|
92
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
93
|
+
const spreadsheets = await client.list({
|
|
94
|
+
limit: parseInt(options.limit, 10),
|
|
95
|
+
query: options.query,
|
|
96
|
+
});
|
|
97
|
+
printGSheetsList(spreadsheets);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
handleError(error);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
gsheets
|
|
104
|
+
.command('get')
|
|
105
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
106
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B10)')
|
|
107
|
+
.description('Get values from a range')
|
|
108
|
+
.option('--profile <name>', 'Profile name')
|
|
109
|
+
.option('--dimension <dim>', 'Major dimension: ROWS or COLUMNS')
|
|
110
|
+
.option('--render <opt>', 'Value render: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA')
|
|
111
|
+
.action(async (spreadsheetId: string, range: string, options) => {
|
|
112
|
+
try {
|
|
113
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
114
|
+
const result = await client.get(spreadsheetId, range, {
|
|
115
|
+
majorDimension: options.dimension,
|
|
116
|
+
valueRenderOption: options.render,
|
|
117
|
+
});
|
|
118
|
+
printGSheetsValues(result);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
handleError(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
gsheets
|
|
125
|
+
.command('update')
|
|
126
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
127
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B2)')
|
|
128
|
+
.argument('[values...]', 'Values (comma-separated rows, pipe-separated cells)')
|
|
129
|
+
.description('Update values in a range')
|
|
130
|
+
.option('--profile <name>', 'Profile name')
|
|
131
|
+
.option('--values-json <json>', 'Values as JSON 2D array')
|
|
132
|
+
.option('--input <opt>', 'Value input option: RAW or USER_ENTERED', 'USER_ENTERED')
|
|
133
|
+
.addHelpText(
|
|
134
|
+
'after',
|
|
135
|
+
`
|
|
136
|
+
Value Formats:
|
|
137
|
+
|
|
138
|
+
Simple format (comma = row separator, pipe = cell separator):
|
|
139
|
+
agentio gsheets update <id> Sheet1!A1:B2 "a|b,c|d"
|
|
140
|
+
Results in:
|
|
141
|
+
A1=a B1=b
|
|
142
|
+
A2=c B2=d
|
|
143
|
+
|
|
144
|
+
JSON format:
|
|
145
|
+
agentio gsheets update <id> Sheet1!A1:B2 --values-json '[["a","b"],["c","d"]]'
|
|
146
|
+
|
|
147
|
+
Input Options:
|
|
148
|
+
RAW - Values are stored exactly as entered (no parsing)
|
|
149
|
+
USER_ENTERED - Values are parsed as if typed in the UI (formulas, dates, etc.)
|
|
150
|
+
`
|
|
151
|
+
)
|
|
152
|
+
.action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
|
|
153
|
+
try {
|
|
154
|
+
const values = parseValues(valueArgs, options.valuesJson);
|
|
155
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
156
|
+
const result = await client.update(spreadsheetId, range, values, {
|
|
157
|
+
valueInputOption: options.input,
|
|
158
|
+
});
|
|
159
|
+
printGSheetsUpdateResult(result);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
handleError(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
gsheets
|
|
166
|
+
.command('append')
|
|
167
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
168
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A:C)')
|
|
169
|
+
.argument('[values...]', 'Values (comma-separated rows, pipe-separated cells)')
|
|
170
|
+
.description('Append values to a range')
|
|
171
|
+
.option('--profile <name>', 'Profile name')
|
|
172
|
+
.option('--values-json <json>', 'Values as JSON 2D array')
|
|
173
|
+
.option('--input <opt>', 'Value input option: RAW or USER_ENTERED', 'USER_ENTERED')
|
|
174
|
+
.option('--insert <opt>', 'Insert data option: OVERWRITE or INSERT_ROWS')
|
|
175
|
+
.addHelpText(
|
|
176
|
+
'after',
|
|
177
|
+
`
|
|
178
|
+
Value Formats:
|
|
179
|
+
|
|
180
|
+
Simple format (comma = row separator, pipe = cell separator):
|
|
181
|
+
agentio gsheets append <id> Sheet1!A:C "a|b|c,d|e|f"
|
|
182
|
+
Appends two rows to columns A, B, C
|
|
183
|
+
|
|
184
|
+
JSON format:
|
|
185
|
+
agentio gsheets append <id> Sheet1!A:C --values-json '[["a","b","c"],["d","e","f"]]'
|
|
186
|
+
|
|
187
|
+
Insert Options:
|
|
188
|
+
OVERWRITE - New data overwrites existing data in the areas it is written
|
|
189
|
+
INSERT_ROWS - Rows are inserted for the new data
|
|
190
|
+
`
|
|
191
|
+
)
|
|
192
|
+
.action(async (spreadsheetId: string, range: string, valueArgs: string[], options) => {
|
|
193
|
+
try {
|
|
194
|
+
const values = parseValues(valueArgs, options.valuesJson);
|
|
195
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
196
|
+
const result = await client.append(spreadsheetId, range, values, {
|
|
197
|
+
valueInputOption: options.input,
|
|
198
|
+
insertDataOption: options.insert,
|
|
199
|
+
});
|
|
200
|
+
printGSheetsAppendResult(result);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
handleError(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
gsheets
|
|
207
|
+
.command('clear')
|
|
208
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
209
|
+
.argument('<range>', 'Range in A1 notation (e.g., Sheet1!A1:B10)')
|
|
210
|
+
.description('Clear values in a range')
|
|
211
|
+
.option('--profile <name>', 'Profile name')
|
|
212
|
+
.action(async (spreadsheetId: string, range: string, options) => {
|
|
213
|
+
try {
|
|
214
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
215
|
+
const result = await client.clear(spreadsheetId, range);
|
|
216
|
+
printGSheetsClearResult(result);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
handleError(error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
gsheets
|
|
223
|
+
.command('metadata')
|
|
224
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
225
|
+
.description('Get spreadsheet metadata')
|
|
226
|
+
.option('--profile <name>', 'Profile name')
|
|
227
|
+
.action(async (spreadsheetId: string, options) => {
|
|
228
|
+
try {
|
|
229
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
230
|
+
const metadata = await client.metadata(spreadsheetId);
|
|
231
|
+
printGSheetsMetadata(metadata);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
handleError(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
gsheets
|
|
238
|
+
.command('create')
|
|
239
|
+
.argument('<title>', 'Spreadsheet title')
|
|
240
|
+
.description('Create a new spreadsheet')
|
|
241
|
+
.option('--profile <name>', 'Profile name')
|
|
242
|
+
.option('--sheets <names>', 'Comma-separated sheet names to create')
|
|
243
|
+
.action(async (title: string, options) => {
|
|
244
|
+
try {
|
|
245
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
246
|
+
const sheetNames = options.sheets ? options.sheets.split(',').map((n: string) => n.trim()) : undefined;
|
|
247
|
+
const result = await client.create(title, sheetNames);
|
|
248
|
+
printGSheetsCreated(result);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
handleError(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
gsheets
|
|
255
|
+
.command('copy')
|
|
256
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
257
|
+
.argument('<title>', 'New spreadsheet title')
|
|
258
|
+
.description('Copy a spreadsheet')
|
|
259
|
+
.option('--profile <name>', 'Profile name')
|
|
260
|
+
.option('--parent <folder-id>', 'Destination folder ID')
|
|
261
|
+
.action(async (spreadsheetId: string, title: string, options) => {
|
|
262
|
+
try {
|
|
263
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
264
|
+
const result = await client.copy(spreadsheetId, title, options.parent);
|
|
265
|
+
printGSheetsCreated(result);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
handleError(error);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
gsheets
|
|
272
|
+
.command('export')
|
|
273
|
+
.argument('<spreadsheet-id-or-url>', 'Spreadsheet ID or URL')
|
|
274
|
+
.description('Export a spreadsheet to a file')
|
|
275
|
+
.option('--profile <name>', 'Profile name')
|
|
276
|
+
.requiredOption('--output <path>', 'Output file path')
|
|
277
|
+
.option('--format <fmt>', 'Export format: xlsx, pdf, csv, ods, tsv', 'xlsx')
|
|
278
|
+
.addHelpText(
|
|
279
|
+
'after',
|
|
280
|
+
`
|
|
281
|
+
Export Formats:
|
|
282
|
+
xlsx - Microsoft Excel (default)
|
|
283
|
+
pdf - PDF document
|
|
284
|
+
csv - Comma-separated values (first sheet only)
|
|
285
|
+
ods - OpenDocument Spreadsheet
|
|
286
|
+
tsv - Tab-separated values (first sheet only)
|
|
287
|
+
|
|
288
|
+
Examples:
|
|
289
|
+
agentio gsheets export <id> --output report.xlsx
|
|
290
|
+
agentio gsheets export <id> --output data.csv --format csv
|
|
291
|
+
`
|
|
292
|
+
)
|
|
293
|
+
.action(async (spreadsheetId: string, options) => {
|
|
294
|
+
try {
|
|
295
|
+
const { client } = await getGSheetsClient(options.profile);
|
|
296
|
+
const format = options.format.toLowerCase();
|
|
297
|
+
|
|
298
|
+
if (!['xlsx', 'pdf', 'csv', 'ods', 'tsv'].includes(format)) {
|
|
299
|
+
throw new CliError('INVALID_PARAMS', `Unknown format: ${format}`, 'Use xlsx, pdf, csv, ods, or tsv');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const result = await client.export(spreadsheetId, format as 'xlsx' | 'pdf' | 'csv' | 'ods' | 'tsv');
|
|
303
|
+
await writeFile(options.output, result.data);
|
|
304
|
+
console.log(`Exported to ${options.output}`);
|
|
305
|
+
console.log(` Format: ${format}`);
|
|
306
|
+
console.log(` Size: ${result.data.length} bytes`);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
handleError(error);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Profile management
|
|
313
|
+
const profile = createProfileCommands<GSheetsCredentials>(gsheets, {
|
|
314
|
+
service: 'gsheets',
|
|
315
|
+
displayName: 'Google Sheets',
|
|
316
|
+
getExtraInfo: (credentials) => (credentials?.email ? ` - ${credentials.email}` : ''),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
profile
|
|
320
|
+
.command('add')
|
|
321
|
+
.description('Add a new Google Sheets profile')
|
|
322
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
323
|
+
.action(async (options) => {
|
|
324
|
+
try {
|
|
325
|
+
console.error('Starting OAuth flow for Google Sheets...\n');
|
|
326
|
+
|
|
327
|
+
const tokens = await performOAuthFlow('gsheets');
|
|
328
|
+
const auth = createGoogleAuth(tokens);
|
|
329
|
+
|
|
330
|
+
// Fetch user email for profile naming
|
|
331
|
+
let userEmail: string;
|
|
332
|
+
try {
|
|
333
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
334
|
+
const userInfo = await oauth2.userinfo.get();
|
|
335
|
+
userEmail = userInfo.data.email || '';
|
|
336
|
+
if (!userEmail) {
|
|
337
|
+
throw new Error('No email returned');
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
341
|
+
throw new CliError('AUTH_FAILED', `Failed to fetch user email: ${errorMessage}`, 'Ensure the account has an email address');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const profileName = options.profile || userEmail;
|
|
345
|
+
|
|
346
|
+
const credentials: GSheetsCredentials = {
|
|
347
|
+
accessToken: tokens.access_token,
|
|
348
|
+
refreshToken: tokens.refresh_token,
|
|
349
|
+
expiryDate: tokens.expiry_date,
|
|
350
|
+
tokenType: tokens.token_type,
|
|
351
|
+
scope: tokens.scope,
|
|
352
|
+
email: userEmail,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
await setProfile('gsheets', profileName);
|
|
356
|
+
await setCredentials('gsheets', profileName, credentials);
|
|
357
|
+
|
|
358
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
359
|
+
console.log(` Email: ${userEmail}`);
|
|
360
|
+
console.log(` Test with: agentio gsheets list --profile ${profileName}`);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
handleError(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { registerGDocsCommands } from './commands/gdocs';
|
|
|
10
10
|
import { registerGDriveCommands } from './commands/gdrive';
|
|
11
11
|
import { registerGitHubCommands } from './commands/github';
|
|
12
12
|
import { registerGmailCommands } from './commands/gmail';
|
|
13
|
+
import { registerGSheetsCommands } from './commands/gsheets';
|
|
13
14
|
import { registerGTasksCommands } from './commands/gtasks';
|
|
14
15
|
import { registerJiraCommands } from './commands/jira';
|
|
15
16
|
import { registerRssCommands } from './commands/rss';
|
|
@@ -51,6 +52,7 @@ registerGDocsCommands(program);
|
|
|
51
52
|
registerGDriveCommands(program);
|
|
52
53
|
registerGitHubCommands(program);
|
|
53
54
|
registerGmailCommands(program);
|
|
55
|
+
registerGSheetsCommands(program);
|
|
54
56
|
registerGTasksCommands(program);
|
|
55
57
|
registerJiraCommands(program);
|
|
56
58
|
registerRssCommands(program);
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import type { sheets_v4, drive_v3 } from 'googleapis';
|
|
3
|
+
import { CliError, httpStatusToErrorCode, type ErrorCode } from '../../utils/errors';
|
|
4
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
5
|
+
import { GOOGLE_OAUTH_CONFIG } from '../../config/credentials';
|
|
6
|
+
import type {
|
|
7
|
+
GSheetsCredentials,
|
|
8
|
+
GSheetsSpreadsheet,
|
|
9
|
+
GSheetsSheet,
|
|
10
|
+
GSheetsGetOptions,
|
|
11
|
+
GSheetsGetResult,
|
|
12
|
+
GSheetsUpdateOptions,
|
|
13
|
+
GSheetsUpdateResult,
|
|
14
|
+
GSheetsAppendOptions,
|
|
15
|
+
GSheetsAppendResult,
|
|
16
|
+
GSheetsClearResult,
|
|
17
|
+
GSheetsCreateResult,
|
|
18
|
+
GSheetsListOptions,
|
|
19
|
+
GSheetsListItem,
|
|
20
|
+
} from '../../types/gsheets';
|
|
21
|
+
|
|
22
|
+
export class GSheetsClient implements ServiceClient {
|
|
23
|
+
private credentials: GSheetsCredentials;
|
|
24
|
+
private sheets: sheets_v4.Sheets;
|
|
25
|
+
private drive: drive_v3.Drive;
|
|
26
|
+
|
|
27
|
+
constructor(credentials: GSheetsCredentials) {
|
|
28
|
+
this.credentials = credentials;
|
|
29
|
+
const auth = this.createOAuthClient();
|
|
30
|
+
this.sheets = google.sheets({ version: 'v4', auth });
|
|
31
|
+
this.drive = google.drive({ version: 'v3', auth });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async validate(): Promise<ValidationResult> {
|
|
35
|
+
try {
|
|
36
|
+
await this.drive.files.list({
|
|
37
|
+
pageSize: 1,
|
|
38
|
+
q: "mimeType='application/vnd.google-apps.spreadsheet'",
|
|
39
|
+
});
|
|
40
|
+
return { valid: true, info: this.credentials.email };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
43
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
44
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
45
|
+
}
|
|
46
|
+
return { valid: false, error: message };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async list(options: GSheetsListOptions = {}): Promise<GSheetsListItem[]> {
|
|
51
|
+
const { limit = 10, query } = options;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
let q = "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false";
|
|
55
|
+
if (query) {
|
|
56
|
+
q += ` and ${query}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const response = await this.drive.files.list({
|
|
60
|
+
pageSize: Math.min(limit, 100),
|
|
61
|
+
q,
|
|
62
|
+
fields: 'files(id,name,owners,createdTime,modifiedTime,webViewLink)',
|
|
63
|
+
orderBy: 'modifiedTime desc',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const files = response.data.files || [];
|
|
67
|
+
return files.map((file) => ({
|
|
68
|
+
id: file.id!,
|
|
69
|
+
title: file.name || 'Untitled',
|
|
70
|
+
owner: file.owners?.[0]?.displayName || file.owners?.[0]?.emailAddress || undefined,
|
|
71
|
+
createdTime: file.createdTime || undefined,
|
|
72
|
+
modifiedTime: file.modifiedTime || undefined,
|
|
73
|
+
webViewLink: file.webViewLink || `https://docs.google.com/spreadsheets/d/${file.id}`,
|
|
74
|
+
}));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
this.throwApiError(err, 'list spreadsheets');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async get(spreadsheetIdOrUrl: string, range: string, options: GSheetsGetOptions = {}): Promise<GSheetsGetResult> {
|
|
81
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
82
|
+
const cleanedRange = this.cleanRange(range);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const call = this.sheets.spreadsheets.values.get({
|
|
86
|
+
spreadsheetId,
|
|
87
|
+
range: cleanedRange,
|
|
88
|
+
majorDimension: options.majorDimension,
|
|
89
|
+
valueRenderOption: options.valueRenderOption,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const response = await call;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
range: response.data.range || cleanedRange,
|
|
96
|
+
values: (response.data.values as unknown[][]) || [],
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.throwApiError(err, 'get values');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async update(
|
|
104
|
+
spreadsheetIdOrUrl: string,
|
|
105
|
+
range: string,
|
|
106
|
+
values: unknown[][],
|
|
107
|
+
options: GSheetsUpdateOptions = {}
|
|
108
|
+
): Promise<GSheetsUpdateResult> {
|
|
109
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
110
|
+
const cleanedRange = this.cleanRange(range);
|
|
111
|
+
const valueInputOption = options.valueInputOption || 'USER_ENTERED';
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const response = await this.sheets.spreadsheets.values.update({
|
|
115
|
+
spreadsheetId,
|
|
116
|
+
range: cleanedRange,
|
|
117
|
+
valueInputOption,
|
|
118
|
+
requestBody: {
|
|
119
|
+
values,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
updatedRange: response.data.updatedRange || cleanedRange,
|
|
125
|
+
updatedRows: response.data.updatedRows || 0,
|
|
126
|
+
updatedColumns: response.data.updatedColumns || 0,
|
|
127
|
+
updatedCells: response.data.updatedCells || 0,
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.throwApiError(err, 'update values');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async append(
|
|
135
|
+
spreadsheetIdOrUrl: string,
|
|
136
|
+
range: string,
|
|
137
|
+
values: unknown[][],
|
|
138
|
+
options: GSheetsAppendOptions = {}
|
|
139
|
+
): Promise<GSheetsAppendResult> {
|
|
140
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
141
|
+
const cleanedRange = this.cleanRange(range);
|
|
142
|
+
const valueInputOption = options.valueInputOption || 'USER_ENTERED';
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await this.sheets.spreadsheets.values.append({
|
|
146
|
+
spreadsheetId,
|
|
147
|
+
range: cleanedRange,
|
|
148
|
+
valueInputOption,
|
|
149
|
+
insertDataOption: options.insertDataOption,
|
|
150
|
+
requestBody: {
|
|
151
|
+
values,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const updates = response.data.updates;
|
|
156
|
+
return {
|
|
157
|
+
updatedRange: updates?.updatedRange || cleanedRange,
|
|
158
|
+
updatedRows: updates?.updatedRows || 0,
|
|
159
|
+
updatedColumns: updates?.updatedColumns || 0,
|
|
160
|
+
updatedCells: updates?.updatedCells || 0,
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
this.throwApiError(err, 'append values');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async clear(spreadsheetIdOrUrl: string, range: string): Promise<GSheetsClearResult> {
|
|
168
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
169
|
+
const cleanedRange = this.cleanRange(range);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await this.sheets.spreadsheets.values.clear({
|
|
173
|
+
spreadsheetId,
|
|
174
|
+
range: cleanedRange,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
clearedRange: response.data.clearedRange || cleanedRange,
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
this.throwApiError(err, 'clear values');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async metadata(spreadsheetIdOrUrl: string): Promise<GSheetsSpreadsheet> {
|
|
186
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const response = await this.sheets.spreadsheets.get({
|
|
190
|
+
spreadsheetId,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const props = response.data.properties!;
|
|
194
|
+
const sheets: GSheetsSheet[] = (response.data.sheets || []).map((sheet) => ({
|
|
195
|
+
id: sheet.properties?.sheetId || 0,
|
|
196
|
+
title: sheet.properties?.title || 'Untitled',
|
|
197
|
+
rowCount: sheet.properties?.gridProperties?.rowCount || 0,
|
|
198
|
+
columnCount: sheet.properties?.gridProperties?.columnCount || 0,
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
id: response.data.spreadsheetId!,
|
|
203
|
+
title: props.title || 'Untitled',
|
|
204
|
+
locale: props.locale || undefined,
|
|
205
|
+
timeZone: props.timeZone || undefined,
|
|
206
|
+
url: response.data.spreadsheetUrl || `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
|
207
|
+
sheets,
|
|
208
|
+
};
|
|
209
|
+
} catch (err) {
|
|
210
|
+
this.throwApiError(err, 'get metadata');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async create(title: string, sheetNames?: string[]): Promise<GSheetsCreateResult> {
|
|
215
|
+
try {
|
|
216
|
+
const sheets: sheets_v4.Schema$Sheet[] | undefined = sheetNames?.map((name) => ({
|
|
217
|
+
properties: {
|
|
218
|
+
title: name.trim(),
|
|
219
|
+
},
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const response = await this.sheets.spreadsheets.create({
|
|
223
|
+
requestBody: {
|
|
224
|
+
properties: {
|
|
225
|
+
title,
|
|
226
|
+
},
|
|
227
|
+
sheets,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id: response.data.spreadsheetId!,
|
|
233
|
+
title: response.data.properties?.title || title,
|
|
234
|
+
url: response.data.spreadsheetUrl || `https://docs.google.com/spreadsheets/d/${response.data.spreadsheetId}`,
|
|
235
|
+
};
|
|
236
|
+
} catch (err) {
|
|
237
|
+
this.throwApiError(err, 'create spreadsheet');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async copy(spreadsheetIdOrUrl: string, newTitle: string, parentFolderId?: string): Promise<GSheetsCreateResult> {
|
|
242
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const response = await this.drive.files.copy({
|
|
246
|
+
fileId: spreadsheetId,
|
|
247
|
+
requestBody: {
|
|
248
|
+
name: newTitle,
|
|
249
|
+
parents: parentFolderId ? [parentFolderId] : undefined,
|
|
250
|
+
},
|
|
251
|
+
fields: 'id,name,webViewLink',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
id: response.data.id!,
|
|
256
|
+
title: response.data.name || newTitle,
|
|
257
|
+
url: response.data.webViewLink || `https://docs.google.com/spreadsheets/d/${response.data.id}`,
|
|
258
|
+
};
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this.throwApiError(err, 'copy spreadsheet');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async export(
|
|
265
|
+
spreadsheetIdOrUrl: string,
|
|
266
|
+
format: 'xlsx' | 'pdf' | 'csv' | 'ods' | 'tsv'
|
|
267
|
+
): Promise<{ data: Buffer; mimeType: string; extension: string }> {
|
|
268
|
+
const spreadsheetId = this.extractSpreadsheetId(spreadsheetIdOrUrl);
|
|
269
|
+
|
|
270
|
+
const formatMap: Record<string, { mimeType: string; extension: string }> = {
|
|
271
|
+
xlsx: {
|
|
272
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
273
|
+
extension: '.xlsx',
|
|
274
|
+
},
|
|
275
|
+
pdf: { mimeType: 'application/pdf', extension: '.pdf' },
|
|
276
|
+
csv: { mimeType: 'text/csv', extension: '.csv' },
|
|
277
|
+
ods: { mimeType: 'application/vnd.oasis.opendocument.spreadsheet', extension: '.ods' },
|
|
278
|
+
tsv: { mimeType: 'text/tab-separated-values', extension: '.tsv' },
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const formatInfo = formatMap[format];
|
|
282
|
+
if (!formatInfo) {
|
|
283
|
+
throw new CliError('INVALID_PARAMS', `Unknown export format: ${format}`, 'Use xlsx, pdf, csv, ods, or tsv');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const response = await this.drive.files.export(
|
|
288
|
+
{ fileId: spreadsheetId, mimeType: formatInfo.mimeType },
|
|
289
|
+
{ responseType: 'arraybuffer' }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
data: Buffer.from(response.data as ArrayBuffer),
|
|
294
|
+
mimeType: formatInfo.mimeType,
|
|
295
|
+
extension: formatInfo.extension,
|
|
296
|
+
};
|
|
297
|
+
} catch (err) {
|
|
298
|
+
this.throwApiError(err, 'export spreadsheet');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private extractSpreadsheetId(idOrUrl: string): string {
|
|
303
|
+
// Handle URLs like https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit
|
|
304
|
+
const urlMatch = idOrUrl.match(/\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/);
|
|
305
|
+
if (urlMatch) return urlMatch[1];
|
|
306
|
+
|
|
307
|
+
// Handle other URL formats
|
|
308
|
+
const idMatch = idOrUrl.match(/id=([a-zA-Z0-9_-]+)/);
|
|
309
|
+
if (idMatch) return idMatch[1];
|
|
310
|
+
|
|
311
|
+
// Assume it's already an ID
|
|
312
|
+
return idOrUrl;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private cleanRange(range: string): string {
|
|
316
|
+
// Some shells escape ! to \! (bash history expansion), which breaks API calls
|
|
317
|
+
return range.replace(/\\!/g, '!');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private createOAuthClient() {
|
|
321
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
322
|
+
GOOGLE_OAUTH_CONFIG.clientId,
|
|
323
|
+
GOOGLE_OAUTH_CONFIG.clientSecret
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
oauth2Client.setCredentials({
|
|
327
|
+
access_token: this.credentials.accessToken,
|
|
328
|
+
refresh_token: this.credentials.refreshToken,
|
|
329
|
+
expiry_date: this.credentials.expiryDate,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return oauth2Client;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private throwApiError(err: unknown, operation: string): never {
|
|
336
|
+
const code = this.getErrorCode(err);
|
|
337
|
+
const message = this.getErrorMessage(err);
|
|
338
|
+
throw new CliError(code, `Failed to ${operation}: ${message}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private getErrorCode(err: unknown): ErrorCode {
|
|
342
|
+
if (err && typeof err === 'object') {
|
|
343
|
+
const error = err as Record<string, unknown>;
|
|
344
|
+
const code = error.code || error.status;
|
|
345
|
+
if (typeof code === 'number') return httpStatusToErrorCode(code);
|
|
346
|
+
}
|
|
347
|
+
return 'API_ERROR';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private getErrorMessage(err: unknown): string {
|
|
351
|
+
if (err && typeof err === 'object') {
|
|
352
|
+
const error = err as Record<string, unknown>;
|
|
353
|
+
const code = error.code || error.status;
|
|
354
|
+
if (code === 401) return 'OAuth token expired or invalid';
|
|
355
|
+
if (code === 403) return 'Insufficient permissions to access this spreadsheet';
|
|
356
|
+
if (code === 404) return 'Spreadsheet not found';
|
|
357
|
+
if (code === 429) return 'Rate limit exceeded, please try again later';
|
|
358
|
+
if (error.message && typeof error.message === 'string') return error.message;
|
|
359
|
+
}
|
|
360
|
+
return err instanceof Error ? err.message : String(err);
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface Config {
|
|
|
37
37
|
gcal?: string[];
|
|
38
38
|
gtasks?: string[];
|
|
39
39
|
gchat?: string[];
|
|
40
|
+
gsheets?: string[];
|
|
40
41
|
github?: string[];
|
|
41
42
|
jira?: string[];
|
|
42
43
|
slack?: string[];
|
|
@@ -49,4 +50,4 @@ export interface Config {
|
|
|
49
50
|
gateway?: GatewayConfig;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export type ServiceName = 'gdocs' | 'gdrive' | 'gmail' | 'gcal' | 'gtasks' | 'gchat' | 'github' | 'jira' | 'slack' | 'telegram' | 'whatsapp' | 'discourse' | 'sql';
|
|
53
|
+
export type ServiceName = 'gdocs' | 'gdrive' | 'gmail' | 'gcal' | 'gtasks' | 'gchat' | 'gsheets' | 'github' | 'jira' | 'slack' | 'telegram' | 'whatsapp' | 'discourse' | 'sql';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface GSheetsCredentials {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
expiryDate?: number;
|
|
5
|
+
tokenType: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
email: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GSheetsSpreadsheet {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
locale?: string;
|
|
14
|
+
timeZone?: string;
|
|
15
|
+
url: string;
|
|
16
|
+
sheets: GSheetsSheet[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GSheetsSheet {
|
|
20
|
+
id: number;
|
|
21
|
+
title: string;
|
|
22
|
+
rowCount: number;
|
|
23
|
+
columnCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GSheetsGetOptions {
|
|
27
|
+
majorDimension?: 'ROWS' | 'COLUMNS';
|
|
28
|
+
valueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GSheetsGetResult {
|
|
32
|
+
range: string;
|
|
33
|
+
values: unknown[][];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GSheetsUpdateOptions {
|
|
37
|
+
valueInputOption?: 'RAW' | 'USER_ENTERED';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GSheetsUpdateResult {
|
|
41
|
+
updatedRange: string;
|
|
42
|
+
updatedRows: number;
|
|
43
|
+
updatedColumns: number;
|
|
44
|
+
updatedCells: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GSheetsAppendOptions {
|
|
48
|
+
valueInputOption?: 'RAW' | 'USER_ENTERED';
|
|
49
|
+
insertDataOption?: 'OVERWRITE' | 'INSERT_ROWS';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GSheetsAppendResult {
|
|
53
|
+
updatedRange: string;
|
|
54
|
+
updatedRows: number;
|
|
55
|
+
updatedColumns: number;
|
|
56
|
+
updatedCells: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GSheetsClearResult {
|
|
60
|
+
clearedRange: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GSheetsCreateResult {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
url: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GSheetsListOptions {
|
|
70
|
+
limit?: number;
|
|
71
|
+
query?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface GSheetsListItem {
|
|
75
|
+
id: string;
|
|
76
|
+
title: string;
|
|
77
|
+
owner?: string;
|
|
78
|
+
createdTime?: string;
|
|
79
|
+
modifiedTime?: string;
|
|
80
|
+
webViewLink: string;
|
|
81
|
+
}
|
package/src/utils/output.ts
CHANGED
|
@@ -405,6 +405,87 @@ export function printGDocCreated(result: GDocsCreateResult): void {
|
|
|
405
405
|
console.log(`Link: ${result.webViewLink}`);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// Google Sheets specific formatters
|
|
409
|
+
import type {
|
|
410
|
+
GSheetsListItem,
|
|
411
|
+
GSheetsSpreadsheet,
|
|
412
|
+
GSheetsGetResult,
|
|
413
|
+
GSheetsUpdateResult,
|
|
414
|
+
GSheetsAppendResult,
|
|
415
|
+
GSheetsClearResult,
|
|
416
|
+
GSheetsCreateResult,
|
|
417
|
+
} from '../types/gsheets';
|
|
418
|
+
|
|
419
|
+
export function printGSheetsList(spreadsheets: GSheetsListItem[]): void {
|
|
420
|
+
if (spreadsheets.length === 0) {
|
|
421
|
+
console.log('No spreadsheets found');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.log(`Spreadsheets (${spreadsheets.length})\n`);
|
|
426
|
+
|
|
427
|
+
for (let i = 0; i < spreadsheets.length; i++) {
|
|
428
|
+
const sheet = spreadsheets[i];
|
|
429
|
+
console.log(`[${i + 1}] ${sheet.title}`);
|
|
430
|
+
console.log(` ID: ${sheet.id}`);
|
|
431
|
+
if (sheet.owner) console.log(` Owner: ${sheet.owner}`);
|
|
432
|
+
if (sheet.modifiedTime) console.log(` Modified: ${sheet.modifiedTime}`);
|
|
433
|
+
console.log(` Link: ${sheet.webViewLink}`);
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function printGSheetsMetadata(spreadsheet: GSheetsSpreadsheet): void {
|
|
439
|
+
console.log(`ID: ${spreadsheet.id}`);
|
|
440
|
+
console.log(`Title: ${spreadsheet.title}`);
|
|
441
|
+
if (spreadsheet.locale) console.log(`Locale: ${spreadsheet.locale}`);
|
|
442
|
+
if (spreadsheet.timeZone) console.log(`TimeZone: ${spreadsheet.timeZone}`);
|
|
443
|
+
console.log(`URL: ${spreadsheet.url}`);
|
|
444
|
+
console.log('');
|
|
445
|
+
console.log('Sheets:');
|
|
446
|
+
|
|
447
|
+
for (const sheet of spreadsheet.sheets) {
|
|
448
|
+
console.log(` [${sheet.id}] ${sheet.title} (${sheet.rowCount} rows x ${sheet.columnCount} cols)`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function printGSheetsValues(result: GSheetsGetResult): void {
|
|
453
|
+
if (result.values.length === 0) {
|
|
454
|
+
console.log('No data found');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.log(`Range: ${result.range}\n`);
|
|
459
|
+
|
|
460
|
+
for (const row of result.values) {
|
|
461
|
+
const cells = row.map((cell) => String(cell ?? ''));
|
|
462
|
+
console.log(cells.join('\t'));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function printGSheetsUpdateResult(result: GSheetsUpdateResult): void {
|
|
467
|
+
console.log(`Updated ${result.updatedCells} cells in ${result.updatedRange}`);
|
|
468
|
+
console.log(` Rows: ${result.updatedRows}`);
|
|
469
|
+
console.log(` Columns: ${result.updatedColumns}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function printGSheetsAppendResult(result: GSheetsAppendResult): void {
|
|
473
|
+
console.log(`Appended ${result.updatedCells} cells to ${result.updatedRange}`);
|
|
474
|
+
console.log(` Rows: ${result.updatedRows}`);
|
|
475
|
+
console.log(` Columns: ${result.updatedColumns}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function printGSheetsClearResult(result: GSheetsClearResult): void {
|
|
479
|
+
console.log(`Cleared ${result.clearedRange}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function printGSheetsCreated(result: GSheetsCreateResult): void {
|
|
483
|
+
console.log('Spreadsheet created');
|
|
484
|
+
console.log(`ID: ${result.id}`);
|
|
485
|
+
console.log(`Title: ${result.title}`);
|
|
486
|
+
console.log(`URL: ${result.url}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
408
489
|
// Google Drive specific formatters
|
|
409
490
|
function getShortMimeType(mimeType: string): string {
|
|
410
491
|
const shortTypes: Record<string, string> = {
|