@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 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 renderList(result, json) {
322
- if (json) return printResult(result, true);
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
- const rows = result.items.map((item) => {
325
- const id = item?.providerData?.itemId || item?.itemId || item?.id || '';
326
- return `${item?.kind || 'file'}\t${item?.name || 'Untitled'}\t${id}\t${item?.webUrl || ''}`;
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
- return printResult(rows.length ? `kind\tname\tid\turl\n${rows.join('\n')}` : 'No files found.', false);
329
- }
330
- const owned = (result.owned || []).map((file) => `owned\t${file?.filename || 'Untitled'}\t${file?.shareId || ''}`);
331
- const shared = (result.sharedWithMe || []).map((file) => `shared\t${file?.filename || 'Untitled'}\t${file?.shareId || ''}`);
332
- return printResult([...owned, ...shared].length ? `access\tname\tshareId\n${[...owned, ...shared].join('\n')}` : 'No shared files found.', false);
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 renderList(await remote.googleList({ driveId: parsed.options['drive-id'], parentId: parsed.options['parent-id'], cursor: parsed.options.cursor, shared: parsed.options.shared === true }), parsed.options.json);
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 renderList(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 }), parsed.options.json);
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 markdown = await readFile(filePath, 'utf8');
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 bytes = await readFile(filePath);
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: filename || path.basename(filePath), mimeType: mimeType(filePath), bytes: bytes.toString('base64'),
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpdoc/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "GPDoc command-line file conversion and validation tools",
6
6
  "repository": "https://github.com/repetere/gpdoc.git",