@meistrari/tela-cli 0.7.58 → 0.8.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 +24 -3
- package/dist/commands/auth.js +131 -0
- package/dist/commands/context.js +118 -0
- package/dist/commands/doctor-auth.js +41 -0
- package/dist/commands/env.js +40 -0
- package/dist/commands/ingredients.js +824 -0
- package/dist/credentials.js +7 -0
- package/dist/docs/workflow-code/api-reference.md +434 -0
- package/dist/docs/workflow-code/cli.md +187 -0
- package/dist/docs/workflow-code/config.md +69 -0
- package/dist/docs/workflow-code/guides.md +56 -0
- package/dist/docs/workflow-code/installation.md +33 -0
- package/dist/docs/workflow-code/manifest.json +50 -0
- package/dist/docs/workflow-code/overview.md +14 -0
- package/dist/docs/workflow-code/quick-start.md +122 -0
- package/dist/index.js +37553 -19184
- package/dist/output.js +18 -0
- package/dist/program.js +96 -0
- package/package.json +5 -2
- package/scripts/install-skills.cjs +120 -111
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runTelactlCommand = runTelactlCommand;
|
|
4
|
+
exports.runIngredientEnvCommand = runIngredientEnvCommand;
|
|
5
|
+
exports.fetchIngredientCatalog = fetchIngredientCatalog;
|
|
6
|
+
exports.joinUrl = joinUrl;
|
|
7
|
+
exports.parseHttpieArg = parseHttpieArg;
|
|
8
|
+
exports.extractGitHubFilePath = extractGitHubFilePath;
|
|
9
|
+
exports.resolveGuidePath = resolveGuidePath;
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const tela_local_config_1 = require("@meistrari/tela-local-config");
|
|
14
|
+
const credentials_1 = require("../credentials");
|
|
15
|
+
const output_1 = require("../output");
|
|
16
|
+
const PANTRY_BASE_URL = 'https://pantry-api.tela.tools';
|
|
17
|
+
const INGREDIENT_PAGE_LIMIT = 100;
|
|
18
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
19
|
+
const FALLBACK_ENVIRONMENT_ORDER = ['tooling', 'production', 'staging', 'development'];
|
|
20
|
+
const PANTRY_INGREDIENT = {
|
|
21
|
+
slug: 'pantry',
|
|
22
|
+
name: 'Pantry',
|
|
23
|
+
description: 'Ingredient registry and API catalog',
|
|
24
|
+
type: 'api',
|
|
25
|
+
status: 'active',
|
|
26
|
+
environments: [{ slug: 'production', url: PANTRY_BASE_URL }],
|
|
27
|
+
};
|
|
28
|
+
async function runTelactlCommand(command, args, options, io) {
|
|
29
|
+
try {
|
|
30
|
+
if (command === 'update')
|
|
31
|
+
return await updateIngredients(args, options, io);
|
|
32
|
+
if (command === 'api')
|
|
33
|
+
return await callIngredientApi(args, options, io);
|
|
34
|
+
if (command === 'docs')
|
|
35
|
+
return await runDocsCommand(args, options, io);
|
|
36
|
+
if (command === 'whoami')
|
|
37
|
+
return await whoami(args, options, io);
|
|
38
|
+
if (command === 'token')
|
|
39
|
+
return await tokenAlias(args, options, io);
|
|
40
|
+
(0, output_1.writeError)(io, `Unknown telactl command: ${command}`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
(0, output_1.writeError)(io, error instanceof Error ? error.message : String(error));
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runIngredientEnvCommand(args, options, io) {
|
|
49
|
+
const [action, ...rest] = args;
|
|
50
|
+
if (action === 'set')
|
|
51
|
+
return await setIngredientEnvironment(rest, io);
|
|
52
|
+
if (action === 'list' && rest.some(arg => !arg.startsWith('-')))
|
|
53
|
+
return await listIngredientEnvironments(rest, options, io);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async function updateIngredients(args, options, io) {
|
|
57
|
+
const flags = parseCommonFlags(args);
|
|
58
|
+
if ('error' in flags)
|
|
59
|
+
return usageError(flags.error, io);
|
|
60
|
+
if (flags.rest.length > 0)
|
|
61
|
+
return usageError(`Unknown update argument: ${flags.rest[0]}`, io);
|
|
62
|
+
const session = await loadSession(flags.contextId);
|
|
63
|
+
const cache = {
|
|
64
|
+
updatedAt: new Date().toISOString(),
|
|
65
|
+
ingredients: await fetchIngredientCatalog(requestHeaders(session.credential)),
|
|
66
|
+
};
|
|
67
|
+
await writeCache(cache);
|
|
68
|
+
if (options.json) {
|
|
69
|
+
(0, output_1.writeJson)(io, {
|
|
70
|
+
cache: {
|
|
71
|
+
updatedAt: cache.updatedAt,
|
|
72
|
+
ingredientCount: cache.ingredients.length,
|
|
73
|
+
contextId: session.resolved.id,
|
|
74
|
+
refreshed: session.refreshed,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
(0, output_1.writeLine)(io, `Cached ${cache.ingredients.length} ingredients`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
async function callIngredientApi(args, _options, io) {
|
|
83
|
+
const flags = parseApiFlags(args);
|
|
84
|
+
if ('error' in flags)
|
|
85
|
+
return usageError(flags.error, io);
|
|
86
|
+
const [slug, method, routePath, ...httpieArgs] = flags.rest;
|
|
87
|
+
if (!slug || !method || !routePath) {
|
|
88
|
+
return usageError('Usage: tela api <slug> <METHOD> <PATH> [args...] [--env <slug>] [--body <json>] [-p|--pretty]', io);
|
|
89
|
+
}
|
|
90
|
+
if (!HTTP_METHODS.has(method.toUpperCase()))
|
|
91
|
+
return usageError(`Unsupported HTTP method: ${method}`, io);
|
|
92
|
+
const ingredient = requireIngredient(slug, 'api');
|
|
93
|
+
const environment = resolveIngredientEnvironment(ingredient, flags.env);
|
|
94
|
+
if (!environment) {
|
|
95
|
+
return usageError(`Ingredient '${slug}' has no environments configured. Run \`tela update\` after Pantry is updated.`, io);
|
|
96
|
+
}
|
|
97
|
+
const headers = new Headers();
|
|
98
|
+
const queryParams = [];
|
|
99
|
+
const fields = {};
|
|
100
|
+
let hasFields = false;
|
|
101
|
+
for (const arg of httpieArgs) {
|
|
102
|
+
const parsed = parseHttpieArg(arg);
|
|
103
|
+
if (!parsed)
|
|
104
|
+
return usageError(`Invalid argument '${arg}'. Use Key:Value, name=value, name==value, or name:=rawJson.`, io);
|
|
105
|
+
if (parsed.kind === 'query') {
|
|
106
|
+
queryParams.push([parsed.key, parsed.value]);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (parsed.kind === 'header') {
|
|
110
|
+
headers.set(parsed.key.trim(), parsed.value.trim());
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (parsed.kind === 'field') {
|
|
114
|
+
fields[parsed.key] = parsed.value;
|
|
115
|
+
hasFields = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
fields[parsed.key] = JSON.parse(parsed.value);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return usageError(`Invalid JSON for raw field '${parsed.key}': ${parsed.value}`, io);
|
|
123
|
+
}
|
|
124
|
+
hasFields = true;
|
|
125
|
+
}
|
|
126
|
+
const url = new URL(joinUrl(environment.url, routePath));
|
|
127
|
+
for (const [key, value] of queryParams)
|
|
128
|
+
url.searchParams.append(key, value);
|
|
129
|
+
const session = await loadSession(flags.contextId);
|
|
130
|
+
for (const [key, value] of Object.entries(requestHeaders(session.credential))) {
|
|
131
|
+
if (!headers.has(key))
|
|
132
|
+
headers.set(key, value);
|
|
133
|
+
}
|
|
134
|
+
if (!headers.has('Accept'))
|
|
135
|
+
headers.set('Accept', 'application/json');
|
|
136
|
+
let requestBody;
|
|
137
|
+
if (flags.body !== undefined) {
|
|
138
|
+
requestBody = flags.body;
|
|
139
|
+
}
|
|
140
|
+
else if (hasFields) {
|
|
141
|
+
requestBody = JSON.stringify(fields);
|
|
142
|
+
}
|
|
143
|
+
if (requestBody !== undefined && !headers.has('Content-Type'))
|
|
144
|
+
headers.set('Content-Type', 'application/json');
|
|
145
|
+
const response = await fetch(url, {
|
|
146
|
+
method: method.toUpperCase(),
|
|
147
|
+
headers,
|
|
148
|
+
body: requestBody,
|
|
149
|
+
});
|
|
150
|
+
const text = await response.text();
|
|
151
|
+
assertHttpOk(response, text);
|
|
152
|
+
if (flags.pretty) {
|
|
153
|
+
const parsed = tryParseJson(text);
|
|
154
|
+
if (parsed !== null) {
|
|
155
|
+
(0, output_1.writeJson)(io, parsed);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
(0, output_1.writeLine)(io, text);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
async function runDocsCommand(args, options, io) {
|
|
163
|
+
const [action, ...rest] = args;
|
|
164
|
+
if (!action || action === 'help' || action === '--help' || action === '-h') {
|
|
165
|
+
writeDocsHelp(io);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
if (action === 'endpoints')
|
|
169
|
+
return await docsEndpoints(rest, options, io);
|
|
170
|
+
if (action === 'reference')
|
|
171
|
+
return await docsReference(rest, options, io);
|
|
172
|
+
if (action === 'guide')
|
|
173
|
+
return await docsGuide(rest, options, io);
|
|
174
|
+
return usageError(`Unknown docs command: ${action}`, io);
|
|
175
|
+
}
|
|
176
|
+
async function docsEndpoints(args, options, io) {
|
|
177
|
+
const flags = parseCommonFlags(args);
|
|
178
|
+
if ('error' in flags)
|
|
179
|
+
return usageError(flags.error, io);
|
|
180
|
+
const [slug] = flags.rest;
|
|
181
|
+
if (!slug)
|
|
182
|
+
return usageError('Usage: tela docs endpoints <slug> [-p|--pretty]', io);
|
|
183
|
+
requireIngredient(slug, 'api');
|
|
184
|
+
const spec = await fetchOpenApiSpec(slug, flags.contextId);
|
|
185
|
+
const paths = (isRecord(spec.paths) ? spec.paths : {});
|
|
186
|
+
if (flags.pretty && !options.json) {
|
|
187
|
+
(0, output_1.writeLine)(io, formatEndpointsTable(paths));
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
(0, output_1.writeJson)(io, paths);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
async function docsReference(args, options, io) {
|
|
194
|
+
const flags = parseCommonFlags(args);
|
|
195
|
+
if ('error' in flags)
|
|
196
|
+
return usageError(flags.error, io);
|
|
197
|
+
const [slug, first, second] = flags.rest;
|
|
198
|
+
if (!slug)
|
|
199
|
+
return usageError('Usage: tela docs reference <slug> [METHOD PATH|file-path] [-p|--pretty]', io);
|
|
200
|
+
const ingredient = requireIngredient(slug);
|
|
201
|
+
if (ingredient.type === 'api') {
|
|
202
|
+
if (first && !second && HTTP_METHODS.has(first.toUpperCase())) {
|
|
203
|
+
return usageError('Both METHOD and PATH are required to show a specific endpoint', io);
|
|
204
|
+
}
|
|
205
|
+
const spec = await fetchOpenApiSpec(slug, flags.contextId);
|
|
206
|
+
if (first && second) {
|
|
207
|
+
if (!HTTP_METHODS.has(first.toUpperCase()))
|
|
208
|
+
return usageError(`Unsupported HTTP method: ${first}`, io);
|
|
209
|
+
const pathEntry = isRecord(spec.paths) && isRecord(spec.paths[second]) ? spec.paths[second] : null;
|
|
210
|
+
if (!pathEntry)
|
|
211
|
+
return usageError(`Path not found: ${second}`, io);
|
|
212
|
+
const op = pathEntry[first.toLowerCase()];
|
|
213
|
+
if (!op)
|
|
214
|
+
return usageError(`Method ${first.toUpperCase()} not found for ${second}`, io);
|
|
215
|
+
if (flags.pretty && !options.json) {
|
|
216
|
+
(0, output_1.writeLine)(io, formatEndpoint(spec, op, first, second));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
(0, output_1.writeJson)(io, op);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
if (flags.pretty && !options.json) {
|
|
223
|
+
(0, output_1.writeJson)(io, spec);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
(0, output_1.writeLine)(io, JSON.stringify(spec));
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
if (ingredient.type !== 'library')
|
|
230
|
+
return usageError(`Ingredient '${slug}' is not an API or library ingredient.`, io);
|
|
231
|
+
const ingredientBody = await fetchIngredient(slug, flags.contextId);
|
|
232
|
+
const apiRef = isRecord(ingredientBody.docs) ? stringValue(ingredientBody.docs.api) : '';
|
|
233
|
+
if (!apiRef)
|
|
234
|
+
return usageError(`Library ${slug} has no API reference`, io);
|
|
235
|
+
const filePath = extractGitHubFilePath(apiRef);
|
|
236
|
+
if (!filePath)
|
|
237
|
+
return usageError(`Could not extract file path from API reference URL: ${apiRef}`, io);
|
|
238
|
+
const slashIndex = filePath.lastIndexOf('/');
|
|
239
|
+
const dir = slashIndex === -1 ? '' : filePath.slice(0, slashIndex + 1);
|
|
240
|
+
const relativePath = first ? first.replace(/^\/+/, '') : 'globals.md';
|
|
241
|
+
const guide = await fetchGuide(slug, `${dir}${relativePath}`, flags.contextId);
|
|
242
|
+
(0, output_1.writeLine)(io, stringValue(guide.content));
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
async function docsGuide(args, _options, io) {
|
|
246
|
+
const flags = parseCommonFlags(args);
|
|
247
|
+
if ('error' in flags)
|
|
248
|
+
return usageError(flags.error, io);
|
|
249
|
+
const [slug, providedPath] = flags.rest;
|
|
250
|
+
if (!slug)
|
|
251
|
+
return usageError('Usage: tela docs guide <slug> [path] [-p|--pretty]', io);
|
|
252
|
+
const ingredient = requireIngredient(slug);
|
|
253
|
+
if (ingredient.type !== 'api' && ingredient.type !== 'library') {
|
|
254
|
+
return usageError(`Ingredient '${slug}' is not an API or library ingredient.`, io);
|
|
255
|
+
}
|
|
256
|
+
const ingredientBody = await fetchIngredient(slug, flags.contextId);
|
|
257
|
+
const guidesRef = isRecord(ingredientBody.docs) ? stringValue(ingredientBody.docs.guides) : '';
|
|
258
|
+
if (!guidesRef)
|
|
259
|
+
return usageError(`Ingredient ${slug} has no guide`, io);
|
|
260
|
+
const filePath = extractGitHubFilePath(guidesRef);
|
|
261
|
+
if (!filePath)
|
|
262
|
+
return usageError(`Could not extract file path from guide URL: ${guidesRef}`, io);
|
|
263
|
+
const guide = await fetchGuide(slug, resolveGuidePath(filePath, providedPath), flags.contextId);
|
|
264
|
+
(0, output_1.writeLine)(io, stringValue(guide.content));
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
async function listIngredientEnvironments(args, options, io) {
|
|
268
|
+
const flags = parseCommonFlags(args);
|
|
269
|
+
if ('error' in flags)
|
|
270
|
+
return usageError(flags.error, io);
|
|
271
|
+
const [slug] = flags.rest;
|
|
272
|
+
if (!slug)
|
|
273
|
+
return usageError('Usage: tela env list <ingredient-slug> [-p|--pretty|--json]', io);
|
|
274
|
+
const ingredient = requireIngredient(slug);
|
|
275
|
+
const environments = ingredient.environments ?? [];
|
|
276
|
+
if (options.json || !flags.pretty) {
|
|
277
|
+
(0, output_1.writeJson)(io, { environments });
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
if (environments.length === 0) {
|
|
281
|
+
(0, output_1.writeLine)(io, '(no environments configured)');
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
const slugWidth = Math.max(...environments.map(environment => environment.slug.length));
|
|
285
|
+
for (const environment of environments) {
|
|
286
|
+
(0, output_1.writeLine)(io, `${environment.slug.padEnd(slugWidth)} ${environment.url}`);
|
|
287
|
+
}
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
async function setIngredientEnvironment(args, io) {
|
|
291
|
+
const [slug, environment] = args;
|
|
292
|
+
if (!slug || !environment)
|
|
293
|
+
return usageError('Usage: tela env set <ingredient-slug> <environment>', io);
|
|
294
|
+
const ingredient = requireIngredient(slug);
|
|
295
|
+
if (!ingredient.environments.some(candidate => candidate.slug === environment)) {
|
|
296
|
+
const available = ingredient.environments.map(candidate => candidate.slug).join(', ') || '(none)';
|
|
297
|
+
return usageError(`Environment '${environment}' not found for '${slug}'. Available: ${available}`, io);
|
|
298
|
+
}
|
|
299
|
+
const defaults = await readEnvironmentDefaults();
|
|
300
|
+
defaults[slug] = environment;
|
|
301
|
+
await writeEnvironmentDefaults(defaults);
|
|
302
|
+
(0, output_1.writeLine)(io, `Set default environment for '${slug}' to '${environment}'`);
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
async function whoami(args, _options, io) {
|
|
306
|
+
const flags = parseCommonFlags(args);
|
|
307
|
+
if ('error' in flags)
|
|
308
|
+
return usageError(flags.error, io);
|
|
309
|
+
if (flags.rest.length > 0)
|
|
310
|
+
return usageError(`Unknown whoami argument: ${flags.rest[0]}`, io);
|
|
311
|
+
const session = await loadSession(flags.contextId);
|
|
312
|
+
if (!session.credential.accessToken) {
|
|
313
|
+
return usageError(`Context ${session.resolved.id} does not have an access token credential.`, io);
|
|
314
|
+
}
|
|
315
|
+
const response = await fetch(new URL('/api/auth/applications/whoami', session.resolved.environment.authApiUrl), {
|
|
316
|
+
headers: {
|
|
317
|
+
'Accept': 'application/json',
|
|
318
|
+
'x-tela-access-token': session.credential.accessToken,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const text = await response.text();
|
|
322
|
+
assertHttpOk(response, text);
|
|
323
|
+
if (flags.pretty) {
|
|
324
|
+
const parsed = tryParseJson(text);
|
|
325
|
+
if (parsed !== null) {
|
|
326
|
+
(0, output_1.writeJson)(io, parsed);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
(0, output_1.writeLine)(io, text);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
async function tokenAlias(args, _options, io) {
|
|
334
|
+
const flags = parseCommonFlags(args);
|
|
335
|
+
if ('error' in flags)
|
|
336
|
+
return usageError(flags.error, io);
|
|
337
|
+
if (flags.rest.length > 0)
|
|
338
|
+
return usageError(`Unknown token argument: ${flags.rest[0]}`, io);
|
|
339
|
+
const session = await loadSession(flags.contextId);
|
|
340
|
+
if (!session.credential.accessToken)
|
|
341
|
+
return usageError(`Context ${session.resolved.id} does not have an access token credential.`, io);
|
|
342
|
+
(0, output_1.writeLine)(io, session.credential.accessToken);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
async function fetchOpenApiSpec(slug, contextId) {
|
|
346
|
+
const session = await loadSession(contextId);
|
|
347
|
+
const response = await fetch(getPantryOpenapiUrl(slug), {
|
|
348
|
+
headers: requestHeaders(session.credential),
|
|
349
|
+
});
|
|
350
|
+
const text = await response.text();
|
|
351
|
+
assertHttpOk(response, text, 'Failed to fetch OpenAPI spec');
|
|
352
|
+
const parsed = JSON.parse(text);
|
|
353
|
+
if (!isRecord(parsed))
|
|
354
|
+
throw new Error('OpenAPI response was not a JSON object');
|
|
355
|
+
return parsed;
|
|
356
|
+
}
|
|
357
|
+
async function fetchIngredient(slug, contextId) {
|
|
358
|
+
const session = await loadSession(contextId);
|
|
359
|
+
const response = await fetch(getPantryIngredientUrl(slug), {
|
|
360
|
+
headers: requestHeaders(session.credential),
|
|
361
|
+
});
|
|
362
|
+
const text = await response.text();
|
|
363
|
+
assertHttpOk(response, text, 'Failed to fetch ingredient');
|
|
364
|
+
const parsed = JSON.parse(text);
|
|
365
|
+
if (!isRecord(parsed))
|
|
366
|
+
throw new Error('Ingredient response was not a JSON object');
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
async function fetchGuide(slug, filePath, contextId) {
|
|
370
|
+
const session = await loadSession(contextId);
|
|
371
|
+
const response = await fetch(getPantryGuideUrl(slug, filePath), {
|
|
372
|
+
headers: requestHeaders(session.credential),
|
|
373
|
+
});
|
|
374
|
+
const text = await response.text();
|
|
375
|
+
assertHttpOk(response, text, 'Failed to fetch guide');
|
|
376
|
+
const parsed = JSON.parse(text);
|
|
377
|
+
if (!isRecord(parsed))
|
|
378
|
+
throw new Error('Guide response was not a JSON object');
|
|
379
|
+
return parsed;
|
|
380
|
+
}
|
|
381
|
+
async function loadSession(contextId) {
|
|
382
|
+
const config = await (0, tela_local_config_1.loadConfig)();
|
|
383
|
+
const resolved = (0, tela_local_config_1.resolveContext)(config, { contextId });
|
|
384
|
+
const credentials = (0, credentials_1.createCliCredentialAdapter)();
|
|
385
|
+
const credentialRef = resolved.context.credentialRef;
|
|
386
|
+
const originalCredential = await credentials.get(credentialRef);
|
|
387
|
+
if (!originalCredential)
|
|
388
|
+
throw new Error(`Missing credential for Tela context ${resolved.id}`);
|
|
389
|
+
const credential = originalCredential.kind === 'user-session'
|
|
390
|
+
? await (0, tela_local_config_1.refreshUserSessionCredential)(resolved.environment, credentialRef, originalCredential, credentials)
|
|
391
|
+
: originalCredential;
|
|
392
|
+
return {
|
|
393
|
+
resolved,
|
|
394
|
+
credentialRef,
|
|
395
|
+
credential,
|
|
396
|
+
refreshed: credential !== originalCredential,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function requestHeaders(credential) {
|
|
400
|
+
if (credential.kind === 'data-token' && credential.dataToken) {
|
|
401
|
+
return {
|
|
402
|
+
'Accept': 'application/json',
|
|
403
|
+
'x-data-token': credential.dataToken,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (credential.accessToken) {
|
|
407
|
+
return {
|
|
408
|
+
Accept: 'application/json',
|
|
409
|
+
Authorization: `Bearer ${credential.accessToken}`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
throw new Error(`Credential ${credential.kind} does not contain usable token material`);
|
|
413
|
+
}
|
|
414
|
+
function requireIngredient(slug, type) {
|
|
415
|
+
const ingredient = readModules().find(candidate => candidate.slug === slug);
|
|
416
|
+
if (!ingredient)
|
|
417
|
+
throw new Error(`Ingredient '${slug}' not found. Run \`tela update\` first.`);
|
|
418
|
+
if (type && ingredient.type !== type)
|
|
419
|
+
throw new Error(`Ingredient '${slug}' is not a ${type} ingredient.`);
|
|
420
|
+
return ingredient;
|
|
421
|
+
}
|
|
422
|
+
async function fetchIngredientCatalog(headers, fetchImpl = fetch) {
|
|
423
|
+
const ingredients = [];
|
|
424
|
+
let total = null;
|
|
425
|
+
for (let offset = 0;; offset += INGREDIENT_PAGE_LIMIT) {
|
|
426
|
+
const url = new URL('/ingredients', PANTRY_BASE_URL);
|
|
427
|
+
url.searchParams.set('limit', String(INGREDIENT_PAGE_LIMIT));
|
|
428
|
+
url.searchParams.set('offset', String(offset));
|
|
429
|
+
const response = await fetchImpl(url.toString(), { headers });
|
|
430
|
+
const text = await response.text();
|
|
431
|
+
assertHttpOk(response, text, 'Failed to fetch ingredients');
|
|
432
|
+
const data = JSON.parse(text);
|
|
433
|
+
const rawPageIngredients = data.ingredients ?? [];
|
|
434
|
+
const pageIngredients = rawPageIngredients.map(normalizeCachedIngredient).filter(ingredient => ingredient.slug);
|
|
435
|
+
ingredients.push(...pageIngredients);
|
|
436
|
+
total = typeof data.total === 'number' ? data.total : total;
|
|
437
|
+
if (total !== null && ingredients.length >= total)
|
|
438
|
+
break;
|
|
439
|
+
if (rawPageIngredients.length < INGREDIENT_PAGE_LIMIT)
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
return ingredients;
|
|
443
|
+
}
|
|
444
|
+
function normalizeCachedIngredient(ingredient) {
|
|
445
|
+
return {
|
|
446
|
+
slug: stringValue(ingredient.slug),
|
|
447
|
+
name: stringValue(ingredient.name),
|
|
448
|
+
description: stringValue(ingredient.description),
|
|
449
|
+
type: normalizeIngredientType(ingredient.type),
|
|
450
|
+
status: stringValue(ingredient.status),
|
|
451
|
+
environments: (ingredient.environments ?? []).map(environment => ({
|
|
452
|
+
slug: environment.slug,
|
|
453
|
+
url: environment.url,
|
|
454
|
+
})).filter(environment => environment.slug && environment.url),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function readModules() {
|
|
458
|
+
const cache = readCache();
|
|
459
|
+
const ingredients = cache?.ingredients ?? [];
|
|
460
|
+
return ingredients.some(ingredient => ingredient.slug === PANTRY_INGREDIENT.slug)
|
|
461
|
+
? ingredients
|
|
462
|
+
: [...ingredients, PANTRY_INGREDIENT];
|
|
463
|
+
}
|
|
464
|
+
function readCache() {
|
|
465
|
+
try {
|
|
466
|
+
const text = (0, node_fs_1.readFileSync)(cachePath(), 'utf8');
|
|
467
|
+
const parsed = JSON.parse(text);
|
|
468
|
+
if (!Array.isArray(parsed.ingredients))
|
|
469
|
+
return null;
|
|
470
|
+
return parsed;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function writeCache(cache) {
|
|
477
|
+
const filePath = cachePath();
|
|
478
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
479
|
+
await (0, promises_1.writeFile)(filePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
480
|
+
}
|
|
481
|
+
function cachePath() {
|
|
482
|
+
return (0, node_path_1.join)((0, tela_local_config_1.getTelaLocalPaths)().rootDir, 'ingredients.json');
|
|
483
|
+
}
|
|
484
|
+
async function readEnvironmentDefaults() {
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(await (0, promises_1.readFile)(environmentDefaultsPath(), 'utf8'));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function writeEnvironmentDefaults(defaults) {
|
|
493
|
+
const filePath = environmentDefaultsPath();
|
|
494
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
495
|
+
await (0, promises_1.writeFile)(filePath, `${JSON.stringify(defaults, null, 2)}\n`, 'utf8');
|
|
496
|
+
}
|
|
497
|
+
function environmentDefaultsPath() {
|
|
498
|
+
return (0, node_path_1.join)((0, tela_local_config_1.getTelaLocalPaths)().rootDir, 'ingredient-environments.json');
|
|
499
|
+
}
|
|
500
|
+
function resolveIngredientEnvironment(ingredient, override) {
|
|
501
|
+
const environments = ingredient.environments ?? [];
|
|
502
|
+
if (override) {
|
|
503
|
+
const found = environments.find(environment => environment.slug === override);
|
|
504
|
+
if (!found) {
|
|
505
|
+
const available = environments.map(environment => environment.slug).join(', ') || '(none)';
|
|
506
|
+
throw new Error(`Environment '${override}' not found for ingredient '${ingredient.slug}'. Available: ${available}`);
|
|
507
|
+
}
|
|
508
|
+
return found;
|
|
509
|
+
}
|
|
510
|
+
if (environments.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
if (environments.length === 1)
|
|
513
|
+
return environments[0] ?? null;
|
|
514
|
+
const defaults = readEnvironmentDefaultsSync();
|
|
515
|
+
const saved = defaults[ingredient.slug];
|
|
516
|
+
if (saved) {
|
|
517
|
+
const found = environments.find(environment => environment.slug === saved);
|
|
518
|
+
if (found)
|
|
519
|
+
return found;
|
|
520
|
+
}
|
|
521
|
+
for (const preferred of FALLBACK_ENVIRONMENT_ORDER) {
|
|
522
|
+
const found = environments.find(environment => environment.slug === preferred);
|
|
523
|
+
if (found)
|
|
524
|
+
return found;
|
|
525
|
+
}
|
|
526
|
+
const available = environments.map(environment => environment.slug).join(', ');
|
|
527
|
+
throw new Error(`Multiple environments configured for '${ingredient.slug}' and no default matches.\n`
|
|
528
|
+
+ `Available: ${available}\n`
|
|
529
|
+
+ `Use --env <slug> to pick one, or run \`tela env set ${ingredient.slug} <environment>\` to save a default.`);
|
|
530
|
+
}
|
|
531
|
+
function readEnvironmentDefaultsSync() {
|
|
532
|
+
try {
|
|
533
|
+
return JSON.parse((0, node_fs_1.readFileSync)(environmentDefaultsPath(), 'utf8'));
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function parseCommonFlags(args) {
|
|
540
|
+
const flags = {
|
|
541
|
+
pretty: false,
|
|
542
|
+
rest: [],
|
|
543
|
+
};
|
|
544
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
545
|
+
const arg = args[index];
|
|
546
|
+
if (arg === undefined)
|
|
547
|
+
continue;
|
|
548
|
+
if (arg === '--context') {
|
|
549
|
+
const value = args[index + 1];
|
|
550
|
+
if (!value)
|
|
551
|
+
return { error: 'Expected value after --context' };
|
|
552
|
+
flags.contextId = value;
|
|
553
|
+
index += 1;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (arg === '--pretty' || arg === '-p') {
|
|
557
|
+
flags.pretty = true;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
flags.rest.push(arg);
|
|
561
|
+
}
|
|
562
|
+
return flags;
|
|
563
|
+
}
|
|
564
|
+
function parseApiFlags(args) {
|
|
565
|
+
const flags = {
|
|
566
|
+
pretty: false,
|
|
567
|
+
rest: [],
|
|
568
|
+
};
|
|
569
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
570
|
+
const arg = args[index];
|
|
571
|
+
if (arg === undefined)
|
|
572
|
+
continue;
|
|
573
|
+
if (arg === '--context') {
|
|
574
|
+
const value = args[index + 1];
|
|
575
|
+
if (!value)
|
|
576
|
+
return { error: 'Expected value after --context' };
|
|
577
|
+
flags.contextId = value;
|
|
578
|
+
index += 1;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (arg === '--env') {
|
|
582
|
+
const value = args[index + 1];
|
|
583
|
+
if (!value)
|
|
584
|
+
return { error: 'Expected value after --env' };
|
|
585
|
+
flags.env = value;
|
|
586
|
+
index += 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (arg === '--body') {
|
|
590
|
+
const value = args[index + 1];
|
|
591
|
+
if (value === undefined)
|
|
592
|
+
return { error: 'Expected value after --body' };
|
|
593
|
+
flags.body = value;
|
|
594
|
+
index += 1;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (arg === '--pretty' || arg === '-p') {
|
|
598
|
+
flags.pretty = true;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
flags.rest.push(arg);
|
|
602
|
+
}
|
|
603
|
+
return flags;
|
|
604
|
+
}
|
|
605
|
+
function joinUrl(base, routePath) {
|
|
606
|
+
const trimmedBase = base.replace(/\/$/, '');
|
|
607
|
+
const prefixedPath = routePath.startsWith('/') ? routePath : `/${routePath}`;
|
|
608
|
+
return `${trimmedBase}${prefixedPath}`;
|
|
609
|
+
}
|
|
610
|
+
function parseHttpieArg(arg) {
|
|
611
|
+
for (let index = 0; index < arg.length; index += 1) {
|
|
612
|
+
const two = arg.slice(index, index + 2);
|
|
613
|
+
if (two === '==')
|
|
614
|
+
return { kind: 'query', key: arg.slice(0, index), value: arg.slice(index + 2) };
|
|
615
|
+
if (two === ':=')
|
|
616
|
+
return { kind: 'rawField', key: arg.slice(0, index), value: arg.slice(index + 2) };
|
|
617
|
+
if (arg[index] === ':')
|
|
618
|
+
return { kind: 'header', key: arg.slice(0, index), value: arg.slice(index + 1) };
|
|
619
|
+
if (arg[index] === '=')
|
|
620
|
+
return { kind: 'field', key: arg.slice(0, index), value: arg.slice(index + 1) };
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
function getPantryOpenapiUrl(slug) {
|
|
625
|
+
return slug === 'pantry' ? `${PANTRY_BASE_URL}/openapi` : `${PANTRY_BASE_URL}/ingredients/${slug}/openapi`;
|
|
626
|
+
}
|
|
627
|
+
function getPantryIngredientUrl(slug) {
|
|
628
|
+
return `${PANTRY_BASE_URL}/ingredients/${slug}`;
|
|
629
|
+
}
|
|
630
|
+
function getPantryGuideUrl(slug, filePath) {
|
|
631
|
+
const base = `${PANTRY_BASE_URL}/ingredients/${slug}/guide`;
|
|
632
|
+
if (!filePath)
|
|
633
|
+
return base;
|
|
634
|
+
const params = new URLSearchParams({ path: filePath });
|
|
635
|
+
return `${base}?${params.toString()}`;
|
|
636
|
+
}
|
|
637
|
+
function formatEndpointsTable(paths) {
|
|
638
|
+
const lines = [];
|
|
639
|
+
for (const [routePath, methods] of Object.entries(paths)) {
|
|
640
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
641
|
+
if (method.startsWith('x-'))
|
|
642
|
+
continue;
|
|
643
|
+
const summary = op.summary ?? op.operationId ?? '';
|
|
644
|
+
lines.push(`${method.toUpperCase().padEnd(7)} ${routePath} ${summary}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return lines.join('\n');
|
|
648
|
+
}
|
|
649
|
+
function formatEndpoint(spec, op, method, routePath) {
|
|
650
|
+
if (!isRecord(op))
|
|
651
|
+
return '';
|
|
652
|
+
const lines = [`${method.toUpperCase()} ${routePath}`];
|
|
653
|
+
if (typeof op.summary === 'string')
|
|
654
|
+
lines.push(`\n${op.summary}`);
|
|
655
|
+
if (typeof op.description === 'string' && op.description !== op.summary)
|
|
656
|
+
lines.push(op.description);
|
|
657
|
+
const parameters = Array.isArray(op.parameters) ? op.parameters : [];
|
|
658
|
+
if (parameters.length > 0) {
|
|
659
|
+
lines.push('\n## Parameters\n');
|
|
660
|
+
for (const parameter of parameters) {
|
|
661
|
+
const resolved = isRecord(parameter) && typeof parameter.$ref === 'string' ? resolveRef(spec, parameter.$ref) : parameter;
|
|
662
|
+
if (!isRecord(resolved))
|
|
663
|
+
continue;
|
|
664
|
+
const required = resolved.required ? ' (required)' : '';
|
|
665
|
+
const description = typeof resolved.description === 'string' ? ` - ${resolved.description}` : '';
|
|
666
|
+
const schema = isRecord(resolved.schema) ? resolved.schema : {};
|
|
667
|
+
const type = typeof schema.type === 'string' ? schema.type : '';
|
|
668
|
+
lines.push(` ${stringValue(resolved.in)} ${stringValue(resolved.name)}: ${type}${required}${description}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (isRecord(op.requestBody)) {
|
|
672
|
+
lines.push('\n## Request Body\n');
|
|
673
|
+
const content = isRecord(op.requestBody.content) ? op.requestBody.content : {};
|
|
674
|
+
for (const [contentType, media] of Object.entries(content)) {
|
|
675
|
+
if (!isRecord(media))
|
|
676
|
+
continue;
|
|
677
|
+
lines.push(` Content-Type: ${contentType}`);
|
|
678
|
+
const schema = resolveSchema(spec, media.schema);
|
|
679
|
+
if (schema)
|
|
680
|
+
lines.push(formatSchema(schema, 1));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (isRecord(op.responses)) {
|
|
684
|
+
lines.push('\n## Responses\n');
|
|
685
|
+
for (const [status, response] of Object.entries(op.responses)) {
|
|
686
|
+
const resolved = isRecord(response) && typeof response.$ref === 'string' ? resolveRef(spec, response.$ref) : response;
|
|
687
|
+
if (!isRecord(resolved))
|
|
688
|
+
continue;
|
|
689
|
+
lines.push(` ${status}: ${stringValue(resolved.description)}`);
|
|
690
|
+
const content = isRecord(resolved.content) ? resolved.content : {};
|
|
691
|
+
for (const media of Object.values(content)) {
|
|
692
|
+
if (!isRecord(media))
|
|
693
|
+
continue;
|
|
694
|
+
const schema = resolveSchema(spec, media.schema);
|
|
695
|
+
if (schema)
|
|
696
|
+
lines.push(formatSchema(schema, 2));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return lines.join('\n');
|
|
701
|
+
}
|
|
702
|
+
function resolveRef(spec, ref) {
|
|
703
|
+
const parts = ref.replace('#/', '').split('/');
|
|
704
|
+
let current = spec;
|
|
705
|
+
for (const part of parts) {
|
|
706
|
+
current = isRecord(current) ? current[part] : undefined;
|
|
707
|
+
}
|
|
708
|
+
return current;
|
|
709
|
+
}
|
|
710
|
+
function resolveSchema(spec, schema) {
|
|
711
|
+
if (!isRecord(schema))
|
|
712
|
+
return null;
|
|
713
|
+
if (typeof schema.$ref === 'string')
|
|
714
|
+
return resolveSchema(spec, resolveRef(spec, schema.$ref));
|
|
715
|
+
if (schema.items)
|
|
716
|
+
return { ...schema, items: resolveSchema(spec, schema.items) };
|
|
717
|
+
if (isRecord(schema.properties)) {
|
|
718
|
+
const resolved = {};
|
|
719
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
720
|
+
resolved[key] = resolveSchema(spec, value) ?? value;
|
|
721
|
+
}
|
|
722
|
+
return { ...schema, properties: resolved };
|
|
723
|
+
}
|
|
724
|
+
return schema;
|
|
725
|
+
}
|
|
726
|
+
function formatSchema(schema, indent = 0) {
|
|
727
|
+
if (!schema)
|
|
728
|
+
return '';
|
|
729
|
+
const pad = ' '.repeat(indent);
|
|
730
|
+
if (schema.type === 'object' && isRecord(schema.properties)) {
|
|
731
|
+
const requiredFields = Array.isArray(schema.required) ? schema.required.filter(item => typeof item === 'string') : [];
|
|
732
|
+
return Object.entries(schema.properties).map(([key, prop]) => {
|
|
733
|
+
if (!isRecord(prop))
|
|
734
|
+
return `${pad}${key}: unknown`;
|
|
735
|
+
const required = requiredFields.includes(key) ? '' : '?';
|
|
736
|
+
const type = typeof prop.type === 'string' ? prop.type : 'object';
|
|
737
|
+
const description = typeof prop.description === 'string' ? ` - ${prop.description}` : '';
|
|
738
|
+
if (prop.type === 'object' && isRecord(prop.properties)) {
|
|
739
|
+
return `${pad}${key}${required}: {${description}\n${formatSchema(prop, indent + 1)}\n${pad}}`;
|
|
740
|
+
}
|
|
741
|
+
if (prop.type === 'array' && isRecord(prop.items)) {
|
|
742
|
+
const itemType = typeof prop.items.type === 'string' ? prop.items.type : 'object';
|
|
743
|
+
return `${pad}${key}${required}: ${itemType}[]${description}`;
|
|
744
|
+
}
|
|
745
|
+
return `${pad}${key}${required}: ${type}${description}`;
|
|
746
|
+
}).join('\n');
|
|
747
|
+
}
|
|
748
|
+
return `${pad}${typeof schema.type === 'string' ? schema.type : 'unknown'}`;
|
|
749
|
+
}
|
|
750
|
+
function extractGitHubFilePath(url) {
|
|
751
|
+
let parsed;
|
|
752
|
+
try {
|
|
753
|
+
parsed = new URL(url);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
const parts = parsed.pathname
|
|
759
|
+
.replace(/^\/+|\/+$/g, '')
|
|
760
|
+
.split('/')
|
|
761
|
+
.filter(Boolean)
|
|
762
|
+
.map(part => decodeURIComponent(part));
|
|
763
|
+
if (parsed.hostname === 'github.com') {
|
|
764
|
+
const [owner, repo, scope, ref, ...filePathParts] = parts;
|
|
765
|
+
if (!owner || !repo || scope !== 'blob' || !ref || filePathParts.length === 0)
|
|
766
|
+
return null;
|
|
767
|
+
return filePathParts.join('/');
|
|
768
|
+
}
|
|
769
|
+
if (parsed.hostname === 'raw.githubusercontent.com') {
|
|
770
|
+
const [owner, repo, firstRefPart, ...rest] = parts;
|
|
771
|
+
if (!owner || !repo || !firstRefPart)
|
|
772
|
+
return null;
|
|
773
|
+
if (firstRefPart === 'refs') {
|
|
774
|
+
const [, , ...filePathParts] = rest;
|
|
775
|
+
return filePathParts.length > 0 ? filePathParts.join('/') : null;
|
|
776
|
+
}
|
|
777
|
+
return rest.length > 0 ? rest.join('/') : null;
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
function resolveGuidePath(rootFilePath, providedPath) {
|
|
782
|
+
const slashIndex = rootFilePath.lastIndexOf('/');
|
|
783
|
+
const dir = slashIndex === -1 ? '' : rootFilePath.slice(0, slashIndex + 1);
|
|
784
|
+
const basename = slashIndex === -1 ? rootFilePath : rootFilePath.slice(slashIndex + 1);
|
|
785
|
+
const relativePath = providedPath ? providedPath.replace(/^\/+/, '') : basename;
|
|
786
|
+
return `${dir}${relativePath}`;
|
|
787
|
+
}
|
|
788
|
+
function assertHttpOk(response, bodyText, context) {
|
|
789
|
+
if (response.ok)
|
|
790
|
+
return;
|
|
791
|
+
const prefix = context ? `${context}: ` : '';
|
|
792
|
+
throw new Error(`${prefix}${response.status} ${response.statusText}${bodyText ? `\n${bodyText}` : ''}`);
|
|
793
|
+
}
|
|
794
|
+
function usageError(message, io) {
|
|
795
|
+
(0, output_1.writeError)(io, message);
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
function writeDocsHelp(io) {
|
|
799
|
+
(0, output_1.writeLine)(io, 'Usage: tela docs <command> [args]');
|
|
800
|
+
(0, output_1.writeLine)(io);
|
|
801
|
+
(0, output_1.writeLine)(io, 'Commands:');
|
|
802
|
+
(0, output_1.writeLine)(io, ' docs endpoints <slug> [-p|--pretty] [--json]');
|
|
803
|
+
(0, output_1.writeLine)(io, ' docs reference <slug> [METHOD PATH|file-path] [-p|--pretty] [--json]');
|
|
804
|
+
(0, output_1.writeLine)(io, ' docs guide <slug> [path] [-p|--pretty]');
|
|
805
|
+
}
|
|
806
|
+
function tryParseJson(text) {
|
|
807
|
+
try {
|
|
808
|
+
return JSON.parse(text);
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function isRecord(value) {
|
|
815
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
816
|
+
}
|
|
817
|
+
function stringValue(value) {
|
|
818
|
+
return typeof value === 'string' ? value : '';
|
|
819
|
+
}
|
|
820
|
+
function normalizeIngredientType(value) {
|
|
821
|
+
if (value === 'library' || value === 'skill')
|
|
822
|
+
return value;
|
|
823
|
+
return 'api';
|
|
824
|
+
}
|