@gpdoc/cli 1.3.0 → 1.5.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/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) {
@@ -21,7 +26,11 @@ async function jsonRequest(fetchImpl, url, init, fallbackCode = 'REMOTE_REQUEST_
21
26
  }
22
27
  const payload = await response.json().catch(() => ({}));
23
28
  if (!response.ok) {
24
- const error = new RemoteError(payload.code || fallbackCode, payload.error || `Remote provider request failed (${response.status}).`);
29
+ const code = payload.code || fallbackCode;
30
+ const message = code === 'ACCOUNT_STATE_UNAVAILABLE'
31
+ ? 'GPDoc account-state verification is temporarily unavailable. No provider request was sent. Retry after the GPDoc account service is available.'
32
+ : payload.error || `Remote provider request failed (${response.status}).`;
33
+ const error = new RemoteError(code, message);
25
34
  error.status = response.status;
26
35
  throw error;
27
36
  }
@@ -45,6 +54,19 @@ function titleFrom(filePath, fallback = 'GPDoc Export') {
45
54
  return path.basename(filePath).replace(/\.gpdoc\.md$/i, '').replace(/\.[^.]+$/, '') || fallback;
46
55
  }
47
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
+
48
70
  async function githubToken({ auth, env, fetchImpl }) {
49
71
  const token = await auth.getAccessToken();
50
72
  const payload = await jsonRequest(fetchImpl, `${apiBase(env)}/.netlify/functions/get-user-profile`, { headers: bearer(token) }, 'GITHUB_IDENTITY_REQUIRED');
@@ -66,36 +88,119 @@ function githubHeaders(token) {
66
88
  return { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28' };
67
89
  }
68
90
 
69
- // @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
70
92
  export function createRemoteClient({ auth, env = process.env, fetchImpl = globalThis.fetch } = {}) {
71
93
  if (!auth) throw new Error('Remote client requires an auth manager.');
72
- async function provider(functionName, body, method = 'POST') {
94
+ async function provider(functionName, body, method = 'POST', onProgress) {
73
95
  const token = await auth.getAccessToken();
96
+ onProgress?.({ stage: 'request', functionName });
74
97
  return jsonRequest(fetchImpl, `${apiBase(env)}/.netlify/functions/${functionName}`, {
75
98
  method, headers: bearer(token), ...(method === 'GET' ? {} : { body: JSON.stringify(body || {}) }),
76
99
  });
77
100
  }
78
101
 
102
+ function connectionFunction(providerName, action) {
103
+ if (!['google', 'microsoft'].includes(providerName)) throw new RemoteError('USAGE', 'Provider must be google or microsoft.');
104
+ return `${providerName}-oauth-${action}`;
105
+ }
106
+
79
107
  return {
80
- async googleSave({ filePath, title, mode = 'new', driveId, itemId, expectedRevision, filetype = 'document' }) {
108
+ async googleSave({ filePath, title, mode = 'new', driveId, itemId, expectedRevision, filetype = 'document', onProgress }) {
81
109
  await auth.getAccessToken();
82
- const markdown = await readFile(filePath, 'utf8');
110
+ onProgress?.({ provider: 'google', stage: 'preparing', current: 1, total: 3, filePath });
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');
114
+ onProgress?.({ provider: 'google', stage: 'uploading', current: 2, total: 3, filePath, bytes: Buffer.byteLength(markdown) });
83
115
  const payload = await provider('google-drive-source-save', {
84
- title: title || titleFrom(filePath), markdown, mode, driveId, itemId, expectedRevision, filetype,
85
- });
116
+ title: title || document.title || titleFrom(filePath), markdown, mode, driveId, itemId, expectedRevision, filetype,
117
+ }, 'POST', () => {});
118
+ onProgress?.({ provider: 'google', stage: 'complete', current: 3, total: 3, filePath });
86
119
  return { provider: 'google', mode, item: payload.item, warning: payload.warning || null };
87
120
  },
88
121
 
89
- async microsoftSave({ filePath, mode = 'new', driveId, itemId, filename }) {
122
+ async microsoftSave({ filePath, mode = 'new', driveId, itemId, filename, onProgress }) {
90
123
  await auth.getAccessToken();
91
- const bytes = await readFile(filePath);
124
+ onProgress?.({ provider: 'microsoft', stage: 'preparing', current: 1, total: 3, 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);
92
138
  if (bytes.byteLength > 4 * 1024 * 1024) throw new RemoteError('MICROSOFT_FILE_TOO_LARGE', 'Microsoft files must be 4 MiB or smaller.');
139
+ onProgress?.({ provider: 'microsoft', stage: 'uploading', current: 2, total: 3, filePath, bytes: bytes.byteLength });
93
140
  const payload = await provider('microsoft-drive-export', {
94
- mode, driveId, itemId, filename: filename || path.basename(filePath), mimeType: mimeType(filePath), bytes: bytes.toString('base64'),
95
- });
141
+ mode, driveId, itemId, filename: remoteFilename, mimeType: remoteMimeType, bytes: bytes.toString('base64'),
142
+ }, 'POST', () => {});
143
+ onProgress?.({ provider: 'microsoft', stage: 'complete', current: 3, total: 3, filePath });
96
144
  return { provider: 'microsoft', mode, item: payload.item || payload };
97
145
  },
98
146
 
147
+ async providerStatus(providerName) {
148
+ const payload = await provider(connectionFunction(providerName, 'status'), undefined, 'GET');
149
+ return { provider: providerName, connected: Boolean(payload.connection?.connected), connection: payload.connection || null };
150
+ },
151
+
152
+ async providerConnect(providerName) {
153
+ const payload = await provider(connectionFunction(providerName, 'start'), {
154
+ externalClient: true,
155
+ returnOrigin: apiBase(env),
156
+ });
157
+ const launchUrl = typeof payload.launchUrl === 'string' ? payload.launchUrl : payload.authUrl;
158
+ if (!launchUrl) throw new RemoteError('OAUTH_START_FAILED', `GPDoc could not start ${providerName} authorization.`);
159
+ return { provider: providerName, launchUrl, expiresAt: payload.expiresAt || null };
160
+ },
161
+
162
+ async googleList({ driveId, parentId, cursor, shared = false }) {
163
+ if (driveId || parentId) {
164
+ if (!driveId || !parentId) throw new RemoteError('USAGE', 'Google listing requires both --drive-id and --parent-id.');
165
+ const payload = await provider('google-drive-browse', {
166
+ operation: 'children', driveId, parentId, rootKind: driveId === 'my-drive' ? 'my-drive' : 'shared-drive', continuation: cursor,
167
+ });
168
+ return { provider: 'google', items: payload.nodes || [], cursor: payload.cursor || null, incomplete: Boolean(payload.incomplete) };
169
+ }
170
+ const roots = await provider('google-drive-browse', undefined, 'GET');
171
+ const rootNodes = roots.nodes || [];
172
+ if (shared) return { provider: 'google', items: rootNodes.filter((node) => node?.providerData?.rootKind === 'shared-drive'), cursor: roots.cursor || null };
173
+ const myDrive = rootNodes.find((node) => node?.providerData?.rootKind === 'my-drive');
174
+ if (!myDrive?.providerData?.driveId || !myDrive?.providerData?.itemId) return { provider: 'google', items: [], cursor: roots.cursor || null };
175
+ const payload = await provider('google-drive-browse', {
176
+ operation: 'children', driveId: myDrive.providerData.driveId, parentId: myDrive.providerData.itemId, rootKind: 'my-drive', continuation: cursor,
177
+ });
178
+ return { provider: 'google', items: payload.nodes || [], cursor: payload.cursor || null, incomplete: Boolean(payload.incomplete) };
179
+ },
180
+
181
+ async microsoftList({ driveId, itemId, cursor, query, shared = false }) {
182
+ const payload = query
183
+ ? await provider('microsoft-drive-search', { query, continuation: cursor })
184
+ : await provider('microsoft-drive-browse', { driveId, itemId, continuation: cursor });
185
+ const entries = payload.nodes || payload.files || [];
186
+ const items = shared
187
+ ? entries.filter((entry) => entry?.providerData?.remote === 'true' || entry?.remote === true)
188
+ : entries;
189
+ return { provider: 'microsoft', items, cursor: payload.cursor || payload.continuation || null };
190
+ },
191
+
192
+ async sharedList() {
193
+ const token = await auth.getAccessToken();
194
+ const payload = await jsonRequest(fetchImpl, `${apiBase(env)}/api/shared-files`, {
195
+ method: 'GET', headers: bearer(token),
196
+ }, 'SHARED_FILES_REQUEST_FAILED');
197
+ return {
198
+ provider: 'gpdoc',
199
+ owned: payload.ownedFiles || [],
200
+ sharedWithMe: payload.collaboratingFiles || [],
201
+ };
202
+ },
203
+
99
204
  async microsoftShare({ driveId, itemId, role, scope }) {
100
205
  if (!driveId || !itemId) throw new RemoteError('USAGE', 'Microsoft sharing requires --drive-id and --item-id.');
101
206
  if (!['view', 'edit'].includes(role)) throw new RemoteError('USAGE', 'Microsoft sharing requires --role view or edit.');
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@gpdoc/cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
7
7
  "files": [
8
8
  "bin",
9
9
  "lib",
10
+ "web",
10
11
  "README.md"
11
12
  ],
12
13
  "publishConfig": {
@@ -19,6 +20,8 @@
19
20
  "node": ">=20"
20
21
  },
21
22
  "scripts": {
23
+ "build:editor": "node scripts/build-editor.mjs",
24
+ "prepack": "npm run build:editor",
22
25
  "test": "node --test test/**/*.test.js"
23
26
  },
24
27
  "dependencies": {