@meistrari/tela-cli 1.1.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.
@@ -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
- 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");
8
+ const node_buffer_1 = require("node:buffer");
13
9
  const tela_local_config_1 = require("@meistrari/tela-local-config");
14
- const credentials_1 = require("../credentials");
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
- 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
- };
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
- (0, output_1.writeError)(io, `Unknown telactl command: ${command}`);
41
- return 1;
53
+ return usageError(`Unknown telactl command: ${command}`, io);
42
54
  }
43
55
  catch (error) {
44
- (0, output_1.writeError)(io, error instanceof Error ? error.message : String(error));
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 setIngredientEnvironment(rest, io);
63
+ return await setServiceEnvironment(rest, io);
52
64
  if (action === 'list' && rest.some(arg => !arg.startsWith('-')))
53
- return await listIngredientEnvironments(rest, options, io);
65
+ return await listServiceEnvironments(rest, options, io, true);
54
66
  return null;
55
67
  }
56
- async function updateIngredients(args, options, io) {
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 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);
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, `Cached ${cache.ingredients.length} ingredients`);
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 [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);
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
- const ingredient = requireIngredient(slug, 'api');
93
- const environment = resolveIngredientEnvironment(ingredient, flags.env);
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. 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;
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 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;
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
- else if (hasFields) {
141
- requestBody = JSON.stringify(fields);
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: requestBody,
156
+ headers: request.headers,
157
+ body: request.body,
149
158
  });
150
- const text = await response.text();
151
- assertHttpOk(response, text);
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
- (0, output_1.writeLine)(io, text);
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 [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 : {});
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 [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())) {
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 op = pathEntry[first.toLowerCase()];
213
- if (!op)
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, op, first, second));
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, op);
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 (ingredient.type !== 'library')
242
+ if (detail.type !== 'library')
230
243
  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)
244
+ const apiReference = detail.docs.api ?? '';
245
+ if (!apiReference)
234
246
  return usageError(`Library ${slug} has no API reference`, io);
235
- const filePath = extractGitHubFilePath(apiRef);
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: ${apiRef}`, io);
256
+ return usageError(`Could not extract file path from API reference URL: ${apiReference}`, io);
238
257
  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));
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 [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') {
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
- 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));
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 listIngredientEnvironments(args, options, io) {
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 [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 });
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 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);
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
- const defaults = await readEnvironmentDefaults();
300
- defaults[slug] = environment;
301
- await writeEnvironmentDefaults(defaults);
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
- 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
- }
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 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
- }
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: 'Expected value after --env' };
585
- flags.env = value;
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 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';
504
+ function formatError(error) {
505
+ return error instanceof Error ? error.message : String(error);
824
506
  }