@gpdoc/cli 1.2.0 → 1.2.2

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 CHANGED
@@ -18,7 +18,7 @@ Conversions write to standard output or an explicit output path. Existing files
18
18
 
19
19
  ## Account and remote providers
20
20
 
21
- Use `gpdoc login` to begin GPDoc's device authorization flow, open the printed verification URL in a browser, and then run `gpdoc login complete`. `gpdoc whoami --json` reports the CLI session without including credentials. `gpdoc logout` clears only the local CLI credential file.
21
+ Use `gpdoc login` to begin GPDoc's device authorization flow. It attempts to open your browser, waits for authorization, and writes the local CLI session before returning. If it cannot launch a browser, use the printed URL and code manually. Use `gpdoc login --no-browser` in a headless session. If you interrupt the command after completing browser authorization, run `gpdoc login complete` before the displayed expiry to finish saving the local session. `gpdoc whoami --json` reports the CLI session without including credentials. `gpdoc logout` clears only the local CLI credential file.
22
22
 
23
23
  For CI, set `GPDOC_ACCESS_TOKEN` instead of signing in interactively. The CLI does not persist that environment value. Interactive credentials are stored outside the current workspace with user-only permissions. Provider tokens are not shown in command output.
24
24
 
package/bin/gpdoc.js CHANGED
@@ -27,7 +27,7 @@ class CliError extends Error {
27
27
  }
28
28
 
29
29
  function usage() {
30
- return `Usage:\n gpdoc new OUTPUT [--title TITLE] [--json]\n gpdoc inspect INPUT [--json]\n gpdoc validate INPUT [--json]\n gpdoc convert INPUT --to gpdoc|markdown|html|text|json|docx|pdf [--output PATH | --in-place] [--json]\n gpdoc edit INPUT [--json]\n gpdoc login [complete] [--json]\n gpdoc logout [--json]\n gpdoc whoami [--json]\n gpdoc google upload INPUT [--title TITLE] [--json]\n gpdoc google update INPUT --drive-id ID --item-id ID --revision REVISION [--json]\n gpdoc microsoft upload INPUT [--filename NAME] [--json]\n gpdoc microsoft update INPUT --drive-id ID --item-id ID [--json]\n gpdoc microsoft share --drive-id ID --item-id ID --role view|edit --scope anonymous|organization [--json]\n gpdoc gist create INPUT [--private] [--description TEXT] [--json]\n gpdoc gist update GIST_ID INPUT [--description TEXT] [--json]\n gpdoc repo put OWNER/REPOSITORY INPUT --path REMOTE_PATH [--branch BRANCH] [--message TEXT] [--json]\n`;
30
+ return `Usage:\n gpdoc new OUTPUT [--title TITLE] [--json]\n gpdoc inspect INPUT [--json]\n gpdoc validate INPUT [--json]\n gpdoc convert INPUT --to gpdoc|markdown|html|text|json|docx|pdf [--output PATH | --in-place] [--json]\n gpdoc edit INPUT [--json]\n gpdoc login [complete] [--no-browser] [--json]\n gpdoc logout [--json]\n gpdoc whoami [--json]\n gpdoc google upload INPUT [--title TITLE] [--json]\n gpdoc google update INPUT --drive-id ID --item-id ID --revision REVISION [--json]\n gpdoc microsoft upload INPUT [--filename NAME] [--json]\n gpdoc microsoft update INPUT --drive-id ID --item-id ID [--json]\n gpdoc microsoft share --drive-id ID --item-id ID --role view|edit --scope anonymous|organization [--json]\n gpdoc gist create INPUT [--private] [--description TEXT] [--json]\n gpdoc gist update GIST_ID INPUT [--description TEXT] [--json]\n gpdoc repo put OWNER/REPOSITORY INPUT --path REMOTE_PATH [--branch BRANCH] [--message TEXT] [--json]\n`;
31
31
  }
32
32
 
33
33
  function parseArguments(argv) {
@@ -42,7 +42,7 @@ function parseArguments(argv) {
42
42
  continue;
43
43
  }
44
44
  const key = value.slice(2);
45
- if (['json', 'in-place', 'private'].includes(key)) {
45
+ if (['json', 'in-place', 'private', 'no-browser'].includes(key)) {
46
46
  options[key] = true;
47
47
  continue;
48
48
  }
@@ -105,6 +105,29 @@ function printResult(result, json) {
105
105
  else process.stdout.write(`${Object.entries(result).map(([key, value]) => `${key}: ${value}`).join('\n')}\n`);
106
106
  }
107
107
 
108
+ // @spec CLI-029
109
+ function renderWhoami(status) {
110
+ const { identity, pending, ...fields } = status;
111
+ const lines = Object.entries(fields).map(([key, value]) => `${key}: ${value}`);
112
+ if (identity && typeof identity === 'object') {
113
+ const orderedClaims = ['email', 'name', 'nickname', 'sub'];
114
+ const claimEntries = [
115
+ ...orderedClaims.filter((key) => typeof identity[key] === 'string').map((key) => [key, identity[key]]),
116
+ ...Object.entries(identity).filter(([key, value]) => !orderedClaims.includes(key) && typeof value === 'string'),
117
+ ];
118
+ if (claimEntries.length) {
119
+ lines.push('identity:');
120
+ lines.push(...claimEntries.map(([key, value]) => ` ${key}: ${value}`));
121
+ } else {
122
+ lines.push('identity: unavailable');
123
+ }
124
+ } else if (identity === null) {
125
+ lines.push('identity: null');
126
+ }
127
+ if (pending) lines.push('pending: true');
128
+ return lines.join('\n');
129
+ }
130
+
108
131
  function printError(error, json) {
109
132
  const result = { error: error.code || 'ERROR', message: error.message || String(error) };
110
133
  if (json) process.stderr.write(`${JSON.stringify(result)}\n`);
@@ -119,6 +142,47 @@ function launchEditor(editor, target) {
119
142
  });
120
143
  }
121
144
 
145
+ function delay(milliseconds) {
146
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
147
+ }
148
+
149
+ // The device flow does not send a callback to the local terminal. The foreground
150
+ // CLI process opens the provider page, then polls the token endpoint itself.
151
+ export async function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
152
+ const command = platform === 'darwin'
153
+ ? ['open', [url]]
154
+ : platform === 'win32'
155
+ ? ['cmd', ['/c', 'start', '', url]]
156
+ : ['xdg-open', [url]];
157
+ try {
158
+ return await new Promise((resolve) => {
159
+ let settled = false;
160
+ const complete = (opened) => {
161
+ if (settled) return;
162
+ settled = true;
163
+ resolve(opened);
164
+ };
165
+ const child = spawnImpl(command[0], command[1], { detached: true, stdio: 'ignore', windowsHide: true });
166
+ child.once('error', () => complete(false));
167
+ child.once('spawn', () => {
168
+ child.unref?.();
169
+ complete(true);
170
+ });
171
+ });
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ async function waitForDeviceAuthorization(auth, sleep = delay) {
178
+ for (;;) {
179
+ const result = await auth.complete();
180
+ if (result.status !== 'pending') return result;
181
+ const retryAfterSeconds = Math.max(1, Number(result.retryAfterSeconds || 1));
182
+ await sleep(retryAfterSeconds * 1000);
183
+ }
184
+ }
185
+
122
186
  function requireOption(options, key, message) {
123
187
  if (!options[key]) throw new CliError('USAGE', message || `--${key} is required.`);
124
188
  return options[key];
@@ -139,7 +203,7 @@ function renderRemote(result, json) {
139
203
  return printResult(result, false);
140
204
  }
141
205
 
142
- // @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
206
+ // @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
143
207
  export async function run(argv, runtime = {}) {
144
208
  const parsed = parseArguments(argv);
145
209
  if (parsed.command === 'help') {
@@ -154,8 +218,22 @@ export async function run(argv, runtime = {}) {
154
218
  const remote = runtime.remote || createRemoteClient({ auth, env: runtime.env || process.env, fetchImpl: runtime.fetchImpl || globalThis.fetch });
155
219
 
156
220
  if (parsed.command === 'login') {
157
- const result = parsed.positionals[0] === 'complete' ? await auth.complete() : await auth.start();
158
- printResult(result, parsed.options.json);
221
+ if (parsed.positionals[0] === 'complete') {
222
+ printResult(await auth.complete(), parsed.options.json);
223
+ return;
224
+ }
225
+ const started = await auth.start();
226
+ const verificationUrl = started.verificationUriComplete || started.verificationUri;
227
+ const browserOpened = parsed.options['no-browser']
228
+ ? false
229
+ : await (runtime.openBrowser || openBrowser)(verificationUrl);
230
+ if (!parsed.options.json) {
231
+ printResult(started, false);
232
+ if (browserOpened) process.stdout.write('Opened your browser. Complete sign-in there; GPDoc will finish this command automatically.\n');
233
+ else process.stdout.write(`Open ${verificationUrl} in a browser, then return here. GPDoc is waiting for authorization.\n`);
234
+ }
235
+ const completed = await waitForDeviceAuthorization(auth, runtime.sleep || delay);
236
+ printResult({ ...started, browserOpened, ...completed }, parsed.options.json);
159
237
  return;
160
238
  }
161
239
  if (parsed.command === 'logout') {
@@ -163,7 +241,8 @@ export async function run(argv, runtime = {}) {
163
241
  return;
164
242
  }
165
243
  if (parsed.command === 'whoami') {
166
- printResult(await auth.status(), parsed.options.json);
244
+ const status = await auth.status();
245
+ printResult(parsed.options.json ? status : renderWhoami(status), parsed.options.json);
167
246
  return;
168
247
  }
169
248
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpdoc/cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "description": "GPDoc command-line file conversion and validation tools",
6
6
  "repository": "https://github.com/repetere/gpdoc.git",