clipy-kit 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/auth.d.ts +53 -0
  4. package/dist/auth.js +314 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.js +160 -0
  7. package/dist/color.d.ts +12 -0
  8. package/dist/color.js +19 -0
  9. package/dist/colors.d.ts +6 -0
  10. package/dist/colors.js +26 -0
  11. package/dist/host/doctor.d.ts +17 -0
  12. package/dist/host/doctor.js +54 -0
  13. package/dist/host/help.d.ts +20 -0
  14. package/dist/host/help.js +142 -0
  15. package/dist/host/setup.d.ts +16 -0
  16. package/dist/host/setup.js +93 -0
  17. package/dist/http.d.ts +14 -0
  18. package/dist/http.js +53 -0
  19. package/dist/ids.d.ts +2 -0
  20. package/dist/ids.js +21 -0
  21. package/dist/json-input.d.ts +4 -0
  22. package/dist/json-input.js +34 -0
  23. package/dist/lib/format.d.ts +14 -0
  24. package/dist/lib/format.js +73 -0
  25. package/dist/lib/index.d.ts +6 -0
  26. package/dist/lib/index.js +6 -0
  27. package/dist/lib/ops.d.ts +460 -0
  28. package/dist/lib/ops.js +475 -0
  29. package/dist/lib/table.d.ts +14 -0
  30. package/dist/lib/table.js +67 -0
  31. package/dist/lib/types.d.ts +26 -0
  32. package/dist/lib/types.js +1 -0
  33. package/dist/output.d.ts +37 -0
  34. package/dist/output.js +67 -0
  35. package/dist/plugins/catalog.d.ts +11 -0
  36. package/dist/plugins/catalog.js +31 -0
  37. package/dist/plugins/sheet/plugin.d.ts +2 -0
  38. package/dist/plugins/sheet/plugin.js +8 -0
  39. package/dist/plugins/sheet/register.d.ts +2 -0
  40. package/dist/plugins/sheet/register.js +529 -0
  41. package/dist/plugins/sheet/specs.d.ts +20 -0
  42. package/dist/plugins/sheet/specs.js +291 -0
  43. package/dist/plugins/types.d.ts +7 -0
  44. package/dist/plugins/types.js +1 -0
  45. package/dist/ranges.d.ts +20 -0
  46. package/dist/ranges.js +77 -0
  47. package/dist/run.d.ts +353 -0
  48. package/dist/run.js +36 -0
  49. package/dist/specs.d.ts +1 -0
  50. package/dist/specs.js +1 -0
  51. package/package.json +50 -0
  52. package/skills/clipy/SKILL.md +38 -0
  53. package/skills/clipy-sheet/SKILL.md +71 -0
@@ -0,0 +1,475 @@
1
+ import { CliError } from '../output.js';
2
+ import { parseA1, qualifyRange, toGridRange } from '../ranges.js';
3
+ import { formatToRepeatCell, freezeRequest } from './format.js';
4
+ import { findHeader, objectToRow, unknownKeys, valuesToTable } from './table.js';
5
+ export function createOps(client) {
6
+ return {
7
+ getSpreadsheet: (id, fields) => getSpreadsheet(client, id, fields),
8
+ findSpreadsheets: (name, limit) => findSpreadsheets(client, name, limit),
9
+ info: (id) => info(client, id),
10
+ listTabs: (id) => listTabs(client, id),
11
+ readTable: (id, opts) => readTable(client, id, opts),
12
+ readRange: (id, range) => readRange(client, id, range),
13
+ append: (id, opts) => appendRows(client, id, opts),
14
+ update: (id, opts) => updateByKey(client, id, opts),
15
+ upsert: (id, opts) => upsertRows(client, id, opts),
16
+ setRange: (id, opts) => setRange(client, id, opts),
17
+ clear: (id, range, dryRun) => clearRange(client, id, range, dryRun),
18
+ batch: (id, ops, dryRun) => batchOps(client, id, ops, dryRun),
19
+ create: (opts) => createSpreadsheet(client, opts),
20
+ tabAdd: (id, opts) => tabAdd(client, id, opts),
21
+ tabRename: (id, opts) => tabRename(client, id, opts),
22
+ tabDelete: (id, opts) => tabDelete(client, id, opts),
23
+ tabDuplicate: (id, opts) => tabDuplicate(client, id, opts),
24
+ format: (id, opts) => formatRange(client, id, opts),
25
+ formatHeader: (id, opts) => formatHeader(client, id, opts),
26
+ freeze: (id, opts) => freeze(client, id, opts),
27
+ numberFormat: (id, opts) => numberFormat(client, id, opts),
28
+ formula: (id, opts) => writeFormula(client, id, opts),
29
+ fillDown: (id, opts) => fillDown(client, id, opts),
30
+ share: (id, opts) => share(client, id, opts),
31
+ unshare: (id, email, dryRun) => unshare(client, id, email, dryRun),
32
+ permissions: (id) => listPermissions(client, id),
33
+ api: (method, id, json) => apiEscape(client, method, id, json),
34
+ };
35
+ }
36
+ const VALUE_INPUT = { valueInputOption: 'USER_ENTERED' };
37
+ async function getSpreadsheet(client, id, fields) {
38
+ return client.sheets('GET', `/spreadsheets/${encodeURIComponent(id)}`, {
39
+ query: { fields: fields || 'spreadsheetId,spreadsheetUrl,properties,sheets.properties' },
40
+ });
41
+ }
42
+ async function findSpreadsheets(client, name, limit = 10) {
43
+ const q = `mimeType='application/vnd.google-apps.spreadsheet' and name contains '${name.replace(/'/g, "\\'")}' and trashed=false`;
44
+ const data = await client.drive('GET', '/files', {
45
+ query: {
46
+ q,
47
+ pageSize: String(limit),
48
+ fields: 'files(id,name,webViewLink)',
49
+ },
50
+ });
51
+ return {
52
+ query: name,
53
+ files: (data.files ?? []).map((f) => ({
54
+ id: f.id,
55
+ name: f.name,
56
+ url: f.webViewLink || `https://docs.google.com/spreadsheets/d/${f.id}/edit`,
57
+ })),
58
+ };
59
+ }
60
+ async function info(client, id) {
61
+ const ss = await getSpreadsheet(client, id);
62
+ return {
63
+ spreadsheetId: ss.spreadsheetId,
64
+ title: ss.properties?.title,
65
+ url: ss.spreadsheetUrl,
66
+ tabs: (ss.sheets ?? []).map((s) => ({
67
+ sheetId: s.properties?.sheetId,
68
+ title: s.properties?.title,
69
+ index: s.properties?.index,
70
+ rows: s.properties?.gridProperties?.rowCount,
71
+ columns: s.properties?.gridProperties?.columnCount,
72
+ })),
73
+ };
74
+ }
75
+ async function listTabs(client, id) {
76
+ const data = await info(client, id);
77
+ return { title: data.title, tabs: data.tabs };
78
+ }
79
+ function sheetByName(ss, name) {
80
+ const found = ss.sheets?.find((s) => s.properties?.title === name);
81
+ if (!found?.properties) {
82
+ const titles = (ss.sheets ?? []).map((s) => s.properties?.title).filter(Boolean);
83
+ throw new CliError('NOT_FOUND', `Tab not found: ${name}`, { tabs: titles });
84
+ }
85
+ return found.properties;
86
+ }
87
+ async function readValues(client, id, range) {
88
+ const data = await client.sheets('GET', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}`, { query: { valueRenderOption: 'UNFORMATTED_VALUE', dateTimeRenderOption: 'FORMATTED_STRING' } });
89
+ return data.values ?? [];
90
+ }
91
+ async function readTable(client, id, opts) {
92
+ const tab = opts.tab || (opts.range ? parseA1(opts.range).sheet : undefined);
93
+ if (!tab && !opts.range)
94
+ throw new CliError('VALIDATION_ERROR', 'Pass --tab or --range');
95
+ const range = opts.range ? qualifyRange(opts.range, opts.tab) : `'${(tab ?? '').replace(/'/g, "''")}'`;
96
+ const values = await readValues(client, id, range);
97
+ if (opts.raw)
98
+ return { range, values };
99
+ return { range, ...valuesToTable(values, { headerRow: opts.headerRow, limit: opts.limit }) };
100
+ }
101
+ async function readRange(client, id, range) {
102
+ const values = await readValues(client, id, range);
103
+ return { range, values };
104
+ }
105
+ async function writeValues(client, id, range, values, valueInput = 'USER_ENTERED') {
106
+ return client.sheets('PUT', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}`, {
107
+ query: { valueInputOption: valueInput },
108
+ body: { range, values },
109
+ });
110
+ }
111
+ async function appendValues(client, id, range, values, valueInput = 'USER_ENTERED') {
112
+ return client.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}:append`, {
113
+ query: { valueInputOption: valueInput, insertDataOption: 'INSERT_ROWS' },
114
+ body: { range, values },
115
+ });
116
+ }
117
+ async function appendRows(client, id, opts) {
118
+ const range = `'${opts.tab.replace(/'/g, "''")}'`;
119
+ const existing = await readValues(client, id, range);
120
+ const table = valuesToTable(existing);
121
+ let headers = table.headers;
122
+ if (!headers.length) {
123
+ headers = Object.keys(opts.rows[0] ?? {}).filter((k) => k !== '_row');
124
+ if (!headers.length)
125
+ throw new CliError('VALIDATION_ERROR', 'No columns to append');
126
+ const toWrite = [headers, ...opts.rows.map((row) => objectToRow(row, headers))];
127
+ if (opts.dryRun)
128
+ return { dryRun: true, tab: opts.tab, headers, rows: opts.rows.length, bootstrap: true };
129
+ const written = await writeValues(client, id, `${range}!A1`, toWrite, opts.valueInput);
130
+ return { tab: opts.tab, headers, rows: opts.rows.length, bootstrap: true, result: written };
131
+ }
132
+ const dataRows = opts.rows.map((row) => objectToRow(row, headers));
133
+ if (opts.dryRun)
134
+ return { dryRun: true, tab: opts.tab, headers, rows: dataRows.length };
135
+ const appended = await appendValues(client, id, range, dataRows, opts.valueInput);
136
+ return { tab: opts.tab, headers, rows: dataRows.length, result: appended };
137
+ }
138
+ async function updateByKey(client, id, opts) {
139
+ const range = `'${opts.tab.replace(/'/g, "''")}'`;
140
+ const values = await readValues(client, id, range);
141
+ const table = valuesToTable(values);
142
+ const keyHeader = findHeader(table.headers, opts.keyCol);
143
+ if (!keyHeader)
144
+ throw new CliError('NOT_FOUND', `Key column not found: ${opts.keyCol}`, { headers: table.headers });
145
+ const matches = table.rows.filter((r) => String(r[keyHeader] ?? '') === opts.key);
146
+ if (!matches.length)
147
+ throw new CliError('NOT_FOUND', `No row with ${keyHeader}=${opts.key}`);
148
+ if (matches.length > 1 && !opts.allowMulti) {
149
+ throw new CliError('VALIDATION_ERROR', `Multiple rows match ${keyHeader}=${opts.key}. Pass --allow-multi.`);
150
+ }
151
+ const unknown = unknownKeys(opts.set, table.headers);
152
+ if (unknown.length)
153
+ throw new CliError('VALIDATION_ERROR', `Unknown columns: ${unknown.join(', ')}`, { headers: table.headers });
154
+ if (opts.dryRun) {
155
+ return { dryRun: true, matchedRows: matches.map((r) => r._row), set: opts.set };
156
+ }
157
+ const data = matches.map((row) => {
158
+ const merged = { ...row, ...opts.set };
159
+ return {
160
+ range: `'${opts.tab.replace(/'/g, "''")}'!A${row._row}`,
161
+ values: [objectToRow(merged, table.headers)],
162
+ };
163
+ });
164
+ const result = await client.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}/values:batchUpdate`, {
165
+ body: { valueInputOption: opts.valueInput || 'USER_ENTERED', data },
166
+ });
167
+ return { matchedRows: matches.map((r) => r._row), updated: matches.length, result };
168
+ }
169
+ async function updateRow(client, id, tab, row, set, dryRun, valueInput) {
170
+ const range = `'${tab.replace(/'/g, "''")}'`;
171
+ const values = await readValues(client, id, range);
172
+ const table = valuesToTable(values);
173
+ const unknown = unknownKeys(set, table.headers);
174
+ if (unknown.length)
175
+ throw new CliError('VALIDATION_ERROR', `Unknown columns: ${unknown.join(', ')}`, { headers: table.headers });
176
+ const existing = table.rows.find((r) => r._row === row) ?? {};
177
+ const merged = { ...existing, ...set };
178
+ if (dryRun)
179
+ return { dryRun: true, row, set };
180
+ const a1 = `${range}!A${row}`;
181
+ const result = await writeValues(client, id, a1, [objectToRow(merged, table.headers)], valueInput);
182
+ return { row, result };
183
+ }
184
+ async function upsertRows(client, id, opts) {
185
+ const updated = [];
186
+ const inserted = [];
187
+ for (const row of opts.rows) {
188
+ const keyVal = row[opts.keyCol] ?? row[Object.keys(row).find((k) => k.toLowerCase() === opts.keyCol.toLowerCase()) ?? ''];
189
+ if (keyVal === undefined || keyVal === '') {
190
+ throw new CliError('VALIDATION_ERROR', `Upsert row missing key column ${opts.keyCol}`);
191
+ }
192
+ try {
193
+ const result = await updateByKey(client, id, {
194
+ tab: opts.tab,
195
+ keyCol: opts.keyCol,
196
+ key: String(keyVal),
197
+ set: row,
198
+ dryRun: opts.dryRun,
199
+ valueInput: opts.valueInput,
200
+ });
201
+ updated.push({ key: keyVal, result });
202
+ }
203
+ catch (err) {
204
+ if (err instanceof CliError && err.code === 'NOT_FOUND')
205
+ inserted.push(row);
206
+ else
207
+ throw err;
208
+ }
209
+ }
210
+ let appendResult;
211
+ if (inserted.length) {
212
+ appendResult = await appendRows(client, id, { tab: opts.tab, rows: inserted, dryRun: opts.dryRun, valueInput: opts.valueInput });
213
+ }
214
+ return { updated: updated.length, inserted: inserted.length, updatedRows: updated, appendResult };
215
+ }
216
+ async function setRange(client, id, opts) {
217
+ if (opts.dryRun)
218
+ return { dryRun: true, range: opts.range, values: opts.values };
219
+ const result = await writeValues(client, id, opts.range, opts.values, opts.valueInput);
220
+ return { range: opts.range, result };
221
+ }
222
+ async function clearRange(client, id, range, dryRun) {
223
+ if (dryRun)
224
+ return { dryRun: true, range };
225
+ const result = await client.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}:clear`, {
226
+ body: {},
227
+ });
228
+ return { range, result };
229
+ }
230
+ async function batchOps(client, id, ops, dryRun) {
231
+ const results = [];
232
+ for (const op of ops) {
233
+ if (op.op === 'append') {
234
+ const rows = Array.isArray(op.values) ? op.values : [op.values];
235
+ results.push({ op: op.op, result: await appendRows(client, id, { tab: op.sheet, rows, dryRun }) });
236
+ }
237
+ else if (op.op === 'updateKey') {
238
+ results.push({
239
+ op: op.op,
240
+ result: await updateByKey(client, id, {
241
+ tab: op.sheet,
242
+ keyCol: op.keyCol,
243
+ key: op.key,
244
+ set: op.set,
245
+ allowMulti: op.allowMulti,
246
+ dryRun,
247
+ }),
248
+ });
249
+ }
250
+ else if (op.op === 'updateRow') {
251
+ results.push({ op: op.op, result: await updateRow(client, id, op.sheet, op.row, op.set, dryRun) });
252
+ }
253
+ else if (op.op === 'setRange') {
254
+ results.push({ op: op.op, result: await setRange(client, id, { range: op.range, values: op.values, dryRun }) });
255
+ }
256
+ else if (op.op === 'clear') {
257
+ results.push({ op: op.op, result: await clearRange(client, id, op.range, dryRun) });
258
+ }
259
+ else {
260
+ throw new CliError('VALIDATION_ERROR', `Unknown batch op: ${op.op}`);
261
+ }
262
+ }
263
+ return { count: results.length, results };
264
+ }
265
+ async function createSpreadsheet(client, opts) {
266
+ const tabs = opts.tabs?.length ? opts.tabs : ['Sheet1'];
267
+ const body = {
268
+ properties: { title: opts.title },
269
+ sheets: tabs.map((title, index) => ({ properties: { title, index } })),
270
+ };
271
+ if (opts.dryRun)
272
+ return { dryRun: true, ...body };
273
+ const created = await client.sheets('POST', '/spreadsheets', { body });
274
+ return {
275
+ spreadsheetId: created.spreadsheetId,
276
+ url: created.spreadsheetUrl || `https://docs.google.com/spreadsheets/d/${created.spreadsheetId}/edit`,
277
+ title: created.properties?.title,
278
+ tabs,
279
+ };
280
+ }
281
+ async function batchUpdate(client, id, requests, dryRun) {
282
+ if (dryRun)
283
+ return { dryRun: true, requests };
284
+ return client.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}:batchUpdate`, { body: { requests } });
285
+ }
286
+ async function tabAdd(client, id, opts) {
287
+ const requests = [{ addSheet: { properties: { title: opts.title, index: opts.index } } }];
288
+ const result = await batchUpdate(client, id, requests, opts.dryRun);
289
+ return { title: opts.title, result };
290
+ }
291
+ async function tabRename(client, id, opts) {
292
+ const ss = await getSpreadsheet(client, id);
293
+ const sheet = sheetByName(ss, opts.tab);
294
+ const requests = [{ updateSheetProperties: { properties: { sheetId: sheet.sheetId, title: opts.title }, fields: 'title' } }];
295
+ const result = await batchUpdate(client, id, requests, opts.dryRun);
296
+ return { from: opts.tab, title: opts.title, result };
297
+ }
298
+ async function tabDelete(client, id, opts) {
299
+ const ss = await getSpreadsheet(client, id);
300
+ const sheet = sheetByName(ss, opts.tab);
301
+ const result = await batchUpdate(client, id, [{ deleteSheet: { sheetId: sheet.sheetId } }], opts.dryRun);
302
+ return { tab: opts.tab, sheetId: sheet.sheetId, result };
303
+ }
304
+ async function tabDuplicate(client, id, opts) {
305
+ const ss = await getSpreadsheet(client, id);
306
+ const sheet = sheetByName(ss, opts.tab);
307
+ const result = await batchUpdate(client, id, [{ duplicateSheet: { sourceSheetId: sheet.sheetId, newSheetName: opts.title } }], opts.dryRun);
308
+ return { tab: opts.tab, title: opts.title, result };
309
+ }
310
+ async function formatRange(client, id, opts) {
311
+ const range = qualifyRange(opts.range, opts.tab);
312
+ const parsed = parseA1(range);
313
+ const ss = await getSpreadsheet(client, id);
314
+ const tabName = parsed.sheet || opts.tab;
315
+ if (!tabName)
316
+ throw new CliError('VALIDATION_ERROR', 'Range must include a sheet name or pass --tab');
317
+ const sheet = sheetByName(ss, tabName);
318
+ const request = formatToRepeatCell(toGridRange(parsed, sheet.sheetId), opts);
319
+ if (opts.dryRun)
320
+ return { dryRun: true, range, requests: [request] };
321
+ const result = await batchUpdate(client, id, [request], false);
322
+ return { range, result };
323
+ }
324
+ async function formatHeader(client, id, opts) {
325
+ const ss = await getSpreadsheet(client, id);
326
+ const sheet = sheetByName(ss, opts.tab);
327
+ const flags = {
328
+ bold: opts.bold ?? true,
329
+ bg: opts.bg ?? '#1a365d',
330
+ fg: opts.fg ?? 'white',
331
+ align: opts.align ?? 'center',
332
+ italic: opts.italic,
333
+ };
334
+ const requests = [
335
+ formatToRepeatCell({ sheetId: sheet.sheetId, startRowIndex: 0, endRowIndex: 1 }, flags),
336
+ ];
337
+ if (opts.freeze !== false)
338
+ requests.push(freezeRequest(sheet.sheetId, 1));
339
+ const result = await batchUpdate(client, id, requests, opts.dryRun);
340
+ return { tab: opts.tab, freeze: opts.freeze !== false, result };
341
+ }
342
+ async function freeze(client, id, opts) {
343
+ const ss = await getSpreadsheet(client, id);
344
+ const sheet = sheetByName(ss, opts.tab);
345
+ const result = await batchUpdate(client, id, [freezeRequest(sheet.sheetId, opts.rows ?? 1, opts.columns)], opts.dryRun);
346
+ return { tab: opts.tab, rows: opts.rows ?? 1, columns: opts.columns, result };
347
+ }
348
+ async function numberFormat(client, id, opts) {
349
+ return formatRange(client, id, { range: opts.range, tab: opts.tab, pattern: opts.pattern, dryRun: opts.dryRun });
350
+ }
351
+ async function writeFormula(client, id, opts) {
352
+ const expr = opts.expr.startsWith('=') ? opts.expr : `=${opts.expr}`;
353
+ if (opts.dryRun)
354
+ return { dryRun: true, cell: opts.cell, expr };
355
+ const result = await writeValues(client, id, opts.cell, [[expr]]);
356
+ return { cell: opts.cell, expr, result };
357
+ }
358
+ async function fillDown(client, id, opts) {
359
+ const expr = opts.expr.startsWith('=') ? opts.expr : `=${opts.expr}`;
360
+ const parsed = parseA1(opts.range);
361
+ const startRow = parsed.startRow;
362
+ const endRow = parsed.endRow ?? startRow + 1;
363
+ const col = parsed.startCol;
364
+ const sheet = parsed.sheet;
365
+ if (!sheet)
366
+ throw new CliError('VALIDATION_ERROR', 'Range must include a sheet name');
367
+ const values = [];
368
+ for (let r = startRow; r < endRow; r++) {
369
+ values.push([expr]);
370
+ }
371
+ const a1 = qualifyRange(opts.range, sheet);
372
+ if (opts.dryRun)
373
+ return { dryRun: true, range: a1, expr, rows: values.length };
374
+ void col;
375
+ const result = await writeValues(client, id, a1, values);
376
+ return { range: a1, expr, rows: values.length, result };
377
+ }
378
+ async function share(client, id, opts) {
379
+ const role = opts.role === 'reader' || opts.role === 'writer' ? opts.role : null;
380
+ if (!role)
381
+ throw new CliError('VALIDATION_ERROR', 'Role must be reader or writer');
382
+ if (opts.dryRun)
383
+ return { dryRun: true, email: opts.email, role };
384
+ const result = await client.drive('POST', `/files/${encodeURIComponent(id)}/permissions`, {
385
+ query: { sendNotificationEmail: 'false' },
386
+ body: { type: 'user', role, emailAddress: opts.email },
387
+ });
388
+ return { email: opts.email, role, result };
389
+ }
390
+ async function unshare(client, id, email, dryRun) {
391
+ const list = await listPermissions(client, id);
392
+ const perm = list.permissions.find((p) => p.email?.toLowerCase() === email.toLowerCase());
393
+ if (!perm)
394
+ throw new CliError('NOT_FOUND', `No permission for ${email}`);
395
+ if (dryRun)
396
+ return { dryRun: true, email, permissionId: perm.id };
397
+ await client.drive('DELETE', `/files/${encodeURIComponent(id)}/permissions/${encodeURIComponent(perm.id)}`);
398
+ return { email, removed: perm.id };
399
+ }
400
+ async function listPermissions(client, id) {
401
+ const data = await client.drive('GET', `/files/${encodeURIComponent(id)}/permissions`, {
402
+ query: { fields: 'permissions(id,role,type,emailAddress,displayName)', supportsAllDrives: 'true' },
403
+ });
404
+ return {
405
+ permissions: (data.permissions ?? []).map((p) => ({
406
+ id: p.id,
407
+ role: p.role,
408
+ type: p.type,
409
+ email: p.emailAddress,
410
+ name: p.displayName,
411
+ })),
412
+ };
413
+ }
414
+ const API_METHODS = {
415
+ batchUpdate: (c, id, json) => {
416
+ if (!id)
417
+ throw new CliError('VALIDATION_ERROR', 'batchUpdate requires a spreadsheet id');
418
+ return c.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}:batchUpdate`, { body: json });
419
+ },
420
+ 'spreadsheets.get': (c, id) => {
421
+ if (!id)
422
+ throw new CliError('VALIDATION_ERROR', 'spreadsheets.get requires a spreadsheet id');
423
+ return getSpreadsheet(c, id);
424
+ },
425
+ 'spreadsheets.create': (c, _id, json) => c.sheets('POST', '/spreadsheets', { body: json }),
426
+ 'values.get': (c, id, json) => {
427
+ if (!id)
428
+ throw new CliError('VALIDATION_ERROR', 'values.get requires a spreadsheet id');
429
+ const range = json?.range;
430
+ if (!range)
431
+ throw new CliError('VALIDATION_ERROR', 'values.get JSON must include range');
432
+ return c.sheets('GET', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}`);
433
+ },
434
+ 'values.update': (c, id, json) => {
435
+ if (!id)
436
+ throw new CliError('VALIDATION_ERROR', 'values.update requires a spreadsheet id');
437
+ const body = json;
438
+ return c.sheets('PUT', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(body.range)}`, {
439
+ query: { valueInputOption: body.valueInputOption || 'USER_ENTERED' },
440
+ body,
441
+ });
442
+ },
443
+ 'values.append': (c, id, json) => {
444
+ if (!id)
445
+ throw new CliError('VALIDATION_ERROR', 'values.append requires a spreadsheet id');
446
+ const body = json;
447
+ return c.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(body.range)}:append`, {
448
+ query: { ...VALUE_INPUT, insertDataOption: 'INSERT_ROWS' },
449
+ body,
450
+ });
451
+ },
452
+ 'values.clear': (c, id, json) => {
453
+ if (!id)
454
+ throw new CliError('VALIDATION_ERROR', 'values.clear requires a spreadsheet id');
455
+ const range = json?.range;
456
+ if (!range)
457
+ throw new CliError('VALIDATION_ERROR', 'values.clear JSON must include range');
458
+ return c.sheets('POST', `/spreadsheets/${encodeURIComponent(id)}/values/${encodeURIComponent(range)}:clear`, { body: {} });
459
+ },
460
+ 'drive.files.list': (c, _id, json) => c.drive('GET', '/files', { query: json ?? {} }),
461
+ 'drive.permissions.create': (c, id, json) => {
462
+ if (!id)
463
+ throw new CliError('VALIDATION_ERROR', 'drive.permissions.create requires a spreadsheet id');
464
+ return c.drive('POST', `/files/${encodeURIComponent(id)}/permissions`, { body: json });
465
+ },
466
+ };
467
+ async function apiEscape(client, method, id, json) {
468
+ const fn = API_METHODS[method];
469
+ if (!fn) {
470
+ throw new CliError('VALIDATION_ERROR', `Unknown api method: ${method}`, { methods: Object.keys(API_METHODS) });
471
+ }
472
+ const result = await fn(client, id, json ?? {});
473
+ return { method, result };
474
+ }
475
+ export { VALUE_INPUT };
@@ -0,0 +1,14 @@
1
+ export declare function normalizeHeader(value: string): string;
2
+ export declare function detectHeaderRow(values: unknown[][]): number;
3
+ export type Table = {
4
+ headers: string[];
5
+ rows: Record<string, unknown>[];
6
+ headerRow: number;
7
+ };
8
+ export declare function valuesToTable(values: unknown[][], opts?: {
9
+ headerRow?: number;
10
+ limit?: number;
11
+ }): Table;
12
+ export declare function findHeader(headers: string[], key: string): string | undefined;
13
+ export declare function objectToRow(obj: Record<string, unknown>, headers: string[]): unknown[];
14
+ export declare function unknownKeys(obj: Record<string, unknown>, headers: string[]): string[];
@@ -0,0 +1,67 @@
1
+ import { colLetter } from '../ranges.js';
2
+ export function normalizeHeader(value) {
3
+ return value.trim().replace(/\s+/g, ' ').toLowerCase();
4
+ }
5
+ export function detectHeaderRow(values) {
6
+ for (let i = 0; i < values.length; i++) {
7
+ if ((values[i] ?? []).some((c) => String(c ?? '').trim() !== ''))
8
+ return i;
9
+ }
10
+ return 0;
11
+ }
12
+ function uniqueHeaders(raw) {
13
+ const seen = new Map();
14
+ return raw.map((cell, i) => {
15
+ const base = String(cell ?? '').trim() || colLetter(i);
16
+ const n = (seen.get(normalizeHeader(base)) ?? 0) + 1;
17
+ seen.set(normalizeHeader(base), n);
18
+ return n === 1 ? base : `${base}_${n}`;
19
+ });
20
+ }
21
+ export function valuesToTable(values, opts = {}) {
22
+ if (!values.length) {
23
+ return { headers: [], rows: [], headerRow: 1 };
24
+ }
25
+ const headerIdx = opts.headerRow ? opts.headerRow - 1 : detectHeaderRow(values);
26
+ const headers = uniqueHeaders(values[headerIdx] ?? []);
27
+ const rows = [];
28
+ const start = headerIdx + 1;
29
+ const end = opts.limit != null ? Math.min(values.length, start + opts.limit) : values.length;
30
+ for (let i = start; i < end; i++) {
31
+ const line = values[i] ?? [];
32
+ if (!line.some((c) => String(c ?? '').trim() !== ''))
33
+ continue;
34
+ const row = { _row: i + 1 };
35
+ headers.forEach((h, col) => {
36
+ row[h] = line[col] ?? '';
37
+ });
38
+ rows.push(row);
39
+ }
40
+ return { headers, rows, headerRow: headerIdx + 1 };
41
+ }
42
+ export function findHeader(headers, key) {
43
+ const needle = normalizeHeader(key);
44
+ const match = headers.find((h) => normalizeHeader(h) === needle);
45
+ if (match)
46
+ return match;
47
+ if (/^[A-Za-z]+$/.test(key.trim())) {
48
+ const letter = key.trim().toUpperCase();
49
+ return headers.find((h) => h.toUpperCase() === letter);
50
+ }
51
+ return undefined;
52
+ }
53
+ export function objectToRow(obj, headers) {
54
+ const mapped = new Array(headers.length).fill('');
55
+ for (const [key, value] of Object.entries(obj)) {
56
+ if (key === '_row')
57
+ continue;
58
+ const header = findHeader(headers, key);
59
+ if (!header)
60
+ continue;
61
+ mapped[headers.indexOf(header)] = value ?? '';
62
+ }
63
+ return mapped;
64
+ }
65
+ export function unknownKeys(obj, headers) {
66
+ return Object.keys(obj).filter((k) => k !== '_row' && !findHeader(headers, k));
67
+ }
@@ -0,0 +1,26 @@
1
+ export type SheetProperties = {
2
+ sheetId: number;
3
+ title: string;
4
+ index?: number;
5
+ gridProperties?: {
6
+ rowCount?: number;
7
+ columnCount?: number;
8
+ frozenRowCount?: number;
9
+ frozenColumnCount?: number;
10
+ };
11
+ };
12
+ export type Spreadsheet = {
13
+ spreadsheetId: string;
14
+ spreadsheetUrl?: string;
15
+ properties?: {
16
+ title?: string;
17
+ locale?: string;
18
+ };
19
+ sheets?: {
20
+ properties?: SheetProperties;
21
+ }[];
22
+ };
23
+ export type ValueRange = {
24
+ range?: string;
25
+ values?: unknown[][];
26
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ export declare const EXIT: {
2
+ readonly OK: 0;
3
+ readonly VALIDATION: 10;
4
+ readonly AUTH: 20;
5
+ readonly PERMISSION: 30;
6
+ readonly API: 40;
7
+ };
8
+ export type ErrorBody = {
9
+ code: string;
10
+ message: string;
11
+ details?: unknown;
12
+ };
13
+ export type Envelope = {
14
+ ok: true;
15
+ cmd: string;
16
+ spreadsheetId?: string;
17
+ sheet?: string;
18
+ result: unknown;
19
+ } | {
20
+ ok: false;
21
+ cmd: string;
22
+ error: ErrorBody;
23
+ };
24
+ export declare class CliError extends Error {
25
+ readonly code: string;
26
+ readonly details?: unknown;
27
+ readonly exitCode: number;
28
+ constructor(code: string, message: string, details?: unknown, exitCode?: number);
29
+ }
30
+ export declare function compact(value: unknown): unknown;
31
+ export declare function envelopeOk(cmd: string, result: unknown, extra?: {
32
+ spreadsheetId?: string;
33
+ sheet?: string;
34
+ }): Envelope;
35
+ export declare function envelopeErr(cmd: string, err: unknown): Envelope;
36
+ export declare function exitCodeFor(err: unknown): number;
37
+ export declare function stringifyEnvelope(body: Envelope, pretty: boolean): string;