@pokelo/cli 0.1.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 +121 -0
- package/bin/pokelo.mjs +8 -0
- package/package.json +33 -0
- package/skill/SKILL.md +98 -0
- package/skill/reference.md +59 -0
- package/skill/scripts/ensure-pokelo.sh +47 -0
- package/src/cli.mjs +256 -0
- package/src/commands/assets.mjs +283 -0
- package/src/commands/auth.mjs +109 -0
- package/src/commands/docs.mjs +131 -0
- package/src/commands/projects.mjs +35 -0
- package/src/commands/search.mjs +36 -0
- package/src/config.mjs +148 -0
- package/src/exit.mjs +13 -0
- package/src/http.mjs +71 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
|
|
6
|
+
import { resolveConfig } from '../config.mjs';
|
|
7
|
+
import { EXIT_API, fail, writeJson } from '../exit.mjs';
|
|
8
|
+
import { apiRequest } from '../http.mjs';
|
|
9
|
+
|
|
10
|
+
/** Match API `ASSET_BASE64_UPLOAD_MAX_BYTES`. */
|
|
11
|
+
const BASE64_MAX_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
const MIME_BY_EXT = {
|
|
14
|
+
'.png': 'image/png',
|
|
15
|
+
'.jpg': 'image/jpeg',
|
|
16
|
+
'.jpeg': 'image/jpeg',
|
|
17
|
+
'.gif': 'image/gif',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
'.svg': 'image/svg+xml',
|
|
20
|
+
'.pdf': 'application/pdf',
|
|
21
|
+
'.txt': 'text/plain',
|
|
22
|
+
'.md': 'text/markdown',
|
|
23
|
+
'.json': 'application/json',
|
|
24
|
+
'.csv': 'text/csv',
|
|
25
|
+
'.mp4': 'video/mp4',
|
|
26
|
+
'.webm': 'video/webm',
|
|
27
|
+
'.mp3': 'audio/mpeg',
|
|
28
|
+
'.wav': 'audio/wav',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function requireProject(config, flags) {
|
|
32
|
+
const project =
|
|
33
|
+
(typeof flags.project === 'string' && flags.project.trim()) || config.defaultProjectId;
|
|
34
|
+
if (!project) {
|
|
35
|
+
fail('project required (--project / -p, POKELO_PROJECT, or defaultProjectId in config)');
|
|
36
|
+
}
|
|
37
|
+
return project;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function guessMime(filePath, explicit) {
|
|
41
|
+
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
42
|
+
const ext = extname(filePath).toLowerCase();
|
|
43
|
+
return MIME_BY_EXT[ext] || 'application/octet-stream';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function flagStr(flags, key) {
|
|
47
|
+
return typeof flags[key] === 'string' && flags[key].trim() ? flags[key].trim() : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string[]} positional
|
|
52
|
+
* @param {Record<string, unknown>} flags
|
|
53
|
+
*/
|
|
54
|
+
export async function runAssets(positional, flags) {
|
|
55
|
+
const sub = positional[0];
|
|
56
|
+
if (!sub) {
|
|
57
|
+
fail(
|
|
58
|
+
'Usage: pokelo assets <list|get|upload|download|update|delete>\n' +
|
|
59
|
+
' pokelo assets list [--project] [--type] [--source] [--visibility] [--document] [--page] [--limit]\n' +
|
|
60
|
+
' pokelo assets get <asset-id> [--project]\n' +
|
|
61
|
+
' pokelo assets upload <file> [--name] [--mime] [--visibility] [--document] [--description] [--project]\n' +
|
|
62
|
+
' pokelo assets download <asset-id> --out <path> [--project]\n' +
|
|
63
|
+
' pokelo assets update <asset-id> [--name] [--description] [--visibility] [--document] [--project]\n' +
|
|
64
|
+
' pokelo assets delete <asset-id> [--project]',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const config = resolveConfig(flags);
|
|
69
|
+
const project = requireProject(config, flags);
|
|
70
|
+
const base = `/projects/${encodeURIComponent(project)}/assets`;
|
|
71
|
+
|
|
72
|
+
if (sub === 'list') {
|
|
73
|
+
const data = await apiRequest(config, 'GET', base, {
|
|
74
|
+
query: {
|
|
75
|
+
type: flagStr(flags, 'type'),
|
|
76
|
+
source: flagStr(flags, 'source'),
|
|
77
|
+
visibility: flagStr(flags, 'visibility'),
|
|
78
|
+
documentId: flagStr(flags, 'document'),
|
|
79
|
+
page: typeof flags.page === 'number' ? flags.page : undefined,
|
|
80
|
+
limit: typeof flags.limit === 'number' ? flags.limit : undefined,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
writeJson(data);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sub === 'get') {
|
|
88
|
+
const id = positional[1];
|
|
89
|
+
if (!id) fail('Usage: pokelo assets get <asset-id> [--project <ref>]');
|
|
90
|
+
const data = await apiRequest(config, 'GET', `${base}/${encodeURIComponent(id)}`);
|
|
91
|
+
writeJson(data);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (sub === 'upload') {
|
|
96
|
+
const filePath = positional[1];
|
|
97
|
+
if (!filePath) fail('Usage: pokelo assets upload <file> [--project <ref>]');
|
|
98
|
+
await uploadFile(config, project, filePath, flags);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (sub === 'download') {
|
|
103
|
+
const id = positional[1];
|
|
104
|
+
const out = flagStr(flags, 'out');
|
|
105
|
+
if (!id || !out) {
|
|
106
|
+
fail('Usage: pokelo assets download <asset-id> --out <path> [--project <ref>]');
|
|
107
|
+
}
|
|
108
|
+
await downloadAsset(config, project, id, out);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (sub === 'update') {
|
|
113
|
+
const id = positional[1];
|
|
114
|
+
if (!id) fail('Usage: pokelo assets update <asset-id> [options]');
|
|
115
|
+
const body = {};
|
|
116
|
+
if (flagStr(flags, 'name')) body.name = flagStr(flags, 'name');
|
|
117
|
+
if (typeof flags.description === 'string') body.description = flags.description;
|
|
118
|
+
if (flagStr(flags, 'visibility')) body.visibility = flagStr(flags, 'visibility');
|
|
119
|
+
if (typeof flags.document === 'string') {
|
|
120
|
+
body.documentId = flags.document.trim() || null;
|
|
121
|
+
}
|
|
122
|
+
if (Object.keys(body).length === 0) {
|
|
123
|
+
fail('assets update requires at least one of --name, --description, --visibility, --document');
|
|
124
|
+
}
|
|
125
|
+
const data = await apiRequest(config, 'PATCH', `${base}/${encodeURIComponent(id)}`, { body });
|
|
126
|
+
writeJson(data);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (sub === 'delete') {
|
|
131
|
+
const id = positional[1];
|
|
132
|
+
if (!id) fail('Usage: pokelo assets delete <asset-id> [--project <ref>]');
|
|
133
|
+
await apiRequest(config, 'DELETE', `${base}/${encodeURIComponent(id)}`);
|
|
134
|
+
writeJson({ deleted: true, id });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fail(`unknown assets command: ${sub}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function uploadFile(config, project, filePath, flags) {
|
|
142
|
+
let st;
|
|
143
|
+
try {
|
|
144
|
+
st = statSync(filePath);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
147
|
+
fail(`cannot read file: ${msg}`);
|
|
148
|
+
}
|
|
149
|
+
if (!st.isFile()) fail(`not a file: ${filePath}`);
|
|
150
|
+
|
|
151
|
+
const name = flagStr(flags, 'name') || basename(filePath);
|
|
152
|
+
const mimeType = guessMime(filePath, flagStr(flags, 'mime'));
|
|
153
|
+
const visibility = flagStr(flags, 'visibility');
|
|
154
|
+
const documentId = flagStr(flags, 'document');
|
|
155
|
+
const description = flagStr(flags, 'description');
|
|
156
|
+
const base = `/projects/${encodeURIComponent(project)}/assets`;
|
|
157
|
+
|
|
158
|
+
if (st.size <= BASE64_MAX_BYTES) {
|
|
159
|
+
const buf = readFileSync(filePath);
|
|
160
|
+
const body = {
|
|
161
|
+
name,
|
|
162
|
+
mimeType,
|
|
163
|
+
contentBase64: buf.toString('base64'),
|
|
164
|
+
...(visibility ? { visibility } : {}),
|
|
165
|
+
...(documentId ? { documentId } : {}),
|
|
166
|
+
...(description ? { description } : {}),
|
|
167
|
+
};
|
|
168
|
+
const data = await apiRequest(config, 'POST', `${base}/base64`, { body });
|
|
169
|
+
writeJson(data);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Large file: init → PUT → complete
|
|
174
|
+
const init = await apiRequest(config, 'POST', `${base}/uploads`, {
|
|
175
|
+
body: {
|
|
176
|
+
name,
|
|
177
|
+
mimeType,
|
|
178
|
+
size: st.size,
|
|
179
|
+
...(visibility ? { visibility } : {}),
|
|
180
|
+
...(documentId ? { documentId } : {}),
|
|
181
|
+
...(description ? { description } : {}),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const uploadUrl = init?.uploadUrl;
|
|
186
|
+
const headers = { ...(init?.headers || {}) };
|
|
187
|
+
if (!uploadUrl) fail('upload init did not return uploadUrl', EXIT_API);
|
|
188
|
+
|
|
189
|
+
let putRes;
|
|
190
|
+
try {
|
|
191
|
+
putRes = await fetch(uploadUrl, {
|
|
192
|
+
method: 'PUT',
|
|
193
|
+
headers,
|
|
194
|
+
body: createReadStream(filePath),
|
|
195
|
+
// Node fetch needs duplex when body is a stream
|
|
196
|
+
duplex: 'half',
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
+
fail(`S3 PUT failed: ${msg}`, EXIT_API);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!putRes.ok) {
|
|
204
|
+
const t = await putRes.text().catch(() => '');
|
|
205
|
+
fail(`S3 PUT HTTP ${putRes.status}${t ? `: ${t.slice(0, 200)}` : ''}`, EXIT_API);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const assetId = init?.asset?.id;
|
|
209
|
+
if (!assetId) fail('upload init did not return asset.id', EXIT_API);
|
|
210
|
+
|
|
211
|
+
const completed = await apiRequest(
|
|
212
|
+
config,
|
|
213
|
+
'POST',
|
|
214
|
+
`${base}/${encodeURIComponent(assetId)}/complete`,
|
|
215
|
+
);
|
|
216
|
+
writeJson(completed);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function downloadAsset(config, project, assetId, outPath) {
|
|
220
|
+
const asset = await apiRequest(
|
|
221
|
+
config,
|
|
222
|
+
'GET',
|
|
223
|
+
`/projects/${encodeURIComponent(project)}/assets/${encodeURIComponent(assetId)}`,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
let url = asset?.downloadUrl;
|
|
227
|
+
if (!url) {
|
|
228
|
+
// Fall back to stable content redirect (follow redirects with auth on first hop).
|
|
229
|
+
const contentUrl = new URL(
|
|
230
|
+
`${config.url}/projects/${encodeURIComponent(project)}/assets/${encodeURIComponent(assetId)}/content`,
|
|
231
|
+
);
|
|
232
|
+
let res;
|
|
233
|
+
try {
|
|
234
|
+
res = await fetch(contentUrl, {
|
|
235
|
+
method: 'GET',
|
|
236
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
237
|
+
redirect: 'manual',
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
fail(`content request failed: ${msg}`, EXIT_API);
|
|
242
|
+
}
|
|
243
|
+
if (res.status >= 300 && res.status < 400) {
|
|
244
|
+
url = res.headers.get('location');
|
|
245
|
+
} else if (res.ok && res.body) {
|
|
246
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(outPath));
|
|
247
|
+
writeJson({
|
|
248
|
+
id: assetId,
|
|
249
|
+
name: asset?.name ?? null,
|
|
250
|
+
out: outPath,
|
|
251
|
+
bytes: statSync(outPath).size,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
} else {
|
|
255
|
+
fail(`cannot resolve download URL (HTTP ${res.status})`, EXIT_API);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!url) fail('asset has no downloadUrl (not ready or no binary)', EXIT_API);
|
|
260
|
+
|
|
261
|
+
let res;
|
|
262
|
+
try {
|
|
263
|
+
res = await fetch(url);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
fail(`download failed: ${msg}`, EXIT_API);
|
|
267
|
+
}
|
|
268
|
+
if (!res.ok) fail(`download HTTP ${res.status}`, EXIT_API);
|
|
269
|
+
|
|
270
|
+
if (res.body) {
|
|
271
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(outPath));
|
|
272
|
+
} else {
|
|
273
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
274
|
+
writeFileSync(outPath, buf);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
writeJson({
|
|
278
|
+
id: assetId,
|
|
279
|
+
name: asset?.name ?? null,
|
|
280
|
+
out: outPath,
|
|
281
|
+
bytes: statSync(outPath).size,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output, stderr } from 'node:process';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
loadPersistedConfig,
|
|
6
|
+
maskToken,
|
|
7
|
+
normalizeApiBaseUrl,
|
|
8
|
+
saveConfig,
|
|
9
|
+
xdgConfigPath,
|
|
10
|
+
} from '../config.mjs';
|
|
11
|
+
import { fail, writeJson } from '../exit.mjs';
|
|
12
|
+
|
|
13
|
+
async function readTokenStdin() {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of input) {
|
|
16
|
+
chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
|
17
|
+
}
|
|
18
|
+
return chunks.join('').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function promptLogin() {
|
|
22
|
+
if (!input.isTTY) {
|
|
23
|
+
fail('interactive login requires a TTY; use --url and --token (or --token-stdin)');
|
|
24
|
+
}
|
|
25
|
+
const rl = createInterface({ input, output: stderr });
|
|
26
|
+
try {
|
|
27
|
+
const url = (await rl.question('Pokelo URL (e.g. https://pokelo.example.com): ')).trim();
|
|
28
|
+
const token = (await rl.question('Personal Access Token: ')).trim();
|
|
29
|
+
return { url, token };
|
|
30
|
+
} finally {
|
|
31
|
+
rl.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string[]} positional
|
|
37
|
+
* @param {Record<string, unknown>} flags
|
|
38
|
+
*/
|
|
39
|
+
export async function runAuth(positional, flags) {
|
|
40
|
+
const sub = positional[0];
|
|
41
|
+
if (!sub || sub === 'help') {
|
|
42
|
+
fail(
|
|
43
|
+
'Usage: pokelo auth <login|status>\n' +
|
|
44
|
+
' pokelo auth login --url <url> --token <token>\n' +
|
|
45
|
+
' pokelo auth login --url <url> --token-stdin\n' +
|
|
46
|
+
' pokelo auth login\n' +
|
|
47
|
+
' pokelo auth status',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sub === 'status') {
|
|
52
|
+
const file = loadPersistedConfig();
|
|
53
|
+
const envUrl = process.env.POKELO_URL?.trim() || null;
|
|
54
|
+
const envToken = process.env.POKELO_TOKEN?.trim() || null;
|
|
55
|
+
const url = envUrl || file?.url || null;
|
|
56
|
+
const token = envToken || file?.token || null;
|
|
57
|
+
writeJson({
|
|
58
|
+
configured: Boolean(url && token),
|
|
59
|
+
url: url ? normalizeApiBaseUrl(url) : null,
|
|
60
|
+
tokenSet: Boolean(token),
|
|
61
|
+
tokenPreview: token ? maskToken(token) : null,
|
|
62
|
+
defaultProjectId: process.env.POKELO_PROJECT?.trim() || file?.defaultProjectId || null,
|
|
63
|
+
configPath: xdgConfigPath(),
|
|
64
|
+
sources: {
|
|
65
|
+
url: envUrl ? 'env' : file?.url ? 'file' : null,
|
|
66
|
+
token: envToken ? 'env' : file?.token ? 'file' : null,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (sub === 'login') {
|
|
73
|
+
let url = typeof flags.url === 'string' ? flags.url.trim() : '';
|
|
74
|
+
let token = typeof flags.token === 'string' ? flags.token.trim() : '';
|
|
75
|
+
const tokenStdin = flags.tokenStdin === true;
|
|
76
|
+
|
|
77
|
+
if (tokenStdin) {
|
|
78
|
+
token = await readTokenStdin();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!url || !token) {
|
|
82
|
+
if (!url && !token && !tokenStdin && input.isTTY) {
|
|
83
|
+
const prompted = await promptLogin();
|
|
84
|
+
url = url || prompted.url;
|
|
85
|
+
token = token || prompted.token;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!url) fail('url is required (--url or interactive prompt)');
|
|
90
|
+
if (!token) fail('token is required (--token, --token-stdin, or interactive prompt)');
|
|
91
|
+
|
|
92
|
+
const existing = loadPersistedConfig();
|
|
93
|
+
const path = saveConfig({
|
|
94
|
+
url,
|
|
95
|
+
token,
|
|
96
|
+
defaultProjectId: existing?.defaultProjectId ?? null,
|
|
97
|
+
});
|
|
98
|
+
writeJson({
|
|
99
|
+
ok: true,
|
|
100
|
+
configPath: path,
|
|
101
|
+
url: normalizeApiBaseUrl(url),
|
|
102
|
+
tokenSet: true,
|
|
103
|
+
tokenPreview: maskToken(token),
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fail(`unknown auth command: ${sub}`);
|
|
109
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { resolveConfig } from '../config.mjs';
|
|
4
|
+
import { fail, writeJson } from '../exit.mjs';
|
|
5
|
+
import { apiRequest } from '../http.mjs';
|
|
6
|
+
|
|
7
|
+
function requireProject(config, flags) {
|
|
8
|
+
const project =
|
|
9
|
+
(typeof flags.project === 'string' && flags.project.trim()) || config.defaultProjectId;
|
|
10
|
+
if (!project) {
|
|
11
|
+
fail('project required (--project / -p, POKELO_PROJECT, or defaultProjectId in config)');
|
|
12
|
+
}
|
|
13
|
+
return project;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveContent(flags) {
|
|
17
|
+
if (typeof flags.contentFile === 'string' && flags.contentFile.trim()) {
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(flags.contentFile.trim(), 'utf8');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22
|
+
fail(`cannot read --content-file: ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (typeof flags.content === 'string') return flags.content;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string[]} positional
|
|
31
|
+
* @param {Record<string, unknown>} flags
|
|
32
|
+
*/
|
|
33
|
+
export async function runDocs(positional, flags) {
|
|
34
|
+
const sub = positional[0];
|
|
35
|
+
if (!sub) {
|
|
36
|
+
fail(
|
|
37
|
+
'Usage: pokelo docs <list|get|create|update>\n' +
|
|
38
|
+
' pokelo docs list [--project <ref>]\n' +
|
|
39
|
+
' pokelo docs get <id-or-slug> [--project <ref>]\n' +
|
|
40
|
+
' pokelo docs create --title <t> [--type] [--content|--content-file] [--parent] [--project]\n' +
|
|
41
|
+
' pokelo docs update <id-or-slug> [--title] [--content|--content-file] [--project]',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const config = resolveConfig(flags);
|
|
46
|
+
const project = requireProject(config, flags);
|
|
47
|
+
|
|
48
|
+
if (sub === 'list') {
|
|
49
|
+
const data = await apiRequest(
|
|
50
|
+
config,
|
|
51
|
+
'GET',
|
|
52
|
+
`/projects/${encodeURIComponent(project)}/documents`,
|
|
53
|
+
{
|
|
54
|
+
query: {
|
|
55
|
+
page: typeof flags.page === 'number' ? flags.page : undefined,
|
|
56
|
+
limit: typeof flags.limit === 'number' ? flags.limit : undefined,
|
|
57
|
+
parentId: typeof flags.parent === 'string' ? flags.parent : undefined,
|
|
58
|
+
tree: flags.tree === true ? 'true' : undefined,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
writeJson(data);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (sub === 'get') {
|
|
67
|
+
const docRef = positional[1];
|
|
68
|
+
if (!docRef) fail('Usage: pokelo docs get <document-id-or-slug> [--project <ref>]');
|
|
69
|
+
const data = await apiRequest(
|
|
70
|
+
config,
|
|
71
|
+
'GET',
|
|
72
|
+
`/projects/${encodeURIComponent(project)}/documents/${encodeURIComponent(docRef)}`,
|
|
73
|
+
);
|
|
74
|
+
writeJson(data);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sub === 'create') {
|
|
79
|
+
const title = typeof flags.title === 'string' ? flags.title.trim() : '';
|
|
80
|
+
if (!title) fail('docs create requires --title');
|
|
81
|
+
const contentMd = resolveContent(flags);
|
|
82
|
+
const body = {
|
|
83
|
+
title,
|
|
84
|
+
...(typeof flags.type === 'string' && flags.type.trim()
|
|
85
|
+
? { type: flags.type.trim() }
|
|
86
|
+
: {}),
|
|
87
|
+
...(contentMd !== undefined ? { contentMd } : {}),
|
|
88
|
+
...(typeof flags.parent === 'string' && flags.parent.trim()
|
|
89
|
+
? { parentId: flags.parent.trim() }
|
|
90
|
+
: {}),
|
|
91
|
+
...(typeof flags.status === 'string' && flags.status.trim()
|
|
92
|
+
? { status: flags.status.trim() }
|
|
93
|
+
: {}),
|
|
94
|
+
};
|
|
95
|
+
const data = await apiRequest(
|
|
96
|
+
config,
|
|
97
|
+
'POST',
|
|
98
|
+
`/projects/${encodeURIComponent(project)}/documents`,
|
|
99
|
+
{ body },
|
|
100
|
+
);
|
|
101
|
+
writeJson(data);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (sub === 'update') {
|
|
106
|
+
const docRef = positional[1];
|
|
107
|
+
if (!docRef) fail('Usage: pokelo docs update <document-id-or-slug> [options]');
|
|
108
|
+
const contentMd = resolveContent(flags);
|
|
109
|
+
const body = {};
|
|
110
|
+
if (typeof flags.title === 'string' && flags.title.trim()) body.title = flags.title.trim();
|
|
111
|
+
if (contentMd !== undefined) body.contentMd = contentMd;
|
|
112
|
+
if (typeof flags.type === 'string' && flags.type.trim()) body.type = flags.type.trim();
|
|
113
|
+
if (typeof flags.status === 'string' && flags.status.trim()) body.status = flags.status.trim();
|
|
114
|
+
if (typeof flags.parent === 'string') {
|
|
115
|
+
body.parentId = flags.parent.trim() || null;
|
|
116
|
+
}
|
|
117
|
+
if (Object.keys(body).length === 0) {
|
|
118
|
+
fail('docs update requires at least one of --title, --content, --content-file, --type, --status, --parent');
|
|
119
|
+
}
|
|
120
|
+
const data = await apiRequest(
|
|
121
|
+
config,
|
|
122
|
+
'PATCH',
|
|
123
|
+
`/projects/${encodeURIComponent(project)}/documents/${encodeURIComponent(docRef)}`,
|
|
124
|
+
{ body },
|
|
125
|
+
);
|
|
126
|
+
writeJson(data);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fail(`unknown docs command: ${sub}`);
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveConfig } from '../config.mjs';
|
|
2
|
+
import { fail, writeJson } from '../exit.mjs';
|
|
3
|
+
import { apiRequest } from '../http.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} positional
|
|
7
|
+
* @param {Record<string, unknown>} flags
|
|
8
|
+
*/
|
|
9
|
+
export async function runProjects(positional, flags) {
|
|
10
|
+
const sub = positional[0];
|
|
11
|
+
const config = resolveConfig(flags);
|
|
12
|
+
|
|
13
|
+
if (sub === 'list' || !sub) {
|
|
14
|
+
const data = await apiRequest(config, 'GET', '/projects', {
|
|
15
|
+
query: {
|
|
16
|
+
page: typeof flags.page === 'number' ? flags.page : undefined,
|
|
17
|
+
limit: typeof flags.limit === 'number' ? flags.limit : undefined,
|
|
18
|
+
q: typeof flags.search === 'string' ? flags.search : undefined,
|
|
19
|
+
status: typeof flags.status === 'string' ? flags.status : undefined,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
writeJson(data);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (sub === 'get') {
|
|
27
|
+
const ref = positional[1];
|
|
28
|
+
if (!ref) fail('Usage: pokelo projects get <project-id-or-slug>');
|
|
29
|
+
const data = await apiRequest(config, 'GET', `/projects/${encodeURIComponent(ref)}`);
|
|
30
|
+
writeJson(data);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fail(`unknown projects command: ${sub}\nUsage: pokelo projects <list|get>`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveConfig } from '../config.mjs';
|
|
2
|
+
import { fail, writeJson } from '../exit.mjs';
|
|
3
|
+
import { apiRequest } from '../http.mjs';
|
|
4
|
+
|
|
5
|
+
function requireProject(config, flags) {
|
|
6
|
+
const project =
|
|
7
|
+
(typeof flags.project === 'string' && flags.project.trim()) || config.defaultProjectId;
|
|
8
|
+
if (!project) {
|
|
9
|
+
fail('project required (--project / -p, POKELO_PROJECT, or defaultProjectId in config)');
|
|
10
|
+
}
|
|
11
|
+
return project;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string[]} positional
|
|
16
|
+
* @param {Record<string, unknown>} flags
|
|
17
|
+
*/
|
|
18
|
+
export async function runSearch(positional, flags) {
|
|
19
|
+
const query = positional[0];
|
|
20
|
+
if (!query) fail('Usage: pokelo search "<query>" [--project <ref>] [--limit <n>]');
|
|
21
|
+
const config = resolveConfig(flags);
|
|
22
|
+
const project = requireProject(config, flags);
|
|
23
|
+
const limit = typeof flags.limit === 'number' ? flags.limit : undefined;
|
|
24
|
+
const data = await apiRequest(
|
|
25
|
+
config,
|
|
26
|
+
'POST',
|
|
27
|
+
`/projects/${encodeURIComponent(project)}/documents/search`,
|
|
28
|
+
{
|
|
29
|
+
body: {
|
|
30
|
+
query,
|
|
31
|
+
...(limit !== undefined ? { limit } : {}),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
writeJson(data);
|
|
36
|
+
}
|