@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/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 error = new RemoteError(payload.code || fallbackCode, payload.error || `Remote provider request failed (${response.status}).`);
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.0",
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": {