@gpdoc/cli 1.3.0 → 1.4.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 +79 -6
- package/package.json +4 -1
- package/web/editor.html +219 -0
package/lib/remote.js
CHANGED
|
@@ -21,7 +21,11 @@ async function jsonRequest(fetchImpl, url, init, fallbackCode = 'REMOTE_REQUEST_
|
|
|
21
21
|
}
|
|
22
22
|
const payload = await response.json().catch(() => ({}));
|
|
23
23
|
if (!response.ok) {
|
|
24
|
-
const
|
|
24
|
+
const code = payload.code || fallbackCode;
|
|
25
|
+
const message = code === 'ACCOUNT_STATE_UNAVAILABLE'
|
|
26
|
+
? 'GPDoc account-state verification is temporarily unavailable. No provider request was sent. Retry after the GPDoc account service is available.'
|
|
27
|
+
: payload.error || `Remote provider request failed (${response.status}).`;
|
|
28
|
+
const error = new RemoteError(code, message);
|
|
25
29
|
error.status = response.status;
|
|
26
30
|
throw error;
|
|
27
31
|
}
|
|
@@ -69,33 +73,102 @@ function githubHeaders(token) {
|
|
|
69
73
|
// @spec CLI-019, CLI-020, CLI-021, CLI-022, CLI-023, CLI-024, CLI-025
|
|
70
74
|
export function createRemoteClient({ auth, env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
71
75
|
if (!auth) throw new Error('Remote client requires an auth manager.');
|
|
72
|
-
async function provider(functionName, body, method = 'POST') {
|
|
76
|
+
async function provider(functionName, body, method = 'POST', onProgress) {
|
|
73
77
|
const token = await auth.getAccessToken();
|
|
78
|
+
onProgress?.({ stage: 'request', functionName });
|
|
74
79
|
return jsonRequest(fetchImpl, `${apiBase(env)}/.netlify/functions/${functionName}`, {
|
|
75
80
|
method, headers: bearer(token), ...(method === 'GET' ? {} : { body: JSON.stringify(body || {}) }),
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
function connectionFunction(providerName, action) {
|
|
85
|
+
if (!['google', 'microsoft'].includes(providerName)) throw new RemoteError('USAGE', 'Provider must be google or microsoft.');
|
|
86
|
+
return `${providerName}-oauth-${action}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
return {
|
|
80
|
-
async googleSave({ filePath, title, mode = 'new', driveId, itemId, expectedRevision, filetype = 'document' }) {
|
|
90
|
+
async googleSave({ filePath, title, mode = 'new', driveId, itemId, expectedRevision, filetype = 'document', onProgress }) {
|
|
81
91
|
await auth.getAccessToken();
|
|
92
|
+
onProgress?.({ provider: 'google', stage: 'preparing', current: 1, total: 3, filePath });
|
|
82
93
|
const markdown = await readFile(filePath, 'utf8');
|
|
94
|
+
onProgress?.({ provider: 'google', stage: 'uploading', current: 2, total: 3, filePath, bytes: Buffer.byteLength(markdown) });
|
|
83
95
|
const payload = await provider('google-drive-source-save', {
|
|
84
96
|
title: title || titleFrom(filePath), markdown, mode, driveId, itemId, expectedRevision, filetype,
|
|
85
|
-
});
|
|
97
|
+
}, 'POST', () => {});
|
|
98
|
+
onProgress?.({ provider: 'google', stage: 'complete', current: 3, total: 3, filePath });
|
|
86
99
|
return { provider: 'google', mode, item: payload.item, warning: payload.warning || null };
|
|
87
100
|
},
|
|
88
101
|
|
|
89
|
-
async microsoftSave({ filePath, mode = 'new', driveId, itemId, filename }) {
|
|
102
|
+
async microsoftSave({ filePath, mode = 'new', driveId, itemId, filename, onProgress }) {
|
|
90
103
|
await auth.getAccessToken();
|
|
104
|
+
onProgress?.({ provider: 'microsoft', stage: 'preparing', current: 1, total: 3, filePath });
|
|
91
105
|
const bytes = await readFile(filePath);
|
|
92
106
|
if (bytes.byteLength > 4 * 1024 * 1024) throw new RemoteError('MICROSOFT_FILE_TOO_LARGE', 'Microsoft files must be 4 MiB or smaller.');
|
|
107
|
+
onProgress?.({ provider: 'microsoft', stage: 'uploading', current: 2, total: 3, filePath, bytes: bytes.byteLength });
|
|
93
108
|
const payload = await provider('microsoft-drive-export', {
|
|
94
109
|
mode, driveId, itemId, filename: filename || path.basename(filePath), mimeType: mimeType(filePath), bytes: bytes.toString('base64'),
|
|
95
|
-
});
|
|
110
|
+
}, 'POST', () => {});
|
|
111
|
+
onProgress?.({ provider: 'microsoft', stage: 'complete', current: 3, total: 3, filePath });
|
|
96
112
|
return { provider: 'microsoft', mode, item: payload.item || payload };
|
|
97
113
|
},
|
|
98
114
|
|
|
115
|
+
async providerStatus(providerName) {
|
|
116
|
+
const payload = await provider(connectionFunction(providerName, 'status'), undefined, 'GET');
|
|
117
|
+
return { provider: providerName, connected: Boolean(payload.connection?.connected), connection: payload.connection || null };
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async providerConnect(providerName) {
|
|
121
|
+
const payload = await provider(connectionFunction(providerName, 'start'), {
|
|
122
|
+
externalClient: true,
|
|
123
|
+
returnOrigin: apiBase(env),
|
|
124
|
+
});
|
|
125
|
+
const launchUrl = typeof payload.launchUrl === 'string' ? payload.launchUrl : payload.authUrl;
|
|
126
|
+
if (!launchUrl) throw new RemoteError('OAUTH_START_FAILED', `GPDoc could not start ${providerName} authorization.`);
|
|
127
|
+
return { provider: providerName, launchUrl, expiresAt: payload.expiresAt || null };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async googleList({ driveId, parentId, cursor, shared = false }) {
|
|
131
|
+
if (driveId || parentId) {
|
|
132
|
+
if (!driveId || !parentId) throw new RemoteError('USAGE', 'Google listing requires both --drive-id and --parent-id.');
|
|
133
|
+
const payload = await provider('google-drive-browse', {
|
|
134
|
+
operation: 'children', driveId, parentId, rootKind: driveId === 'my-drive' ? 'my-drive' : 'shared-drive', continuation: cursor,
|
|
135
|
+
});
|
|
136
|
+
return { provider: 'google', items: payload.nodes || [], cursor: payload.cursor || null, incomplete: Boolean(payload.incomplete) };
|
|
137
|
+
}
|
|
138
|
+
const roots = await provider('google-drive-browse', undefined, 'GET');
|
|
139
|
+
const rootNodes = roots.nodes || [];
|
|
140
|
+
if (shared) return { provider: 'google', items: rootNodes.filter((node) => node?.providerData?.rootKind === 'shared-drive'), cursor: roots.cursor || null };
|
|
141
|
+
const myDrive = rootNodes.find((node) => node?.providerData?.rootKind === 'my-drive');
|
|
142
|
+
if (!myDrive?.providerData?.driveId || !myDrive?.providerData?.itemId) return { provider: 'google', items: [], cursor: roots.cursor || null };
|
|
143
|
+
const payload = await provider('google-drive-browse', {
|
|
144
|
+
operation: 'children', driveId: myDrive.providerData.driveId, parentId: myDrive.providerData.itemId, rootKind: 'my-drive', continuation: cursor,
|
|
145
|
+
});
|
|
146
|
+
return { provider: 'google', items: payload.nodes || [], cursor: payload.cursor || null, incomplete: Boolean(payload.incomplete) };
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async microsoftList({ driveId, itemId, cursor, query, shared = false }) {
|
|
150
|
+
const payload = query
|
|
151
|
+
? await provider('microsoft-drive-search', { query, continuation: cursor })
|
|
152
|
+
: await provider('microsoft-drive-browse', { driveId, itemId, continuation: cursor });
|
|
153
|
+
const entries = payload.nodes || payload.files || [];
|
|
154
|
+
const items = shared
|
|
155
|
+
? entries.filter((entry) => entry?.providerData?.remote === 'true' || entry?.remote === true)
|
|
156
|
+
: entries;
|
|
157
|
+
return { provider: 'microsoft', items, cursor: payload.cursor || payload.continuation || null };
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async sharedList() {
|
|
161
|
+
const token = await auth.getAccessToken();
|
|
162
|
+
const payload = await jsonRequest(fetchImpl, `${apiBase(env)}/api/shared-files`, {
|
|
163
|
+
method: 'GET', headers: bearer(token),
|
|
164
|
+
}, 'SHARED_FILES_REQUEST_FAILED');
|
|
165
|
+
return {
|
|
166
|
+
provider: 'gpdoc',
|
|
167
|
+
owned: payload.ownedFiles || [],
|
|
168
|
+
sharedWithMe: payload.collaboratingFiles || [],
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
|
|
99
172
|
async microsoftShare({ driveId, itemId, role, scope }) {
|
|
100
173
|
if (!driveId || !itemId) throw new RemoteError('USAGE', 'Microsoft sharing requires --drive-id and --item-id.');
|
|
101
174
|
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.4.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": {
|