@gpdoc/cli 1.5.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.
Files changed (2) hide show
  1. package/bin/gpdoc.js +88 -14
  2. 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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpdoc/cli",
3
- "version": "1.5.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",