@gpdoc/cli 1.4.0 → 1.6.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/bin/gpdoc.js +88 -14
- package/lib/remote.js +37 -5
- package/package.json +1 -1
package/bin/gpdoc.js
CHANGED
|
@@ -159,7 +159,7 @@ function parseArguments(argv) {
|
|
|
159
159
|
options[key] = true;
|
|
160
160
|
continue;
|
|
161
161
|
}
|
|
162
|
-
if (!['title', 'to', 'output', 'drive-id', 'item-id', 'parent-id', 'revision', 'filetype', 'filename', 'role', 'scope', 'description', 'branch', 'path', 'message', 'editor', 'cursor', 'query'].includes(key) || !rest[index + 1] || rest[index + 1].startsWith('--')) {
|
|
162
|
+
if (!['title', 'to', 'output', 'drive-id', 'item-id', 'parent-id', 'revision', 'filetype', 'filename', 'role', 'scope', 'description', 'branch', 'path', 'message', 'editor', 'cursor', 'query', 'open'].includes(key) || !rest[index + 1] || rest[index + 1].startsWith('--')) {
|
|
163
163
|
throw new CliError('USAGE', `Unknown or incomplete option: ${value}.`);
|
|
164
164
|
}
|
|
165
165
|
options[key] = rest[index + 1];
|
|
@@ -318,18 +318,91 @@ function renderRemote(result, json) {
|
|
|
318
318
|
return printResult(result, false);
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
function
|
|
322
|
-
|
|
321
|
+
function truncate(value, width) {
|
|
322
|
+
const text = String(value || '');
|
|
323
|
+
if (text.length <= width) return text;
|
|
324
|
+
if (width < 2) return text.slice(0, width);
|
|
325
|
+
const prefixLength = Math.ceil((width - 1) * 0.7);
|
|
326
|
+
const suffixLength = Math.floor((width - 1) * 0.3);
|
|
327
|
+
return `${text.slice(0, prefixLength)}…${suffixLength ? text.slice(-suffixLength) : ''}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatBytes(value) {
|
|
331
|
+
const bytes = Number(value || 0);
|
|
332
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '—';
|
|
333
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
334
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KiB`;
|
|
335
|
+
return `${(bytes / (1024 * 1024)).toFixed(bytes >= 10 * 1024 * 1024 ? 0 : 1)} MiB`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatDate(value) {
|
|
339
|
+
const date = new Date(value || '');
|
|
340
|
+
return Number.isNaN(date.getTime()) ? '—' : date.toISOString().slice(0, 10);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function displayProvider(provider) {
|
|
344
|
+
return provider === 'google' ? 'Google Drive' : provider === 'microsoft' ? 'Microsoft 365' : 'GPDoc';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function listedItemId(item) {
|
|
348
|
+
return item?.providerData?.itemId || item?.itemId || item?.id || '';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// @spec CLI-038
|
|
352
|
+
export function selectListedItem(items, row) {
|
|
353
|
+
const index = Number(row);
|
|
354
|
+
if (!Number.isSafeInteger(index) || index < 1 || index > items.length) {
|
|
355
|
+
throw new CliError('USAGE', 'Choose a row number from the displayed list.');
|
|
356
|
+
}
|
|
357
|
+
return items[index - 1];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// @spec CLI-038
|
|
361
|
+
export function formatList(result) {
|
|
323
362
|
if (Array.isArray(result.items)) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
363
|
+
if (!result.items.length) return 'No files found.';
|
|
364
|
+
const columns = [
|
|
365
|
+
['#', 3], ['Type', 8], ['Name', 34], ['Modified', 10], ['Size', 9], ['ID', 18],
|
|
366
|
+
];
|
|
367
|
+
const header = columns.map(([label, width]) => label.padEnd(width)).join(' ').trimEnd();
|
|
368
|
+
const divider = columns.map(([, width]) => '─'.repeat(width)).join(' ');
|
|
369
|
+
const rows = result.items.map((item, index) => {
|
|
370
|
+
const values = [
|
|
371
|
+
String(index + 1),
|
|
372
|
+
item?.kind === 'folder' ? 'Folder' : 'File',
|
|
373
|
+
item?.name || 'Untitled',
|
|
374
|
+
formatDate(item?.modifiedAt),
|
|
375
|
+
item?.kind === 'folder' ? '—' : formatBytes(item?.size),
|
|
376
|
+
listedItemId(item),
|
|
377
|
+
];
|
|
378
|
+
return values.map((value, columnIndex) => truncate(value, columns[columnIndex][1]).padEnd(columns[columnIndex][1])).join(' ').trimEnd();
|
|
327
379
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
380
|
+
const provider = result.provider || 'provider';
|
|
381
|
+
return `${displayProvider(provider)}\n${header}\n${divider}\n${rows.join('\n')}\n\nOpen an item: gpdoc ${provider} list --open ROW\nUse --json for full IDs and URLs.`;
|
|
382
|
+
}
|
|
383
|
+
const entries = [
|
|
384
|
+
...(result.owned || []).map((file) => ({ access: 'Owned', name: file?.filename || 'Untitled', id: file?.shareId || '' })),
|
|
385
|
+
...(result.sharedWithMe || []).map((file) => ({ access: 'Shared', name: file?.filename || 'Untitled', id: file?.shareId || '' })),
|
|
386
|
+
];
|
|
387
|
+
if (!entries.length) return 'No shared files found.';
|
|
388
|
+
const header = 'Access Name Share ID';
|
|
389
|
+
const rows = entries.map((entry) => `${entry.access.padEnd(6)} ${truncate(entry.name, 34).padEnd(34)} ${truncate(entry.id, 18)}`);
|
|
390
|
+
return `GPDoc shared files\n${header}\n${'─'.repeat(header.length)}\n${rows.join('\n')}\n\nUse --json for full IDs.`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renderList(result, json) {
|
|
394
|
+
if (json) return printResult(result, true);
|
|
395
|
+
return printResult(formatList(result), false);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function renderProviderList(result, { open, json, runtime }) {
|
|
399
|
+
if (!open) return renderList(result, json);
|
|
400
|
+
if (json) throw new CliError('USAGE', 'Use either --open ROW or --json, not both.');
|
|
401
|
+
const item = selectListedItem(result.items || [], open);
|
|
402
|
+
if (!item?.webUrl) throw new CliError('PROVIDER_URL_UNAVAILABLE', 'The selected provider item has no browser URL. Use --json to inspect its identifiers.');
|
|
403
|
+
renderList(result, false);
|
|
404
|
+
const browserOpened = await (runtime.openBrowser || openBrowser)(item.webUrl);
|
|
405
|
+
process.stdout.write(browserOpened ? `Opened ${item.name || 'provider item'} in your browser.\n` : `Could not open a browser. Open this URL manually: ${item.webUrl}\n`);
|
|
333
406
|
}
|
|
334
407
|
|
|
335
408
|
function createProgressReporter(stderr = process.stderr) {
|
|
@@ -372,7 +445,7 @@ async function waitForProviderConnection(remote, providerName, sleep = delay) {
|
|
|
372
445
|
throw new CliError('PROVIDER_CONNECTION_TIMEOUT', `Timed out waiting for ${providerName} connection. Run gpdoc ${providerName} status to check it later.`);
|
|
373
446
|
}
|
|
374
447
|
|
|
375
|
-
// @spec CLI-001, CLI-002, CLI-003, CLI-004, CLI-005, CLI-006, CLI-007, CLI-008, CLI-009, CLI-014, CLI-015, CLI-016, CLI-017, CLI-018, CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025, CLI-028, CLI-030, CLI-032, CLI-033, CLI-034, CLI-035, CLI-036
|
|
448
|
+
// @spec CLI-001, CLI-002, CLI-003, CLI-004, CLI-005, CLI-006, CLI-007, CLI-008, CLI-009, CLI-014, CLI-015, CLI-016, CLI-017, CLI-018, CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025, CLI-028, CLI-030, CLI-032, CLI-033, CLI-034, CLI-035, CLI-036, CLI-038
|
|
376
449
|
export async function run(argv, runtime = {}) {
|
|
377
450
|
if (argv[0] === 'knowledge') {
|
|
378
451
|
await runKnowledge(argv.slice(1), runtime);
|
|
@@ -381,6 +454,7 @@ export async function run(argv, runtime = {}) {
|
|
|
381
454
|
const parsed = parseArguments(argv);
|
|
382
455
|
if (parsed.command === 'help') {
|
|
383
456
|
process.stdout.write(usage());
|
|
457
|
+
process.stdout.write('\nCloud file lists: use gpdoc google list --open ROW or gpdoc microsoft list --open ROW to open a numbered item in your browser.\n');
|
|
384
458
|
return;
|
|
385
459
|
}
|
|
386
460
|
if (parsed.command === 'version') {
|
|
@@ -434,7 +508,7 @@ export async function run(argv, runtime = {}) {
|
|
|
434
508
|
const connected = await waitForProviderConnection(remote, 'google', runtime.sleep || delay);
|
|
435
509
|
return renderRemote({ ...started, browserOpened, ...connected }, parsed.options.json);
|
|
436
510
|
}
|
|
437
|
-
if (action === 'list') return
|
|
511
|
+
if (action === 'list') return renderProviderList(await remote.googleList({ driveId: parsed.options['drive-id'], parentId: parsed.options['parent-id'], cursor: parsed.options.cursor, shared: parsed.options.shared === true }), { open: parsed.options.open, json: parsed.options.json, runtime });
|
|
438
512
|
const filePath = await requireLocalInput(input);
|
|
439
513
|
const progress = parsed.options.json ? undefined : (runtime.progress || createProgressReporter(runtime.stderr));
|
|
440
514
|
if (action === 'upload') return renderRemote(await remote.googleSave({ filePath, title: parsed.options.title, filetype: parsed.options.filetype || 'document', onProgress: progress }), parsed.options.json);
|
|
@@ -452,7 +526,7 @@ export async function run(argv, runtime = {}) {
|
|
|
452
526
|
const connected = await waitForProviderConnection(remote, 'microsoft', runtime.sleep || delay);
|
|
453
527
|
return renderRemote({ ...started, browserOpened, ...connected }, parsed.options.json);
|
|
454
528
|
}
|
|
455
|
-
if (action === 'list') return
|
|
529
|
+
if (action === 'list') return renderProviderList(await remote.microsoftList({ driveId: parsed.options['drive-id'], itemId: parsed.options['item-id'], cursor: parsed.options.cursor, query: parsed.options.query, shared: parsed.options.shared === true }), { open: parsed.options.open, json: parsed.options.json, runtime });
|
|
456
530
|
if (action === 'share') return renderRemote(await remote.microsoftShare({ driveId: requireOption(parsed.options, 'drive-id'), itemId: requireOption(parsed.options, 'item-id'), role: requireOption(parsed.options, 'role'), scope: requireOption(parsed.options, 'scope') }), parsed.options.json);
|
|
457
531
|
const filePath = await requireLocalInput(input);
|
|
458
532
|
const progress = parsed.options.json ? undefined : (runtime.progress || createProgressReporter(runtime.stderr));
|
package/lib/remote.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
const {
|
|
4
|
+
convertDocument,
|
|
5
|
+
detectFormat,
|
|
6
|
+
readDocument,
|
|
7
|
+
} = await import('@gpdoc/filekit').catch(() => import('../../gpdoc-filekit/src/index.js'));
|
|
3
8
|
|
|
4
9
|
export class RemoteError extends Error {
|
|
5
10
|
constructor(code, message) {
|
|
@@ -49,6 +54,19 @@ function titleFrom(filePath, fallback = 'GPDoc Export') {
|
|
|
49
54
|
return path.basename(filePath).replace(/\.gpdoc\.md$/i, '').replace(/\.[^.]+$/, '') || fallback;
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
async function readExportDocument(filePath) {
|
|
58
|
+
const format = detectFormat(filePath);
|
|
59
|
+
if (['docx', 'pptx'].includes(format)) return null;
|
|
60
|
+
return readDocument(await readFile(filePath, 'utf8'), filePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function microsoftFilename(filename, title) {
|
|
64
|
+
const preferred = String(filename || `${title || 'GPDoc Export'}.docx`).trim() || 'GPDoc Export.docx';
|
|
65
|
+
return /\.docx$/i.test(preferred)
|
|
66
|
+
? preferred
|
|
67
|
+
: `${preferred.replace(/\.[^.]+$/, '')}.docx`;
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
async function githubToken({ auth, env, fetchImpl }) {
|
|
53
71
|
const token = await auth.getAccessToken();
|
|
54
72
|
const payload = await jsonRequest(fetchImpl, `${apiBase(env)}/.netlify/functions/get-user-profile`, { headers: bearer(token) }, 'GITHUB_IDENTITY_REQUIRED');
|
|
@@ -70,7 +88,7 @@ function githubHeaders(token) {
|
|
|
70
88
|
return { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28' };
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
// @spec CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025
|
|
91
|
+
// @spec CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025, CLI-037
|
|
74
92
|
export function createRemoteClient({ auth, env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
75
93
|
if (!auth) throw new Error('Remote client requires an auth manager.');
|
|
76
94
|
async function provider(functionName, body, method = 'POST', onProgress) {
|
|
@@ -90,10 +108,12 @@ export function createRemoteClient({ auth, env = process.env, fetchImpl = global
|
|
|
90
108
|
async googleSave({ filePath, title, mode = 'new', driveId, itemId, expectedRevision, filetype = 'document', onProgress }) {
|
|
91
109
|
await auth.getAccessToken();
|
|
92
110
|
onProgress?.({ provider: 'google', stage: 'preparing', current: 1, total: 3, filePath });
|
|
93
|
-
const
|
|
111
|
+
const document = await readExportDocument(filePath);
|
|
112
|
+
if (!document) throw new RemoteError('GOOGLE_FILE_TYPE_UNSUPPORTED', 'Google Drive upload requires a text-based GPDoc document. Convert Office files before uploading.');
|
|
113
|
+
const markdown = convertDocument(document, 'markdown');
|
|
94
114
|
onProgress?.({ provider: 'google', stage: 'uploading', current: 2, total: 3, filePath, bytes: Buffer.byteLength(markdown) });
|
|
95
115
|
const payload = await provider('google-drive-source-save', {
|
|
96
|
-
title: title || titleFrom(filePath), markdown, mode, driveId, itemId, expectedRevision, filetype,
|
|
116
|
+
title: title || document.title || titleFrom(filePath), markdown, mode, driveId, itemId, expectedRevision, filetype,
|
|
97
117
|
}, 'POST', () => {});
|
|
98
118
|
onProgress?.({ provider: 'google', stage: 'complete', current: 3, total: 3, filePath });
|
|
99
119
|
return { provider: 'google', mode, item: payload.item, warning: payload.warning || null };
|
|
@@ -102,11 +122,23 @@ export function createRemoteClient({ auth, env = process.env, fetchImpl = global
|
|
|
102
122
|
async microsoftSave({ filePath, mode = 'new', driveId, itemId, filename, onProgress }) {
|
|
103
123
|
await auth.getAccessToken();
|
|
104
124
|
onProgress?.({ provider: 'microsoft', stage: 'preparing', current: 1, total: 3, filePath });
|
|
105
|
-
const
|
|
125
|
+
const document = await readExportDocument(filePath);
|
|
126
|
+
if (document && document.filetype !== 'document') {
|
|
127
|
+
throw new RemoteError('MICROSOFT_FILE_TYPE_UNSUPPORTED', 'Microsoft 365 CLI export supports GPDoc documents.');
|
|
128
|
+
}
|
|
129
|
+
const bytes = document
|
|
130
|
+
? Buffer.from(convertDocument(document, 'docx'))
|
|
131
|
+
: await readFile(filePath);
|
|
132
|
+
const remoteFilename = document
|
|
133
|
+
? microsoftFilename(filename, document.title || titleFrom(filePath))
|
|
134
|
+
: filename || path.basename(filePath);
|
|
135
|
+
const remoteMimeType = document
|
|
136
|
+
? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
137
|
+
: mimeType(filePath);
|
|
106
138
|
if (bytes.byteLength > 4 * 1024 * 1024) throw new RemoteError('MICROSOFT_FILE_TOO_LARGE', 'Microsoft files must be 4 MiB or smaller.');
|
|
107
139
|
onProgress?.({ provider: 'microsoft', stage: 'uploading', current: 2, total: 3, filePath, bytes: bytes.byteLength });
|
|
108
140
|
const payload = await provider('microsoft-drive-export', {
|
|
109
|
-
mode, driveId, itemId, filename:
|
|
141
|
+
mode, driveId, itemId, filename: remoteFilename, mimeType: remoteMimeType, bytes: bytes.toString('base64'),
|
|
110
142
|
}, 'POST', () => {});
|
|
111
143
|
onProgress?.({ provider: 'microsoft', stage: 'complete', current: 3, total: 3, filePath });
|
|
112
144
|
return { provider: 'microsoft', mode, item: payload.item || payload };
|