@gpdoc/cli 1.2.1 → 1.3.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 +7 -0
- package/bin/gpdoc.js +136 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,9 @@ npm run cli -- convert notes.html --to gpdoc --output docs/notes.gpdoc.md
|
|
|
10
10
|
npm run cli -- convert https://example.com/guide --to markdown --output guide.md
|
|
11
11
|
npm run cli -- validate docs/notes.gpdoc.md --json
|
|
12
12
|
npm run cli -- edit docs/notes.gpdoc.md
|
|
13
|
+
npm run cli -- knowledge list --json
|
|
14
|
+
npm run cli -- knowledge search repo_123 "deployment review" --json
|
|
15
|
+
npm run cli -- knowledge github-authorize
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
Supported input formats are Markdown, managed GPDoc Markdown, HTML, plain text, GPDoc JSON documents, DOCX, and PPTX text. Supported outputs are `gpdoc`, `markdown`, `html`, `text`, `json`, `docx`, and `pdf`. DOCX/PPTX conversion preserves text and headings, not full presentation layout; PDF output is text-only. PDF input, RTF input, and PPTX output return an explicit unsupported-format error.
|
|
@@ -42,6 +45,10 @@ gpdoc repo put repetere/example docs/release-plan.gpdoc.md --path docs/release-p
|
|
|
42
45
|
|
|
43
46
|
Private Gists and private repositories require the same GPDoc GitHub-source entitlement as the VS Code extension. Google and Microsoft operations use GPDoc's provider API boundary; GitHub actions use the GitHub identity linked to the signed-in GPDoc account.
|
|
44
47
|
|
|
48
|
+
## Git Knowledge
|
|
49
|
+
|
|
50
|
+
Git Knowledge commands call the GPDoc Cloudflare Worker and use `GPDOC_ACCESS_TOKEN`. They never receive GitHub App credentials, Cloudflare credentials, or source-index bindings. Run `gpdoc knowledge github-authorize` to use the GitHub App device flow, then use `connect` with a GitHub repository ID. After authorization, the CLI can list, inspect, search, connect, reindex, manage members, propose reviewable wiki changes, and disconnect repositories. Use `--json` for machine-readable cited search results.
|
|
51
|
+
|
|
45
52
|
## Installation
|
|
46
53
|
|
|
47
54
|
After the public npm release, install with `npm install --global @gpdoc/cli`, or run one command without installation through `npx --yes @gpdoc/cli`. The initial distribution target is npm because the CLI requires Node.js 20 or later on every supported platform. A Homebrew formula and signed macOS, Windows, and Linux downloads should follow once release artifacts and signing are in place.
|
package/bin/gpdoc.js
CHANGED
|
@@ -27,9 +27,115 @@ 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] [--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`;
|
|
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 gpdoc knowledge github-authorize [--json]\n gpdoc knowledge list [--json]\n gpdoc knowledge status REPOSITORY_ID [--json]\n gpdoc knowledge search REPOSITORY_ID[,REPOSITORY_ID] QUERY [--json]\n gpdoc knowledge connect GITHUB_REPOSITORY_ID [--json]\n gpdoc knowledge reindex REPOSITORY_ID [--json]\n gpdoc knowledge members REPOSITORY_ID [ACCOUNT_ID [ROLE]] [--json]\n gpdoc knowledge wiki-propose REPOSITORY_ID PATH CONTENT [--json]\n gpdoc knowledge disconnect REPOSITORY_ID --yes [--json]\n`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function knowledgeError(response, payload) {
|
|
34
|
+
const error = new CliError(payload?.code || 'GIT_KNOWLEDGE_REQUEST_FAILED', payload?.error || `Git Knowledge request failed (${response.status}).`);
|
|
35
|
+
error.status = response.status;
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function knowledgeRequest(pathname, options = {}, runtime = {}) {
|
|
40
|
+
const env = runtime.env || process.env;
|
|
41
|
+
const accessToken = String(env.GPDOC_ACCESS_TOKEN || '').trim();
|
|
42
|
+
if (!accessToken) throw new CliError('AUTH_REQUIRED', 'Set GPDOC_ACCESS_TOKEN from a GPDoc device login before using Git Knowledge.');
|
|
43
|
+
const baseUrl = String(env.GPDOC_API_BASE_URL || 'https://gpdoc.io').trim().replace(/\/+$/, '');
|
|
44
|
+
const response = await fetch(`${baseUrl}/api/git-knowledge${pathname}`, {
|
|
45
|
+
...options,
|
|
46
|
+
headers: { authorization: `Bearer ${accessToken}`, ...(options.body ? { 'content-type': 'application/json' } : {}), ...(options.headers || {}) },
|
|
47
|
+
});
|
|
48
|
+
const payload = await response.json().catch(() => ({}));
|
|
49
|
+
if (!response.ok) throw knowledgeError(response, payload);
|
|
50
|
+
return payload;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runKnowledge(argv, runtime = {}) {
|
|
54
|
+
const request = (pathname, options) => knowledgeRequest(pathname, options, runtime);
|
|
55
|
+
const json = argv.includes('--json');
|
|
56
|
+
const args = argv.filter((value) => value !== '--json');
|
|
57
|
+
const [command, ...positionals] = args;
|
|
58
|
+
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
59
|
+
process.stdout.write(usage());
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (command === 'github-authorize') {
|
|
63
|
+
const started = await request('/github/device/authorizations', { method: 'POST', body: '{}' });
|
|
64
|
+
if (json) printResult({ state: started.state, verificationUri: started.verificationUriComplete || started.verificationUri, userCode: started.userCode, expiresAt: started.expiresAt }, true);
|
|
65
|
+
else process.stdout.write(`Open ${started.verificationUriComplete || started.verificationUri} and enter code: ${started.userCode}\n`);
|
|
66
|
+
const deadline = new Date(started.expiresAt).getTime();
|
|
67
|
+
let intervalMs = Math.max(5, Number(started.intervalSeconds || 5)) * 1000;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
70
|
+
const completed = await request('/github/device/authorizations/complete', { method: 'POST', body: '{}' });
|
|
71
|
+
if (completed.state === 'complete') {
|
|
72
|
+
if (!json) process.stdout.write('GitHub authorization completed. You can now connect a repository.\n');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
intervalMs = Math.max(5, Number(completed.intervalSeconds || intervalMs / 1000)) * 1000;
|
|
76
|
+
}
|
|
77
|
+
throw new CliError('GITHUB_DEVICE_AUTH_EXPIRED', 'GitHub device authorization expired. Run gpdoc knowledge github-authorize again.');
|
|
78
|
+
}
|
|
79
|
+
if (command === 'list') {
|
|
80
|
+
const result = await request('/repositories');
|
|
81
|
+
printResult(result.repositories || [], json);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (command === 'status') {
|
|
85
|
+
const repositoryId = positionals[0];
|
|
86
|
+
if (!repositoryId) throw new CliError('USAGE', 'gpdoc knowledge status requires a repository id.');
|
|
87
|
+
const result = await request(`/repositories/${encodeURIComponent(repositoryId)}`);
|
|
88
|
+
printResult(result.repository || result, json);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (command === 'search') {
|
|
92
|
+
const [scope, ...queryParts] = positionals;
|
|
93
|
+
if (!scope || !queryParts.length) throw new CliError('USAGE', 'gpdoc knowledge search requires repository ids and a query.');
|
|
94
|
+
const result = await request('/search', { method: 'POST', body: JSON.stringify({ repositoryIds: scope.split(',').map((value) => value.trim()).filter(Boolean), query: queryParts.join(' ') }) });
|
|
95
|
+
printResult(json ? result : (result.results || []).map((entry) => `${entry.citation}\n${entry.excerpt}\n`).join('\n') || 'No cited results.', json);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === 'connect') {
|
|
99
|
+
const githubRepositoryId = positionals[0];
|
|
100
|
+
if (!githubRepositoryId) throw new CliError('USAGE', 'gpdoc knowledge connect requires a GitHub repository id.');
|
|
101
|
+
const result = await request('/repositories', { method: 'POST', body: JSON.stringify({ githubRepositoryId }) });
|
|
102
|
+
printResult(result.repository || result, json);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (command === 'reindex') {
|
|
106
|
+
const repositoryId = positionals[0];
|
|
107
|
+
if (!repositoryId) throw new CliError('USAGE', 'gpdoc knowledge reindex requires a repository id.');
|
|
108
|
+
const result = await request(`/repositories/${encodeURIComponent(repositoryId)}/ingest`, { method: 'POST', body: '{}' });
|
|
109
|
+
printResult(result, json);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (command === 'members') {
|
|
113
|
+
const [repositoryId, accountId, role = 'viewer'] = positionals;
|
|
114
|
+
if (!repositoryId) throw new CliError('USAGE', 'gpdoc knowledge members requires a repository id.');
|
|
115
|
+
const result = accountId
|
|
116
|
+
? await request(`/repositories/${encodeURIComponent(repositoryId)}/members`, { method: 'POST', body: JSON.stringify({ accountId, role }) })
|
|
117
|
+
: await request(`/repositories/${encodeURIComponent(repositoryId)}/members`);
|
|
118
|
+
printResult(result.member || result.members || result, json);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (command === 'wiki-propose') {
|
|
122
|
+
const [repositoryId, pathValue, ...contentParts] = positionals;
|
|
123
|
+
if (!repositoryId || !pathValue || !contentParts.length) throw new CliError('USAGE', 'gpdoc knowledge wiki-propose requires a repository id, wiki path, and Markdown content.');
|
|
124
|
+
const result = await request(`/repositories/${encodeURIComponent(repositoryId)}/wiki-update`, { method: 'POST', body: JSON.stringify({ changes: [{ path: pathValue, content: contentParts.join(' ') }] }) });
|
|
125
|
+
printResult(result, json);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (command === 'disconnect') {
|
|
129
|
+
const repositoryId = positionals[0];
|
|
130
|
+
if (!repositoryId || !argv.includes('--yes')) throw new CliError('USAGE', 'gpdoc knowledge disconnect requires a repository id and --yes.');
|
|
131
|
+
const result = await request(`/repositories/${encodeURIComponent(repositoryId)}`, { method: 'DELETE' });
|
|
132
|
+
printResult(result, json);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw new CliError('USAGE', `Unknown Git Knowledge command: ${command}.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
33
139
|
function parseArguments(argv) {
|
|
34
140
|
const [command, ...rest] = argv;
|
|
35
141
|
if (!command || ['--help', '-h', 'help'].includes(command)) return { command: 'help', positionals: [], options: {} };
|
|
@@ -105,6 +211,29 @@ function printResult(result, json) {
|
|
|
105
211
|
else process.stdout.write(`${Object.entries(result).map(([key, value]) => `${key}: ${value}`).join('\n')}\n`);
|
|
106
212
|
}
|
|
107
213
|
|
|
214
|
+
// @spec CLI-029
|
|
215
|
+
function renderWhoami(status) {
|
|
216
|
+
const { identity, pending, ...fields } = status;
|
|
217
|
+
const lines = Object.entries(fields).map(([key, value]) => `${key}: ${value}`);
|
|
218
|
+
if (identity && typeof identity === 'object') {
|
|
219
|
+
const orderedClaims = ['email', 'name', 'nickname', 'sub'];
|
|
220
|
+
const claimEntries = [
|
|
221
|
+
...orderedClaims.filter((key) => typeof identity[key] === 'string').map((key) => [key, identity[key]]),
|
|
222
|
+
...Object.entries(identity).filter(([key, value]) => !orderedClaims.includes(key) && typeof value === 'string'),
|
|
223
|
+
];
|
|
224
|
+
if (claimEntries.length) {
|
|
225
|
+
lines.push('identity:');
|
|
226
|
+
lines.push(...claimEntries.map(([key, value]) => ` ${key}: ${value}`));
|
|
227
|
+
} else {
|
|
228
|
+
lines.push('identity: unavailable');
|
|
229
|
+
}
|
|
230
|
+
} else if (identity === null) {
|
|
231
|
+
lines.push('identity: null');
|
|
232
|
+
}
|
|
233
|
+
if (pending) lines.push('pending: true');
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
108
237
|
function printError(error, json) {
|
|
109
238
|
const result = { error: error.code || 'ERROR', message: error.message || String(error) };
|
|
110
239
|
if (json) process.stderr.write(`${JSON.stringify(result)}\n`);
|
|
@@ -182,6 +311,10 @@ function renderRemote(result, json) {
|
|
|
182
311
|
|
|
183
312
|
// @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
|
|
184
313
|
export async function run(argv, runtime = {}) {
|
|
314
|
+
if (argv[0] === 'knowledge') {
|
|
315
|
+
await runKnowledge(argv.slice(1), runtime);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
185
318
|
const parsed = parseArguments(argv);
|
|
186
319
|
if (parsed.command === 'help') {
|
|
187
320
|
process.stdout.write(usage());
|
|
@@ -218,7 +351,8 @@ export async function run(argv, runtime = {}) {
|
|
|
218
351
|
return;
|
|
219
352
|
}
|
|
220
353
|
if (parsed.command === 'whoami') {
|
|
221
|
-
|
|
354
|
+
const status = await auth.status();
|
|
355
|
+
printResult(parsed.options.json ? status : renderWhoami(status), parsed.options.json);
|
|
222
356
|
return;
|
|
223
357
|
}
|
|
224
358
|
|