@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/README.md +40 -5
- package/bin/gpdoc.js +125 -20
- package/lib/auth.js +6 -4
- package/lib/editor-preview.js +186 -0
- package/lib/remote.js +116 -11
- package/package.json +4 -1
- package/web/editor.html +219 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
"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": {
|