@meistrari/tela-cli 1.0.0 → 1.2.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 +21 -18
- package/dist/commands/auth.d.ts +8 -0
- package/dist/commands/auth.js +366 -4
- package/dist/commands/ingredients.d.ts +7 -35
- package/dist/commands/ingredients.js +241 -559
- package/dist/commands/skills.d.ts +33 -0
- package/dist/commands/skills.js +519 -0
- package/dist/docs/workflow-code/api-reference.md +81 -2
- package/dist/docs/workflow-code/manifest.json +1 -1
- package/dist/index.js +137617 -32063
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.js +116 -0
- package/dist/program.js +22 -14
- package/dist/session.d.ts +28 -0
- package/dist/session.js +109 -0
- package/dist/skills.d.ts +87 -0
- package/dist/skills.js +303 -0
- package/package.json +1 -1
- package/skills/tela-cli/SKILL.md +19 -11
|
@@ -1,34 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveGuidePath = exports.parseHttpieArg = exports.joinUrl = exports.extractGitHubFilePath = void 0;
|
|
4
|
+
exports.runServiceCommand = runServiceCommand;
|
|
3
5
|
exports.runTelactlCommand = runTelactlCommand;
|
|
4
6
|
exports.runIngredientEnvCommand = runIngredientEnvCommand;
|
|
5
7
|
exports.fetchIngredientCatalog = fetchIngredientCatalog;
|
|
6
|
-
|
|
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");
|
|
8
|
+
const node_buffer_1 = require("node:buffer");
|
|
13
9
|
const tela_local_config_1 = require("@meistrari/tela-local-config");
|
|
14
|
-
const
|
|
10
|
+
const tela_pantry_1 = require("@meistrari/tela-pantry");
|
|
11
|
+
Object.defineProperty(exports, "extractGitHubFilePath", { enumerable: true, get: function () { return tela_pantry_1.extractGitHubFilePath; } });
|
|
12
|
+
Object.defineProperty(exports, "joinUrl", { enumerable: true, get: function () { return tela_pantry_1.joinUrl; } });
|
|
13
|
+
Object.defineProperty(exports, "parseHttpieArg", { enumerable: true, get: function () { return tela_pantry_1.parseHttpieArg; } });
|
|
14
|
+
Object.defineProperty(exports, "resolveGuidePath", { enumerable: true, get: function () { return tela_pantry_1.resolveGuidePath; } });
|
|
15
|
+
const markdown_1 = require("../markdown");
|
|
16
|
+
const session_1 = require("../session");
|
|
15
17
|
const output_1 = require("../output");
|
|
16
|
-
const PANTRY_BASE_URL = 'https://pantry-api.tela.tools';
|
|
17
|
-
const INGREDIENT_PAGE_LIMIT = 100;
|
|
18
18
|
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
async function runServiceCommand(args, options, io) {
|
|
20
|
+
const [command, ...rest] = args;
|
|
21
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
22
|
+
writeServiceHelp(io);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (command === 'list')
|
|
27
|
+
return await listServices(rest, options, io);
|
|
28
|
+
if (command === 'show')
|
|
29
|
+
return await showService(rest, options, io);
|
|
30
|
+
if (command === 'api')
|
|
31
|
+
return await callIngredientApi(rest, options, io);
|
|
32
|
+
if (command === 'docs')
|
|
33
|
+
return await runDocsCommand(rest, options, io);
|
|
34
|
+
if (command === 'env')
|
|
35
|
+
return await runServiceEnvCommand(rest, options, io);
|
|
36
|
+
return usageError(`Unknown service command: ${command}`, io);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
(0, output_1.writeError)(io, formatError(error));
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
async function runTelactlCommand(command, args, options, io) {
|
|
29
44
|
try {
|
|
30
|
-
if (command === 'update')
|
|
31
|
-
return await updateIngredients(args, options, io);
|
|
32
45
|
if (command === 'api')
|
|
33
46
|
return await callIngredientApi(args, options, io);
|
|
34
47
|
if (command === 'docs')
|
|
@@ -37,118 +50,115 @@ async function runTelactlCommand(command, args, options, io) {
|
|
|
37
50
|
return await whoami(args, options, io);
|
|
38
51
|
if (command === 'token')
|
|
39
52
|
return await tokenAlias(args, options, io);
|
|
40
|
-
|
|
41
|
-
return 1;
|
|
53
|
+
return usageError(`Unknown telactl command: ${command}`, io);
|
|
42
54
|
}
|
|
43
55
|
catch (error) {
|
|
44
|
-
(0, output_1.writeError)(io,
|
|
56
|
+
(0, output_1.writeError)(io, formatError(error));
|
|
45
57
|
return 1;
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
async function runIngredientEnvCommand(args, options, io) {
|
|
49
61
|
const [action, ...rest] = args;
|
|
50
62
|
if (action === 'set')
|
|
51
|
-
return await
|
|
63
|
+
return await setServiceEnvironment(rest, io);
|
|
52
64
|
if (action === 'list' && rest.some(arg => !arg.startsWith('-')))
|
|
53
|
-
return await
|
|
65
|
+
return await listServiceEnvironments(rest, options, io, true);
|
|
54
66
|
return null;
|
|
55
67
|
}
|
|
56
|
-
async function
|
|
68
|
+
async function runServiceEnvCommand(args, options, io) {
|
|
69
|
+
const [action, ...rest] = args;
|
|
70
|
+
if (action === 'list')
|
|
71
|
+
return await listServiceEnvironments(rest, options, io, false);
|
|
72
|
+
if (action === 'use' || action === 'set')
|
|
73
|
+
return await setServiceEnvironment(rest, io);
|
|
74
|
+
return usageError('Usage: tela service env <list|use> <ingredient-slug> [service-environment]', io);
|
|
75
|
+
}
|
|
76
|
+
async function listServices(args, options, io) {
|
|
57
77
|
const flags = parseCommonFlags(args);
|
|
58
78
|
if ('error' in flags)
|
|
59
79
|
return usageError(flags.error, io);
|
|
60
80
|
if (flags.rest.length > 0)
|
|
61
|
-
return usageError(`Unknown
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
return usageError(`Unknown service list argument: ${flags.rest[0]}`, io);
|
|
82
|
+
const { client, baseUrl } = await (0, session_1.loadPantry)(flags);
|
|
83
|
+
const ingredients = await (0, tela_pantry_1.fetchIngredientCatalog)(client, baseUrl);
|
|
84
|
+
// Only API ingredients are services. Libraries are imported and skills are
|
|
85
|
+
// installed into an agent; neither is reachable over the network, so
|
|
86
|
+
// neither has environments or endpoints to list. `service show` still
|
|
87
|
+
// resolves them by slug.
|
|
88
|
+
const services = ingredients.filter(ingredient => ingredient.type === 'api');
|
|
89
|
+
if (options.json) {
|
|
90
|
+
(0, output_1.writeJson)(io, { services });
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
for (const ingredient of services) {
|
|
94
|
+
const environments = ingredient.environments.map(environment => environment.slug).join(', ') || '-';
|
|
95
|
+
(0, output_1.writeLine)(io, `${ingredient.slug.padEnd(24)} ${ingredient.type.padEnd(8)} ${environments}`);
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
async function showService(args, options, io) {
|
|
100
|
+
const flags = parseCommonFlags(args);
|
|
101
|
+
if ('error' in flags)
|
|
102
|
+
return usageError(flags.error, io);
|
|
103
|
+
const [requestedSlug] = flags.rest;
|
|
104
|
+
if (!requestedSlug)
|
|
105
|
+
return usageError('Usage: tela service show <slug> [--json]', io);
|
|
106
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
107
|
+
const { client, baseUrl } = await (0, session_1.loadPantry)(flags);
|
|
108
|
+
const ingredient = await (0, tela_pantry_1.fetchServiceIngredient)(client, slug, baseUrl);
|
|
68
109
|
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
|
-
});
|
|
110
|
+
(0, output_1.writeJson)(io, { service: ingredient });
|
|
77
111
|
return 0;
|
|
78
112
|
}
|
|
79
|
-
(0, output_1.writeLine)(io,
|
|
113
|
+
(0, output_1.writeLine)(io, `${ingredient.name} (${ingredient.slug})`);
|
|
114
|
+
(0, output_1.writeLine)(io, `type: ${ingredient.type}`);
|
|
115
|
+
(0, output_1.writeLine)(io, `status: ${ingredient.status || '-'}`);
|
|
116
|
+
if (ingredient.description)
|
|
117
|
+
(0, output_1.writeLine)(io, ingredient.description);
|
|
118
|
+
(0, output_1.writeLine)(io, 'environments:');
|
|
119
|
+
for (const environment of ingredient.environments)
|
|
120
|
+
(0, output_1.writeLine)(io, ` ${environment.slug.padEnd(16)} ${environment.url}`);
|
|
80
121
|
return 0;
|
|
81
122
|
}
|
|
82
123
|
async function callIngredientApi(args, _options, io) {
|
|
83
124
|
const flags = parseApiFlags(args);
|
|
84
125
|
if ('error' in flags)
|
|
85
126
|
return usageError(flags.error, io);
|
|
86
|
-
const [
|
|
87
|
-
if (!
|
|
88
|
-
return usageError('Usage: tela api <slug> <METHOD> <PATH> [args...] [--env <slug>] [--body <json>] [-p|--pretty]', io);
|
|
127
|
+
const [requestedSlug, method, routePath, ...httpieArgs] = flags.rest;
|
|
128
|
+
if (!requestedSlug || !method || !routePath) {
|
|
129
|
+
return usageError('Usage: tela service api <slug> <METHOD> <PATH> [args...] [--service-env <slug>] [--body <json>] [-p|--pretty]', io);
|
|
89
130
|
}
|
|
131
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
90
132
|
if (!HTTP_METHODS.has(method.toUpperCase()))
|
|
91
133
|
return usageError(`Unsupported HTTP method: ${method}`, io);
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
// Parse the request before any network work: a typo in the arguments should
|
|
135
|
+
// be reported straight away, not after a login and a Pantry lookup.
|
|
136
|
+
const request = (0, tela_pantry_1.parseHttpieRequest)(httpieArgs, flags.body);
|
|
137
|
+
const targetSession = await (0, session_1.loadSession)(flags.contextId, flags.environmentId);
|
|
138
|
+
const { client, baseUrl } = await (0, session_1.loadPantryForContext)(targetSession.resolved);
|
|
139
|
+
const ingredient = (0, tela_pantry_1.assertIngredientType)(await (0, tela_pantry_1.fetchServiceIngredient)(client, slug, baseUrl), 'api');
|
|
140
|
+
const { inventory } = await (0, tela_local_config_1.loadInventory)((0, tela_local_config_1.getTelaLocalPaths)().inventoryFile);
|
|
141
|
+
const environment = (0, tela_pantry_1.resolveServiceEnvironment)(ingredient, inventory.services.environmentDefaults, flags.serviceEnv);
|
|
94
142
|
if (!environment) {
|
|
95
|
-
return usageError(`Ingredient '${slug}' has no environments configured
|
|
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;
|
|
143
|
+
return usageError(`Ingredient '${slug}' has no environments configured.`, io);
|
|
125
144
|
}
|
|
126
|
-
const url = new URL(joinUrl(environment.url, routePath));
|
|
127
|
-
for (const [key, value] of queryParams)
|
|
145
|
+
const url = new URL((0, tela_pantry_1.joinUrl)(environment.url, routePath));
|
|
146
|
+
for (const [key, value] of request.queryParams)
|
|
128
147
|
url.searchParams.append(key, value);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
148
|
+
for (const [key, value] of Object.entries(requestHeaders(targetSession.credential))) {
|
|
149
|
+
if (!request.headers.has(key))
|
|
150
|
+
request.headers.set(key, value);
|
|
139
151
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (requestBody !== undefined && !headers.has('Content-Type'))
|
|
144
|
-
headers.set('Content-Type', 'application/json');
|
|
152
|
+
if (!request.headers.has('Accept'))
|
|
153
|
+
request.headers.set('Accept', 'application/json');
|
|
145
154
|
const response = await fetch(url, {
|
|
146
155
|
method: method.toUpperCase(),
|
|
147
|
-
headers,
|
|
148
|
-
body:
|
|
156
|
+
headers: request.headers,
|
|
157
|
+
body: request.body,
|
|
149
158
|
});
|
|
150
|
-
const
|
|
151
|
-
|
|
159
|
+
const body = node_buffer_1.Buffer.from(await response.arrayBuffer());
|
|
160
|
+
const text = body.toString('utf8');
|
|
161
|
+
(0, tela_pantry_1.assertHttpOk)(response, text);
|
|
152
162
|
if (flags.pretty) {
|
|
153
163
|
const parsed = tryParseJson(text);
|
|
154
164
|
if (parsed !== null) {
|
|
@@ -156,7 +166,7 @@ async function callIngredientApi(args, _options, io) {
|
|
|
156
166
|
return 0;
|
|
157
167
|
}
|
|
158
168
|
}
|
|
159
|
-
|
|
169
|
+
io.stdout.write(body);
|
|
160
170
|
return 0;
|
|
161
171
|
}
|
|
162
172
|
async function runDocsCommand(args, options, io) {
|
|
@@ -177,14 +187,17 @@ async function docsEndpoints(args, options, io) {
|
|
|
177
187
|
const flags = parseCommonFlags(args);
|
|
178
188
|
if ('error' in flags)
|
|
179
189
|
return usageError(flags.error, io);
|
|
180
|
-
const [
|
|
181
|
-
if (!
|
|
182
|
-
return usageError('Usage: tela docs endpoints <slug> [-p|--pretty]', io);
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
190
|
+
const [requestedSlug] = flags.rest;
|
|
191
|
+
if (!requestedSlug)
|
|
192
|
+
return usageError('Usage: tela service docs endpoints <slug> [-p|--pretty]', io);
|
|
193
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
194
|
+
const { client, detail } = await loadIngredient(slug, flags);
|
|
195
|
+
if (detail.type !== 'api')
|
|
196
|
+
return usageError(`Ingredient '${slug}' is not an api ingredient.`, io);
|
|
197
|
+
const spec = await (0, tela_pantry_1.fetchIngredientOpenapi)(client, detail);
|
|
198
|
+
const paths = ((0, tela_pantry_1.isRecord)(spec.paths) ? spec.paths : {});
|
|
186
199
|
if (flags.pretty && !options.json) {
|
|
187
|
-
(0, output_1.writeLine)(io, formatEndpointsTable(paths));
|
|
200
|
+
(0, output_1.writeLine)(io, (0, tela_pantry_1.formatEndpointsTable)(paths));
|
|
188
201
|
return 0;
|
|
189
202
|
}
|
|
190
203
|
(0, output_1.writeJson)(io, paths);
|
|
@@ -194,29 +207,29 @@ async function docsReference(args, options, io) {
|
|
|
194
207
|
const flags = parseCommonFlags(args);
|
|
195
208
|
if ('error' in flags)
|
|
196
209
|
return usageError(flags.error, io);
|
|
197
|
-
const [
|
|
198
|
-
if (!
|
|
199
|
-
return usageError('Usage: tela docs reference <slug> [METHOD PATH|file-path] [-p|--pretty]', io);
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
const [requestedSlug, first, second] = flags.rest;
|
|
211
|
+
if (!requestedSlug)
|
|
212
|
+
return usageError('Usage: tela service docs reference <slug> [METHOD PATH|file-path] [-p|--pretty]', io);
|
|
213
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
214
|
+
const { client, detail } = await loadIngredient(slug, flags);
|
|
215
|
+
if (detail.type === 'api') {
|
|
216
|
+
if (first && !second && HTTP_METHODS.has(first.toUpperCase()))
|
|
203
217
|
return usageError('Both METHOD and PATH are required to show a specific endpoint', io);
|
|
204
|
-
|
|
205
|
-
const spec = await fetchOpenApiSpec(slug, flags.contextId);
|
|
218
|
+
const spec = await (0, tela_pantry_1.fetchIngredientOpenapi)(client, detail);
|
|
206
219
|
if (first && second) {
|
|
207
220
|
if (!HTTP_METHODS.has(first.toUpperCase()))
|
|
208
221
|
return usageError(`Unsupported HTTP method: ${first}`, io);
|
|
209
|
-
const pathEntry = isRecord(spec.paths) && isRecord(spec.paths[second]) ? spec.paths[second] : null;
|
|
222
|
+
const pathEntry = (0, tela_pantry_1.isRecord)(spec.paths) && (0, tela_pantry_1.isRecord)(spec.paths[second]) ? spec.paths[second] : null;
|
|
210
223
|
if (!pathEntry)
|
|
211
224
|
return usageError(`Path not found: ${second}`, io);
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
225
|
+
const operation = pathEntry[first.toLowerCase()];
|
|
226
|
+
if (!operation)
|
|
214
227
|
return usageError(`Method ${first.toUpperCase()} not found for ${second}`, io);
|
|
215
228
|
if (flags.pretty && !options.json) {
|
|
216
|
-
(0, output_1.writeLine)(io, formatEndpoint(spec,
|
|
229
|
+
(0, output_1.writeLine)(io, (0, tela_pantry_1.formatEndpoint)(spec, operation, first, second));
|
|
217
230
|
return 0;
|
|
218
231
|
}
|
|
219
|
-
(0, output_1.writeJson)(io,
|
|
232
|
+
(0, output_1.writeJson)(io, operation);
|
|
220
233
|
return 0;
|
|
221
234
|
}
|
|
222
235
|
if (flags.pretty && !options.json) {
|
|
@@ -226,79 +239,84 @@ async function docsReference(args, options, io) {
|
|
|
226
239
|
(0, output_1.writeLine)(io, JSON.stringify(spec));
|
|
227
240
|
return 0;
|
|
228
241
|
}
|
|
229
|
-
if (
|
|
242
|
+
if (detail.type !== 'library')
|
|
230
243
|
return usageError(`Ingredient '${slug}' is not an API or library ingredient.`, io);
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (!apiRef)
|
|
244
|
+
const apiReference = detail.docs.api ?? '';
|
|
245
|
+
if (!apiReference)
|
|
234
246
|
return usageError(`Library ${slug} has no API reference`, io);
|
|
235
|
-
|
|
247
|
+
// A generated library publishes a CDN manifest here rather than a GitHub
|
|
248
|
+
// URL, the same way its guides do.
|
|
249
|
+
if (detail.docsSource === 'generated') {
|
|
250
|
+
const reference = await (0, tela_pantry_1.fetchGeneratedApiReference)(client, detail, first);
|
|
251
|
+
(0, output_1.writeLine)(io, await (0, markdown_1.renderMarkdown)(reference.content));
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
const filePath = (0, tela_pantry_1.extractGitHubFilePath)(apiReference);
|
|
236
255
|
if (!filePath)
|
|
237
|
-
return usageError(`Could not extract file path from API reference URL: ${
|
|
256
|
+
return usageError(`Could not extract file path from API reference URL: ${apiReference}`, io);
|
|
238
257
|
const slashIndex = filePath.lastIndexOf('/');
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
(0, output_1.writeLine)(io, stringValue(guide.content));
|
|
258
|
+
const directory = slashIndex === -1 ? '' : filePath.slice(0, slashIndex + 1);
|
|
259
|
+
const guide = await (0, tela_pantry_1.fetchIngredientGuide)(client, slug, `${directory}${first ? first.replace(/^\/+/, '') : 'globals.md'}`);
|
|
260
|
+
(0, output_1.writeLine)(io, await (0, markdown_1.renderMarkdown)(guide.content));
|
|
243
261
|
return 0;
|
|
244
262
|
}
|
|
245
263
|
async function docsGuide(args, _options, io) {
|
|
246
264
|
const flags = parseCommonFlags(args);
|
|
247
265
|
if ('error' in flags)
|
|
248
266
|
return usageError(flags.error, io);
|
|
249
|
-
const [
|
|
250
|
-
if (!
|
|
251
|
-
return usageError('Usage: tela docs guide <slug> [path] [-p|--pretty]', io);
|
|
252
|
-
const
|
|
253
|
-
|
|
267
|
+
const [requestedSlug, providedPath] = flags.rest;
|
|
268
|
+
if (!requestedSlug)
|
|
269
|
+
return usageError('Usage: tela service docs guide <slug> [path] [-p|--pretty]', io);
|
|
270
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
271
|
+
const { client, detail } = await loadIngredient(slug, flags);
|
|
272
|
+
if (detail.type !== 'api' && detail.type !== 'library')
|
|
254
273
|
return usageError(`Ingredient '${slug}' is not an API or library ingredient.`, io);
|
|
255
|
-
|
|
256
|
-
|
|
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));
|
|
274
|
+
const guide = await (0, tela_pantry_1.fetchIngredientGuideDocument)(client, detail, providedPath);
|
|
275
|
+
(0, output_1.writeLine)(io, await (0, markdown_1.renderMarkdown)(guide.content));
|
|
265
276
|
return 0;
|
|
266
277
|
}
|
|
267
|
-
async function
|
|
278
|
+
async function listServiceEnvironments(args, options, io, legacyDefaultJson) {
|
|
268
279
|
const flags = parseCommonFlags(args);
|
|
269
280
|
if ('error' in flags)
|
|
270
281
|
return usageError(flags.error, io);
|
|
271
|
-
const [
|
|
272
|
-
if (!
|
|
273
|
-
return usageError('Usage: tela env list <ingredient-slug> [-p|--pretty|--json]', io);
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
const [requestedSlug] = flags.rest;
|
|
283
|
+
if (!requestedSlug)
|
|
284
|
+
return usageError('Usage: tela service env list <ingredient-slug> [-p|--pretty|--json]', io);
|
|
285
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
286
|
+
const { client, baseUrl } = await (0, session_1.loadPantry)(flags);
|
|
287
|
+
const ingredient = await (0, tela_pantry_1.fetchServiceIngredient)(client, slug, baseUrl);
|
|
288
|
+
if (options.json || (legacyDefaultJson && !flags.pretty)) {
|
|
289
|
+
(0, output_1.writeJson)(io, { environments: ingredient.environments });
|
|
278
290
|
return 0;
|
|
279
291
|
}
|
|
280
|
-
if (environments.length === 0) {
|
|
292
|
+
if (ingredient.environments.length === 0) {
|
|
281
293
|
(0, output_1.writeLine)(io, '(no environments configured)');
|
|
282
294
|
return 0;
|
|
283
295
|
}
|
|
284
|
-
const slugWidth = Math.max(...environments.map(environment => environment.slug.length));
|
|
285
|
-
for (const environment of environments)
|
|
296
|
+
const slugWidth = Math.max(...ingredient.environments.map(environment => environment.slug.length));
|
|
297
|
+
for (const environment of ingredient.environments)
|
|
286
298
|
(0, output_1.writeLine)(io, `${environment.slug.padEnd(slugWidth)} ${environment.url}`);
|
|
287
|
-
}
|
|
288
299
|
return 0;
|
|
289
300
|
}
|
|
290
|
-
async function
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
return usageError(
|
|
294
|
-
const
|
|
301
|
+
async function setServiceEnvironment(args, io) {
|
|
302
|
+
const flags = parseCommonFlags(args);
|
|
303
|
+
if ('error' in flags)
|
|
304
|
+
return usageError(flags.error, io);
|
|
305
|
+
const [requestedSlug, environment, extra] = flags.rest;
|
|
306
|
+
if (!requestedSlug || !environment || extra)
|
|
307
|
+
return usageError('Usage: tela service env use <ingredient-slug> <service-environment> [--context <id>|--environment <id>]', io);
|
|
308
|
+
// Resolve before the write as well as the read: a default saved under an
|
|
309
|
+
// alias would never be found again when the alias is expanded on lookup.
|
|
310
|
+
const slug = (0, tela_pantry_1.resolveIngredientSlug)(requestedSlug);
|
|
311
|
+
const { client, baseUrl } = await (0, session_1.loadPantry)(flags);
|
|
312
|
+
const ingredient = await (0, tela_pantry_1.fetchServiceIngredient)(client, slug, baseUrl);
|
|
295
313
|
if (!ingredient.environments.some(candidate => candidate.slug === environment)) {
|
|
296
314
|
const available = ingredient.environments.map(candidate => candidate.slug).join(', ') || '(none)';
|
|
297
315
|
return usageError(`Environment '${environment}' not found for '${slug}'. Available: ${available}`, io);
|
|
298
316
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
317
|
+
await (0, tela_local_config_1.updateInventory)((inventory) => {
|
|
318
|
+
inventory.services.environmentDefaults[slug] = environment;
|
|
319
|
+
});
|
|
302
320
|
(0, output_1.writeLine)(io, `Set default environment for '${slug}' to '${environment}'`);
|
|
303
321
|
return 0;
|
|
304
322
|
}
|
|
@@ -308,10 +326,9 @@ async function whoami(args, _options, io) {
|
|
|
308
326
|
return usageError(flags.error, io);
|
|
309
327
|
if (flags.rest.length > 0)
|
|
310
328
|
return usageError(`Unknown whoami argument: ${flags.rest[0]}`, io);
|
|
311
|
-
const session = await loadSession(flags.contextId);
|
|
312
|
-
if (!session.credential.accessToken)
|
|
329
|
+
const session = await (0, session_1.loadSession)(flags.contextId, flags.environmentId);
|
|
330
|
+
if (!session.credential.accessToken)
|
|
313
331
|
return usageError(`Context ${session.resolved.id} does not have an access token credential.`, io);
|
|
314
|
-
}
|
|
315
332
|
const response = await fetch(new URL('/api/auth/applications/whoami', session.resolved.environment.authApiUrl), {
|
|
316
333
|
headers: {
|
|
317
334
|
'Accept': 'application/json',
|
|
@@ -319,7 +336,7 @@ async function whoami(args, _options, io) {
|
|
|
319
336
|
},
|
|
320
337
|
});
|
|
321
338
|
const text = await response.text();
|
|
322
|
-
assertHttpOk(response, text);
|
|
339
|
+
(0, tela_pantry_1.assertHttpOk)(response, text);
|
|
323
340
|
if (flags.pretty) {
|
|
324
341
|
const parsed = tryParseJson(text);
|
|
325
342
|
if (parsed !== null) {
|
|
@@ -336,66 +353,27 @@ async function tokenAlias(args, _options, io) {
|
|
|
336
353
|
return usageError(flags.error, io);
|
|
337
354
|
if (flags.rest.length > 0)
|
|
338
355
|
return usageError(`Unknown token argument: ${flags.rest[0]}`, io);
|
|
339
|
-
const session = await loadSession(flags.contextId);
|
|
356
|
+
const session = await (0, session_1.loadSession)(flags.contextId, flags.environmentId);
|
|
340
357
|
if (!session.credential.accessToken)
|
|
341
358
|
return usageError(`Context ${session.resolved.id} does not have an access token credential.`, io);
|
|
342
359
|
(0, output_1.writeLine)(io, session.credential.accessToken);
|
|
343
360
|
return 0;
|
|
344
361
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
362
|
+
/**
|
|
363
|
+
* The docs commands need the ingredient record for its type and its `docs`
|
|
364
|
+
* links, and the OpenAPI fetch needs the same record to decide between the
|
|
365
|
+
* Pantry route and the docs CDN. Fetch it once and pass it along.
|
|
366
|
+
*/
|
|
367
|
+
async function loadIngredient(slug, flags) {
|
|
368
|
+
const { client } = await (0, session_1.loadPantry)(flags);
|
|
369
|
+
return { client, detail: await (0, tela_pantry_1.fetchIngredientDetail)(client, slug) };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Only `tela service api` still builds its own headers — every Pantry route now
|
|
373
|
+
* goes through the generated client, which sends `Authorization: Bearer` and
|
|
374
|
+
* nothing else. The `x-data-token` branch stays because arbitrary ingredient
|
|
375
|
+
* APIs may accept it; Pantry itself demonstrably does not.
|
|
376
|
+
*/
|
|
399
377
|
function requestHeaders(credential) {
|
|
400
378
|
if (credential.kind === 'data-token' && credential.dataToken) {
|
|
401
379
|
return {
|
|
@@ -411,136 +389,11 @@ function requestHeaders(credential) {
|
|
|
411
389
|
}
|
|
412
390
|
throw new Error(`Credential ${credential.kind} does not contain usable token material`);
|
|
413
391
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
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
|
-
}
|
|
392
|
+
async function fetchIngredientCatalog(credential, baseUrl = tela_pantry_1.DEFAULT_PANTRY_BASE_URL) {
|
|
393
|
+
return await (0, tela_pantry_1.fetchIngredientCatalog)((0, tela_pantry_1.createPantryClient)(baseUrl, credential), baseUrl);
|
|
538
394
|
}
|
|
539
395
|
function parseCommonFlags(args) {
|
|
540
|
-
const flags = {
|
|
541
|
-
pretty: false,
|
|
542
|
-
rest: [],
|
|
543
|
-
};
|
|
396
|
+
const flags = { pretty: false, rest: [] };
|
|
544
397
|
for (let index = 0; index < args.length; index += 1) {
|
|
545
398
|
const arg = args[index];
|
|
546
399
|
if (arg === undefined)
|
|
@@ -553,6 +406,14 @@ function parseCommonFlags(args) {
|
|
|
553
406
|
index += 1;
|
|
554
407
|
continue;
|
|
555
408
|
}
|
|
409
|
+
if (arg === '--environment' || arg === '--auth-env') {
|
|
410
|
+
const value = args[index + 1];
|
|
411
|
+
if (!value)
|
|
412
|
+
return { error: `Expected value after ${arg}` };
|
|
413
|
+
flags.environmentId = value;
|
|
414
|
+
index += 1;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
556
417
|
if (arg === '--pretty' || arg === '-p') {
|
|
557
418
|
flags.pretty = true;
|
|
558
419
|
continue;
|
|
@@ -562,10 +423,7 @@ function parseCommonFlags(args) {
|
|
|
562
423
|
return flags;
|
|
563
424
|
}
|
|
564
425
|
function parseApiFlags(args) {
|
|
565
|
-
const flags = {
|
|
566
|
-
pretty: false,
|
|
567
|
-
rest: [],
|
|
568
|
-
};
|
|
426
|
+
const flags = { pretty: false, rest: [] };
|
|
569
427
|
for (let index = 0; index < args.length; index += 1) {
|
|
570
428
|
const arg = args[index];
|
|
571
429
|
if (arg === undefined)
|
|
@@ -578,11 +436,21 @@ function parseApiFlags(args) {
|
|
|
578
436
|
index += 1;
|
|
579
437
|
continue;
|
|
580
438
|
}
|
|
581
|
-
if (arg === '--env') {
|
|
439
|
+
if (arg === '--environment' || arg === '--auth-env') {
|
|
582
440
|
const value = args[index + 1];
|
|
583
441
|
if (!value)
|
|
584
|
-
return { error:
|
|
585
|
-
flags.
|
|
442
|
+
return { error: `Expected value after ${arg}` };
|
|
443
|
+
flags.environmentId = value;
|
|
444
|
+
index += 1;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (arg === '--env' || arg === '--service-env') {
|
|
448
|
+
const value = args[index + 1];
|
|
449
|
+
if (!value)
|
|
450
|
+
return { error: `Expected value after ${arg}` };
|
|
451
|
+
if (flags.serviceEnv && flags.serviceEnv !== value)
|
|
452
|
+
return { error: 'Pass only one service environment override.' };
|
|
453
|
+
flags.serviceEnv = value;
|
|
586
454
|
index += 1;
|
|
587
455
|
continue;
|
|
588
456
|
}
|
|
@@ -602,201 +470,23 @@ function parseApiFlags(args) {
|
|
|
602
470
|
}
|
|
603
471
|
return flags;
|
|
604
472
|
}
|
|
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
473
|
function usageError(message, io) {
|
|
795
474
|
(0, output_1.writeError)(io, message);
|
|
796
475
|
return 1;
|
|
797
476
|
}
|
|
477
|
+
function writeServiceHelp(io) {
|
|
478
|
+
(0, output_1.writeLine)(io, 'Usage: tela service <command> [args]');
|
|
479
|
+
(0, output_1.writeLine)(io);
|
|
480
|
+
(0, output_1.writeLine)(io, 'Commands:');
|
|
481
|
+
(0, output_1.writeLine)(io, ' service list [--json]');
|
|
482
|
+
(0, output_1.writeLine)(io, ' service show <slug> [--json]');
|
|
483
|
+
(0, output_1.writeLine)(io, ' service env list <slug> [-p|--pretty|--json]');
|
|
484
|
+
(0, output_1.writeLine)(io, ' service env use <slug> <service-environment> [--context <id>|--environment <id>]');
|
|
485
|
+
(0, output_1.writeLine)(io, ' service api <slug> <METHOD> <PATH> [args...] [--context <id>|--environment <id>] [--service-env <slug>] [--body <json>] [-p|--pretty]');
|
|
486
|
+
(0, output_1.writeLine)(io, ' service docs endpoints|reference|guide <slug> [...]');
|
|
487
|
+
}
|
|
798
488
|
function writeDocsHelp(io) {
|
|
799
|
-
(0, output_1.writeLine)(io, 'Usage: tela docs <command> [args]');
|
|
489
|
+
(0, output_1.writeLine)(io, 'Usage: tela service docs <command> [args]');
|
|
800
490
|
(0, output_1.writeLine)(io);
|
|
801
491
|
(0, output_1.writeLine)(io, 'Commands:');
|
|
802
492
|
(0, output_1.writeLine)(io, ' docs endpoints <slug> [-p|--pretty] [--json]');
|
|
@@ -811,14 +501,6 @@ function tryParseJson(text) {
|
|
|
811
501
|
return null;
|
|
812
502
|
}
|
|
813
503
|
}
|
|
814
|
-
function
|
|
815
|
-
return
|
|
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';
|
|
504
|
+
function formatError(error) {
|
|
505
|
+
return error instanceof Error ? error.message : String(error);
|
|
824
506
|
}
|