@gpdoc/cli 1.7.0 → 1.8.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/README.md +13 -0
- package/bin/gpdoc.js +129 -19
- package/lib/list-state.js +51 -0
- package/lib/remote.js +31 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,17 @@ gpdoc google list --shared
|
|
|
54
54
|
gpdoc microsoft list
|
|
55
55
|
gpdoc microsoft list --query "release plan"
|
|
56
56
|
|
|
57
|
+
# The table identifies folders and document kinds. When another page exists,
|
|
58
|
+
# GPDoc tells you to run --next. Use --cursor only for scripts.
|
|
59
|
+
gpdoc google list --next
|
|
60
|
+
gpdoc google list --cursor 'CURSOR_FROM_JSON'
|
|
61
|
+
|
|
62
|
+
# Open an item in its provider, or import a supported document into a new
|
|
63
|
+
# local GPDoc Markdown file and open it in GPEditor.
|
|
64
|
+
gpdoc google list --open 7
|
|
65
|
+
gpdoc google list --open-gpdoc 7 --output docs/imported-plan.gpdoc.md
|
|
66
|
+
gpdoc microsoft list --open-gpdoc 3
|
|
67
|
+
|
|
57
68
|
# List GPDoc files you own and files others shared with your GPDoc account.
|
|
58
69
|
gpdoc shared list
|
|
59
70
|
```
|
|
@@ -78,6 +89,8 @@ gpdoc repo put repetere/example docs/release-plan.gpdoc.md --path docs/release-p
|
|
|
78
89
|
|
|
79
90
|
Uploads report preparation, transfer, and completion progress in an interactive terminal. Use `--json` for script-safe output without progress lines. Private Gists and private repositories require the same GPDoc GitHub-source entitlement as the VS Code extension. Google and Microsoft operations use GPDoc's provider API boundary; GitHub actions use the GitHub identity linked to the signed-in GPDoc account.
|
|
80
91
|
|
|
92
|
+
`--open-gpdoc ROW` supports Google Docs and Microsoft Word documents. It imports the selected document into a new managed local `.gpdoc.md` file, preserves source details as GPDoc metadata, opens that local copy in GPEditor, and leaves the original Google Drive or Microsoft 365 file unchanged. If `--output` is omitted, GPDoc creates a safe filename in the current directory and refuses to replace an existing file.
|
|
93
|
+
|
|
81
94
|
If a remote command reports `ACCOUNT_STATE_UNAVAILABLE`, GPDoc could not verify the signed-in account and intentionally did not send a provider request. Retry after the GPDoc account service recovers, then run `gpdoc google status` or `gpdoc microsoft status` to confirm the connection.
|
|
82
95
|
|
|
83
96
|
## Git Knowledge
|
package/bin/gpdoc.js
CHANGED
|
@@ -9,6 +9,7 @@ import process from 'node:process';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { createAuthManager } from '../lib/auth.js';
|
|
11
11
|
import { startGPEditorPreview } from '../lib/editor-preview.js';
|
|
12
|
+
import { createListState } from '../lib/list-state.js';
|
|
12
13
|
import { createRemoteClient } from '../lib/remote.js';
|
|
13
14
|
const {
|
|
14
15
|
convertDocument,
|
|
@@ -33,7 +34,7 @@ class CliError extends Error {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function usage() {
|
|
36
|
-
return `Usage:\n gpdoc version [--json]\n gpdoc new OUTPUT [--title TITLE] [--json]\n gpdoc inspect INPUT [--json]\n gpdoc validate INPUT [--json]\n gpdoc convert INPUT --to gpdoc|markdown|html|text|json|docx|pdf [--output PATH | --in-place] [--json]\n gpdoc edit INPUT [--editor web|local] [--json]\n gpdoc login [complete] [--no-browser] [--json]\n gpdoc logout [--json]\n gpdoc whoami [--json]\n gpdoc google connect|status [--no-browser] [--json]\n gpdoc google list [--drive-id ID --parent-id ID] [--shared] [--cursor TOKEN] [--json]\n gpdoc google upload INPUT [--title TITLE] [--json]\n gpdoc google update INPUT --drive-id ID --item-id ID --revision REVISION [--json]\n gpdoc microsoft connect|status [--no-browser] [--json]\n gpdoc microsoft list [--query TEXT] [--drive-id ID --item-id ID] [--shared] [--cursor TOKEN] [--json]\n gpdoc microsoft upload INPUT [--filename NAME] [--json]\n gpdoc microsoft update INPUT --drive-id ID --item-id ID [--json]\n gpdoc microsoft share --drive-id ID --item-id ID --role view|edit --scope anonymous|organization [--json]\n gpdoc shared list [--json]\n gpdoc gist create INPUT [--private] [--description TEXT] [--json]\n gpdoc gist update GIST_ID INPUT [--description TEXT] [--json]\n gpdoc repo put OWNER/REPOSITORY INPUT --path REMOTE_PATH [--branch BRANCH] [--message TEXT] [--json]\n gpdoc knowledge github-authorize [--json]\n gpdoc knowledge list [--json]\n gpdoc knowledge status REPOSITORY_ID [--json]\n gpdoc knowledge search REPOSITORY_ID[,REPOSITORY_ID] QUERY [--json]\n gpdoc knowledge connect GITHUB_REPOSITORY_ID [--json]\n gpdoc knowledge reindex REPOSITORY_ID [--json]\n gpdoc knowledge members REPOSITORY_ID [ACCOUNT_ID [ROLE]] [--json]\n gpdoc knowledge wiki-propose REPOSITORY_ID PATH CONTENT [--json]\n gpdoc knowledge disconnect REPOSITORY_ID --yes [--json]\n`;
|
|
37
|
+
return `Usage:\n gpdoc version [--json]\n gpdoc new OUTPUT [--title TITLE] [--json]\n gpdoc inspect INPUT [--json]\n gpdoc validate INPUT [--json]\n gpdoc convert INPUT --to gpdoc|markdown|html|text|json|docx|pdf [--output PATH | --in-place] [--json]\n gpdoc edit INPUT [--editor web|local] [--json]\n gpdoc login [complete] [--no-browser] [--json]\n gpdoc logout [--json]\n gpdoc whoami [--json]\n gpdoc google connect|status [--no-browser] [--json]\n gpdoc google list [--drive-id ID --parent-id ID] [--shared] [--cursor TOKEN | --next] [--open ROW | --open-gpdoc ROW [--output PATH]] [--json]\n gpdoc google upload INPUT [--title TITLE] [--json]\n gpdoc google update INPUT --drive-id ID --item-id ID --revision REVISION [--json]\n gpdoc microsoft connect|status [--no-browser] [--json]\n gpdoc microsoft list [--query TEXT] [--drive-id ID --item-id ID] [--shared] [--cursor TOKEN | --next] [--open ROW | --open-gpdoc ROW [--output PATH]] [--json]\n gpdoc microsoft upload INPUT [--filename NAME] [--json]\n gpdoc microsoft update INPUT --drive-id ID --item-id ID [--json]\n gpdoc microsoft share --drive-id ID --item-id ID --role view|edit --scope anonymous|organization [--json]\n gpdoc shared list [--json]\n gpdoc gist create INPUT [--private] [--description TEXT] [--json]\n gpdoc gist update GIST_ID INPUT [--description TEXT] [--json]\n gpdoc repo put OWNER/REPOSITORY INPUT --path REMOTE_PATH [--branch BRANCH] [--message TEXT] [--json]\n gpdoc knowledge github-authorize [--json]\n gpdoc knowledge list [--json]\n gpdoc knowledge status REPOSITORY_ID [--json]\n gpdoc knowledge search REPOSITORY_ID[,REPOSITORY_ID] QUERY [--json]\n gpdoc knowledge connect GITHUB_REPOSITORY_ID [--json]\n gpdoc knowledge reindex REPOSITORY_ID [--json]\n gpdoc knowledge members REPOSITORY_ID [ACCOUNT_ID [ROLE]] [--json]\n gpdoc knowledge wiki-propose REPOSITORY_ID PATH CONTENT [--json]\n gpdoc knowledge disconnect REPOSITORY_ID --yes [--json]\n`;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function knowledgeError(response, payload) {
|
|
@@ -155,11 +156,11 @@ function parseArguments(argv) {
|
|
|
155
156
|
continue;
|
|
156
157
|
}
|
|
157
158
|
const key = value.slice(2);
|
|
158
|
-
if (['json', 'in-place', 'private', 'no-browser', 'shared'].includes(key)) {
|
|
159
|
+
if (['json', 'in-place', 'private', 'no-browser', 'shared', 'next'].includes(key)) {
|
|
159
160
|
options[key] = true;
|
|
160
161
|
continue;
|
|
161
162
|
}
|
|
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
|
+
if (!['title', 'to', 'output', 'drive-id', 'item-id', 'parent-id', 'revision', 'filetype', 'filename', 'role', 'scope', 'description', 'branch', 'path', 'message', 'editor', 'cursor', 'query', 'open', 'open-gpdoc'].includes(key) || !rest[index + 1] || rest[index + 1].startsWith('--')) {
|
|
163
164
|
throw new CliError('USAGE', `Unknown or incomplete option: ${value}.`);
|
|
164
165
|
}
|
|
165
166
|
options[key] = rest[index + 1];
|
|
@@ -348,6 +349,76 @@ function listedItemId(item) {
|
|
|
348
349
|
return item?.providerData?.itemId || item?.itemId || item?.id || '';
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
function displayItemKind(item) {
|
|
353
|
+
if (item?.kind === 'folder' || item?.kind === 'drive') return 'Folder';
|
|
354
|
+
const mimeType = String(item?.mimeType || '').toLowerCase();
|
|
355
|
+
const name = String(item?.name || '').toLowerCase();
|
|
356
|
+
if (mimeType === 'application/vnd.google-apps.document') return 'Google Doc';
|
|
357
|
+
if (mimeType === 'application/vnd.google-apps.spreadsheet') return 'Google Sheet';
|
|
358
|
+
if (mimeType === 'application/vnd.google-apps.presentation') return 'Google Slides';
|
|
359
|
+
if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name.endsWith('.docx')) return 'Word document';
|
|
360
|
+
if (mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || name.endsWith('.pptx')) return 'PowerPoint';
|
|
361
|
+
if (mimeType === 'application/pdf' || name.endsWith('.pdf')) return 'PDF';
|
|
362
|
+
if (/markdown|text\/x-markdown/.test(mimeType) || /\.md(?:own)?$/.test(name)) return 'Markdown';
|
|
363
|
+
if (mimeType === 'text/csv' || name.endsWith('.csv')) return 'CSV';
|
|
364
|
+
return 'File';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function canOpenInGPEditor(provider, item) {
|
|
368
|
+
if (item?.kind === 'folder' || item?.kind === 'drive') return false;
|
|
369
|
+
const kind = displayItemKind(item);
|
|
370
|
+
return provider === 'google' ? kind === 'Google Doc' : provider === 'microsoft' ? kind === 'Word document' : false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function nextPageCommand(provider, cursor) {
|
|
374
|
+
return cursor ? `gpdoc ${provider} list --next` : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function listContext(provider, options) {
|
|
378
|
+
const keys = provider === 'google'
|
|
379
|
+
? ['driveId', 'parentId', 'shared', 'cursor']
|
|
380
|
+
: ['query', 'driveId', 'itemId', 'shared', 'cursor'];
|
|
381
|
+
const source = {
|
|
382
|
+
driveId: options['drive-id'],
|
|
383
|
+
parentId: options['parent-id'],
|
|
384
|
+
itemId: options['item-id'],
|
|
385
|
+
query: options.query,
|
|
386
|
+
shared: options.shared === true,
|
|
387
|
+
cursor: options.cursor,
|
|
388
|
+
};
|
|
389
|
+
return Object.fromEntries(keys.filter((key) => source[key] === true || typeof source[key] === 'string').map((key) => [key, source[key]]));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function resolveListContext(provider, options, state) {
|
|
393
|
+
if (!options.next) return listContext(provider, options);
|
|
394
|
+
if (options.cursor || options['drive-id'] || options['parent-id'] || options['item-id'] || options.query || options.shared || options.open || options['open-gpdoc'] || options.output || options.json) {
|
|
395
|
+
throw new CliError('USAGE', '--next cannot be combined with another list option.');
|
|
396
|
+
}
|
|
397
|
+
const saved = await state.load(provider);
|
|
398
|
+
if (!saved) throw new CliError('NEXT_PAGE_UNAVAILABLE', `No next ${displayProvider(provider)} page is available. Run gpdoc ${provider} list first.`);
|
|
399
|
+
return saved;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function defaultImportedPath(item) {
|
|
403
|
+
const original = String(item?.name || 'imported-document').trim();
|
|
404
|
+
const filename = path.basename(original).replace(/\.[^.]+$/, '').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '') || 'imported-document';
|
|
405
|
+
return path.resolve(`${filename}.gpdoc.md`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createImportedGpdoc({ provider, item, imported }) {
|
|
409
|
+
const title = String(imported?.document?.title || item?.name || 'Imported document').trim() || 'Imported document';
|
|
410
|
+
const body = imported?.document?.body;
|
|
411
|
+
if (typeof body !== 'string') throw new CliError('GPDOC_IMPORT_UNSUPPORTED', 'GPDoc could not convert the selected provider item into a document.');
|
|
412
|
+
const metadata = createManagedDocument(title, body);
|
|
413
|
+
metadata.meta = {
|
|
414
|
+
data: { source: provider === 'google' ? 'google-drive' : 'microsoft-365' },
|
|
415
|
+
integrations: provider === 'google'
|
|
416
|
+
? { google: { fileId: imported.item?.itemId || listedItemId(item), driveId: imported.item?.driveId || item?.providerData?.driveId || null, mimeType: item?.mimeType || imported.item?.mimeType || null } }
|
|
417
|
+
: { microsoft: { itemId: imported.item?.itemId || listedItemId(item), driveId: imported.item?.driveId || item?.providerData?.driveId || null, mimeType: item?.mimeType || imported.item?.mimeType || null } },
|
|
418
|
+
};
|
|
419
|
+
return serializeManagedMarkdown(metadata, body);
|
|
420
|
+
}
|
|
421
|
+
|
|
351
422
|
// @spec CLI-038
|
|
352
423
|
export function selectListedItem(items, row) {
|
|
353
424
|
const index = Number(row);
|
|
@@ -358,18 +429,18 @@ export function selectListedItem(items, row) {
|
|
|
358
429
|
}
|
|
359
430
|
|
|
360
431
|
// @spec CLI-038
|
|
361
|
-
export function formatList(result) {
|
|
432
|
+
export function formatList(result, { nextCommand } = {}) {
|
|
362
433
|
if (Array.isArray(result.items)) {
|
|
363
434
|
if (!result.items.length) return 'No files found.';
|
|
364
435
|
const columns = [
|
|
365
|
-
['#', 3], ['
|
|
436
|
+
['#', 3], ['Kind', 15], ['Name', 29], ['Modified', 10], ['Size', 9], ['ID', 18],
|
|
366
437
|
];
|
|
367
438
|
const header = columns.map(([label, width]) => label.padEnd(width)).join(' ').trimEnd();
|
|
368
439
|
const divider = columns.map(([, width]) => '─'.repeat(width)).join(' ');
|
|
369
440
|
const rows = result.items.map((item, index) => {
|
|
370
441
|
const values = [
|
|
371
442
|
String(index + 1),
|
|
372
|
-
item
|
|
443
|
+
displayItemKind(item),
|
|
373
444
|
item?.name || 'Untitled',
|
|
374
445
|
formatDate(item?.modifiedAt),
|
|
375
446
|
item?.kind === 'folder' ? '—' : formatBytes(item?.size),
|
|
@@ -378,7 +449,8 @@ export function formatList(result) {
|
|
|
378
449
|
return values.map((value, columnIndex) => truncate(value, columns[columnIndex][1]).padEnd(columns[columnIndex][1])).join(' ').trimEnd();
|
|
379
450
|
});
|
|
380
451
|
const provider = result.provider || 'provider';
|
|
381
|
-
|
|
452
|
+
const next = nextCommand ? `\n\nMore files are available. Next page:\n${nextCommand}` : '';
|
|
453
|
+
return `${displayProvider(provider)}\n${header}\n${divider}\n${rows.join('\n')}${next}\n\nOpen in provider: gpdoc ${provider} list --open ROW\nOpen a supported document in GPDoc: gpdoc ${provider} list --open-gpdoc ROW\nUse --json for full IDs and URLs.`;
|
|
382
454
|
}
|
|
383
455
|
const entries = [
|
|
384
456
|
...(result.owned || []).map((file) => ({ access: 'Owned', name: file?.filename || 'Untitled', id: file?.shareId || '' })),
|
|
@@ -395,14 +467,40 @@ function renderList(result, json) {
|
|
|
395
467
|
return printResult(formatList(result), false);
|
|
396
468
|
}
|
|
397
469
|
|
|
398
|
-
async function renderProviderList(result, { open, json, runtime }) {
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
470
|
+
async function renderProviderList(result, { open, openGpdoc, output, json, provider, remote, runtime }) {
|
|
471
|
+
const formatted = () => formatList(result, { nextCommand: nextPageCommand(provider, result.cursor) });
|
|
472
|
+
if (!open && !openGpdoc) {
|
|
473
|
+
if (json) return printResult(result, true);
|
|
474
|
+
return printResult(formatted(), false);
|
|
475
|
+
}
|
|
476
|
+
if (json || (open && openGpdoc)) throw new CliError('USAGE', 'Use one of --open ROW, --open-gpdoc ROW, or --json.');
|
|
477
|
+
const item = selectListedItem(result.items || [], open || openGpdoc);
|
|
478
|
+
if (open) {
|
|
479
|
+
if (!item?.webUrl) throw new CliError('PROVIDER_URL_UNAVAILABLE', 'The selected provider item has no browser URL. Use --json to inspect its identifiers.');
|
|
480
|
+
printResult(formatted(), false);
|
|
481
|
+
const browserOpened = await (runtime.openBrowser || openBrowser)(item.webUrl);
|
|
482
|
+
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`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!canOpenInGPEditor(provider, item)) {
|
|
486
|
+
throw new CliError('GPDOC_IMPORT_UNSUPPORTED', `Only ${provider === 'google' ? 'Google Docs' : 'Microsoft Word documents'} can open in the CLI GPEditor. The selected ${displayItemKind(item)} remains available with --open.`);
|
|
487
|
+
}
|
|
488
|
+
const target = path.resolve(output || defaultImportedPath(item));
|
|
489
|
+
if (await exists(target)) throw new CliError('OUTPUT_EXISTS', `Refusing to overwrite existing output: ${target}. Choose another --output path.`);
|
|
490
|
+
const ids = { driveId: item?.providerData?.driveId, itemId: listedItemId(item) };
|
|
491
|
+
const imported = provider === 'google'
|
|
492
|
+
? await remote.googleImport(ids)
|
|
493
|
+
: await remote.microsoftImport(ids);
|
|
494
|
+
await atomicWrite(target, createImportedGpdoc({ provider, item, imported }));
|
|
495
|
+
const preview = await (runtime.startPreview || startGPEditorPreview)({
|
|
496
|
+
filePath: target,
|
|
497
|
+
editorHtml: runtime.editorHtml ?? (runtime.startPreview ? '' : await loadEditorHtml()),
|
|
498
|
+
});
|
|
499
|
+
const browserOpened = await (runtime.openBrowser || openBrowser)(preview.url);
|
|
500
|
+
printResult(formatted(), false);
|
|
501
|
+
printResult({ imported: target, editor: 'gpeditor', browserOpened, url: preview.url }, false);
|
|
502
|
+
if (!browserOpened) process.stdout.write(`Open ${preview.url} in a browser. Keep this command running while you edit.\n`);
|
|
503
|
+
await (runtime.waitForPreview || waitForPreview)(preview);
|
|
406
504
|
}
|
|
407
505
|
|
|
408
506
|
function createProgressReporter(stderr = process.stderr) {
|
|
@@ -445,7 +543,7 @@ async function waitForProviderConnection(remote, providerName, sleep = delay) {
|
|
|
445
543
|
throw new CliError('PROVIDER_CONNECTION_TIMEOUT', `Timed out waiting for ${providerName} connection. Run gpdoc ${providerName} status to check it later.`);
|
|
446
544
|
}
|
|
447
545
|
|
|
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
|
|
546
|
+
// @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, CLI-039
|
|
449
547
|
export async function run(argv, runtime = {}) {
|
|
450
548
|
if (argv[0] === 'knowledge') {
|
|
451
549
|
await runKnowledge(argv.slice(1), runtime);
|
|
@@ -454,7 +552,7 @@ export async function run(argv, runtime = {}) {
|
|
|
454
552
|
const parsed = parseArguments(argv);
|
|
455
553
|
if (parsed.command === 'help') {
|
|
456
554
|
process.stdout.write(usage());
|
|
457
|
-
process.stdout.write('\nCloud file lists: use
|
|
555
|
+
process.stdout.write('\nCloud file lists: use --open ROW for the provider, --open-gpdoc ROW to import a supported document into GPEditor, and --next for the next page.\n');
|
|
458
556
|
return;
|
|
459
557
|
}
|
|
460
558
|
if (parsed.command === 'version') {
|
|
@@ -508,7 +606,13 @@ export async function run(argv, runtime = {}) {
|
|
|
508
606
|
const connected = await waitForProviderConnection(remote, 'google', runtime.sleep || delay);
|
|
509
607
|
return renderRemote({ ...started, browserOpened, ...connected }, parsed.options.json);
|
|
510
608
|
}
|
|
511
|
-
if (action === 'list')
|
|
609
|
+
if (action === 'list') {
|
|
610
|
+
const state = runtime.listState || createListState({ env: runtime.env || process.env });
|
|
611
|
+
const context = await resolveListContext('google', parsed.options, state);
|
|
612
|
+
const result = await remote.googleList(context);
|
|
613
|
+
if (!parsed.options.json) await state.save('google', result.cursor ? { ...context, cursor: result.cursor } : null);
|
|
614
|
+
return renderProviderList(result, { open: parsed.options.open, openGpdoc: parsed.options['open-gpdoc'], output: parsed.options.output, json: parsed.options.json, provider: 'google', remote, runtime });
|
|
615
|
+
}
|
|
512
616
|
const filePath = await requireLocalInput(input);
|
|
513
617
|
const progress = parsed.options.json ? undefined : (runtime.progress || createProgressReporter(runtime.stderr));
|
|
514
618
|
if (action === 'upload') return renderRemote(await remote.googleSave({ filePath, title: parsed.options.title, filetype: parsed.options.filetype || 'document', onProgress: progress }), parsed.options.json);
|
|
@@ -526,7 +630,13 @@ export async function run(argv, runtime = {}) {
|
|
|
526
630
|
const connected = await waitForProviderConnection(remote, 'microsoft', runtime.sleep || delay);
|
|
527
631
|
return renderRemote({ ...started, browserOpened, ...connected }, parsed.options.json);
|
|
528
632
|
}
|
|
529
|
-
if (action === 'list')
|
|
633
|
+
if (action === 'list') {
|
|
634
|
+
const state = runtime.listState || createListState({ env: runtime.env || process.env });
|
|
635
|
+
const context = await resolveListContext('microsoft', parsed.options, state);
|
|
636
|
+
const result = await remote.microsoftList(context);
|
|
637
|
+
if (!parsed.options.json) await state.save('microsoft', result.cursor ? { ...context, cursor: result.cursor } : null);
|
|
638
|
+
return renderProviderList(result, { open: parsed.options.open, openGpdoc: parsed.options['open-gpdoc'], output: parsed.options.output, json: parsed.options.json, provider: 'microsoft', remote, runtime });
|
|
639
|
+
}
|
|
530
640
|
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);
|
|
531
641
|
const filePath = await requireLocalInput(input);
|
|
532
642
|
const progress = parsed.options.json ? undefined : (runtime.progress || createProgressReporter(runtime.stderr));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function defaultPath(env = process.env) {
|
|
6
|
+
const configured = String(env.GPDOC_LIST_STATE_PATH || '').trim();
|
|
7
|
+
return configured || path.join(homedir(), '.config', 'gpdoc', 'list-state.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sanitizeContext(value) {
|
|
11
|
+
if (!value || typeof value !== 'object') return null;
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const key of ['driveId', 'parentId', 'itemId', 'query', 'cursor']) {
|
|
14
|
+
if (typeof value[key] === 'string' && value[key]) result[key] = value[key];
|
|
15
|
+
}
|
|
16
|
+
if (value.shared === true) result.shared = true;
|
|
17
|
+
return result.cursor ? result : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// @spec CLI-039
|
|
21
|
+
export function createListState({ env = process.env, filePath = defaultPath(env) } = {}) {
|
|
22
|
+
async function readState() {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
25
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
async load(provider) {
|
|
33
|
+
const state = await readState();
|
|
34
|
+
return sanitizeContext(state?.providers?.[provider]);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async save(provider, context) {
|
|
38
|
+
const state = await readState();
|
|
39
|
+
const providers = { ...(state.providers && typeof state.providers === 'object' ? state.providers : {}) };
|
|
40
|
+
const sanitized = sanitizeContext(context);
|
|
41
|
+
if (sanitized) providers[provider] = sanitized;
|
|
42
|
+
else delete providers[provider];
|
|
43
|
+
const directory = path.dirname(filePath);
|
|
44
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
45
|
+
const temporary = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
46
|
+
await writeFile(temporary, `${JSON.stringify({ version: 1, providers })}\n`, { mode: 0o600 });
|
|
47
|
+
await rename(temporary, filePath);
|
|
48
|
+
return sanitized;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/lib/remote.js
CHANGED
|
@@ -96,7 +96,7 @@ function githubHeaders(token) {
|
|
|
96
96
|
return { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28' };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// @spec CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025, CLI-037
|
|
99
|
+
// @spec CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025, CLI-037, CLI-039
|
|
100
100
|
export function createRemoteClient({ auth, env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
101
101
|
if (!auth) throw new Error('Remote client requires an auth manager.');
|
|
102
102
|
async function provider(functionName, body, method = 'POST', onProgress) {
|
|
@@ -197,6 +197,36 @@ export function createRemoteClient({ auth, env = process.env, fetchImpl = global
|
|
|
197
197
|
return { provider: 'microsoft', items, cursor: payload.cursor || payload.continuation || null };
|
|
198
198
|
},
|
|
199
199
|
|
|
200
|
+
async googleImport({ driveId, itemId }) {
|
|
201
|
+
if (!driveId || !itemId) throw new RemoteError('USAGE', 'Google GPDoc import requires a drive id and item id.');
|
|
202
|
+
const payload = await provider('google-drive-source-import', { driveId, itemId });
|
|
203
|
+
const body = payload?.content?.document;
|
|
204
|
+
if (payload?.filetype !== 'document' || typeof body !== 'string') {
|
|
205
|
+
throw new RemoteError('GPDOC_IMPORT_UNSUPPORTED', 'Only Google Docs can open in the CLI GPEditor. Open Google Slides in Google Drive.');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
provider: 'google',
|
|
209
|
+
item: payload.item || { driveId, itemId },
|
|
210
|
+
document: { title: payload.filename || payload.item?.name || 'Imported Google Doc', body },
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async microsoftImport({ driveId, itemId }) {
|
|
215
|
+
if (!driveId || !itemId) throw new RemoteError('USAGE', 'Microsoft GPDoc import requires a drive id and item id.');
|
|
216
|
+
const payload = await provider('microsoft-drive-import', { driveId, itemId });
|
|
217
|
+
const bytes = typeof payload?.bytes === 'string' ? Buffer.from(payload.bytes, 'base64') : null;
|
|
218
|
+
if (!bytes?.byteLength) throw new RemoteError('GPDOC_IMPORT_UNSUPPORTED', 'Microsoft 365 did not return a Word document for GPDoc import.');
|
|
219
|
+
const document = readDocument(bytes, payload.item?.name || 'imported.docx');
|
|
220
|
+
if (document.filetype !== 'document') {
|
|
221
|
+
throw new RemoteError('GPDOC_IMPORT_UNSUPPORTED', 'Only Microsoft Word documents can open in the CLI GPEditor.');
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
provider: 'microsoft',
|
|
225
|
+
item: payload.item || { driveId, itemId },
|
|
226
|
+
document: { title: payload.item?.name || document.title || 'Imported Word document', body: document.body },
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
|
|
200
230
|
async sharedList() {
|
|
201
231
|
const token = await auth.getAccessToken();
|
|
202
232
|
const payload = await jsonRequest(fetchImpl, `${apiBase(env)}/api/shared-files`, {
|