@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 CHANGED
@@ -9,9 +9,12 @@ directly, and normal output must not print token material.
9
9
 
10
10
  ## Commands
11
11
 
12
- Current first-slice commands:
12
+ Canonical commands:
13
13
 
14
14
  ```bash
15
+ tela auth login --no-interactive
16
+ tela auth logout --context <context-id>
17
+ tela auth whoami --json
15
18
  tela auth token --json
16
19
  tela auth token --raw
17
20
  tela context current --json
@@ -19,14 +22,15 @@ tela context list --json
19
22
  tela context show <context-id> --json
20
23
  tela context use <context-id>
21
24
  tela env list --json
22
- tela update --json
23
- tela api <slug> <METHOD> <PATH> [args...] --env <slug> --body <json> --pretty
24
- tela docs endpoints <slug> --pretty
25
- tela docs reference <slug> [METHOD PATH|file-path] --pretty
26
- tela docs guide <slug> [path]
27
- tela env list <ingredient-slug> --pretty
28
- tela env set <ingredient-slug> <environment>
29
- tela whoami --pretty
25
+ tela service update --json
26
+ tela service list --json
27
+ tela service show <slug> --json
28
+ tela service api <slug> <METHOD> <PATH> [args...] --service-env <slug> --body <json> --pretty
29
+ tela service docs endpoints <slug> --pretty
30
+ tela service docs reference <slug> [METHOD PATH|file-path] --pretty
31
+ tela service docs guide <slug> [path]
32
+ tela service env list <slug> --pretty
33
+ tela service env use <slug> <environment>
30
34
  tela doctor auth --json
31
35
  tela agent sync [repo] --current --all --dry-run --json --verbose
32
36
  tela agent run --input <name=path>... --plain --json --verbose
@@ -34,16 +38,15 @@ tela agent push --message <text> --claude --manual --dry-run --json --verbose
34
38
  tela agent feedback --list --apply <all|select> --json --verbose
35
39
  ```
36
40
 
37
- `context use` is the only normal config mutation in this slice. `auth token`
38
- may refresh a user-session credential in Keychain before returning metadata or
39
- printing a raw token. Creating, removing, editing, and repairing contexts are
40
- Dock-first workflows.
41
+ `auth login` uses device flow and creates or updates a context backed by the
42
+ same Keychain credential adapter as Dock. `auth token` may refresh that session
43
+ before returning metadata or printing a raw token.
41
44
 
42
- The legacy `telactl` Pantry/service workflows are ported into this binary as
43
- top-level `update`, `api`, `docs`, `whoami`, and `token` commands. Ingredient
44
- cache and per-ingredient environment defaults live under `TELA_HOME` next to
45
- the Dock config, while credential material still comes from the selected Tela
46
- context through `@meistrari/tela-local-config`.
45
+ The top-level `api`, `docs`, `update`, `whoami`, and `token` aliases on `tela`
46
+ remain available during migration. Service catalog and per-service environment
47
+ defaults live in validated `TELA_HOME/inventory.json`, with a dual-read path for
48
+ the former `ingredients.json` files. Credential material continues to come from
49
+ the selected Tela context.
47
50
 
48
51
  ## Development
49
52
 
@@ -1,2 +1,10 @@
1
1
  import type { CliIo, GlobalOptions } from '../output';
2
2
  export declare function runAuthCommand(args: string[], options: GlobalOptions, io: CliIo): Promise<number>;
3
+ export declare function formatDeviceFlowInstructions(input: {
4
+ userCode: string;
5
+ verificationUriComplete: string;
6
+ deviceCode: string;
7
+ environmentId: string;
8
+ authUrl?: string;
9
+ contextId?: string;
10
+ }): string;
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.runAuthCommand = runAuthCommand;
7
+ exports.formatDeviceFlowInstructions = formatDeviceFlowInstructions;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ const node_process_1 = __importDefault(require("node:process"));
4
13
  const tela_local_config_1 = require("@meistrari/tela-local-config");
5
14
  const credentials_1 = require("../credentials");
6
15
  const output_1 = require("../output");
@@ -10,11 +19,226 @@ async function runAuthCommand(args, options, io) {
10
19
  writeAuthHelp(io);
11
20
  return 0;
12
21
  }
13
- if (action === 'token')
14
- return await authToken(rest, options, io);
22
+ if (action === 'token') {
23
+ if (rest[0] === 'list')
24
+ return await authTokenList(options, io);
25
+ else
26
+ return await authToken(rest, options, io);
27
+ }
28
+ if (action === 'login')
29
+ return await authLogin(rest, options, io);
30
+ if (action === 'logout')
31
+ return await authLogout(rest, options, io);
32
+ if (action === 'whoami')
33
+ return await authWhoami(rest, options, io);
15
34
  (0, output_1.writeError)(io, `Unknown auth command: ${action}`);
16
35
  return 1;
17
36
  }
37
+ async function authLogin(args, options, io) {
38
+ const parsed = parseLoginOptions(args);
39
+ if ('error' in parsed) {
40
+ (0, output_1.writeError)(io, parsed.error);
41
+ return 1;
42
+ }
43
+ try {
44
+ const config = await (0, tela_local_config_1.loadConfig)();
45
+ const context = parsed.contextId ? (0, tela_local_config_1.resolveContext)(config, { contextId: parsed.contextId }) : null;
46
+ const environmentId = context?.context.environmentId ?? parsed.environmentId ?? 'production';
47
+ const environment = config.environments[environmentId];
48
+ if (!environment)
49
+ throw new Error(`Unknown Tela environment: ${environmentId}`);
50
+ const requesterApplicationId = environment.authApplication?.requesterApplicationId;
51
+ const targetApplicationId = environment.authApplication?.targetApplicationId;
52
+ if (!requesterApplicationId || !targetApplicationId) {
53
+ throw new Error(`Environment '${environmentId}' does not define auth application IDs. Configure them before logging in.`);
54
+ }
55
+ // auth-cli is ESM-only with no `require` export condition, so this
56
+ // CJS bundle can only reach it through a dynamic import.
57
+ const { DeviceFlow } = await import('@meistrari/auth-cli');
58
+ const storage = (0, tela_local_config_1.createTelaTokenStorage)();
59
+ const flowOptions = {
60
+ apiUrl: parsed.authUrl ?? environment.authApiUrl,
61
+ requesterApplicationId,
62
+ targetApplicationId,
63
+ storage,
64
+ };
65
+ const loginContext = {
66
+ environmentId,
67
+ contextId: context?.id,
68
+ // Both paths below issue the device code up front, which makes the
69
+ // SDK skip its cache branch anyway. Say so rather than relying on it.
70
+ cache: false,
71
+ };
72
+ if (parsed.deviceCode) {
73
+ const resumed = DeviceFlow.resume(flowOptions, { deviceCode: parsed.deviceCode });
74
+ const tokens = await resumed.authenticate(loginContext);
75
+ return reportLogin(tokens, storage, environmentId, options, io);
76
+ }
77
+ const flow = new DeviceFlow(flowOptions);
78
+ const init = await flow.init();
79
+ if (!parsed.interactive) {
80
+ (0, output_1.writeLine)(io, formatDeviceFlowInstructions({
81
+ userCode: init.userCode,
82
+ verificationUriComplete: init.verificationUriComplete,
83
+ deviceCode: init.deviceCode,
84
+ environmentId,
85
+ authUrl: parsed.authUrl,
86
+ contextId: parsed.contextId,
87
+ }));
88
+ return 0;
89
+ }
90
+ const verificationUrl = resolveSafeVerificationUrl(init.verificationUriComplete, {
91
+ allowLoopbackHttp: environment.kind === 'local',
92
+ });
93
+ if (!verificationUrl)
94
+ throw new Error('Tela auth returned an unsafe verification URL.');
95
+ (0, output_1.writeLine)(io, `Open ${verificationUrl}`);
96
+ (0, output_1.writeLine)(io, `Code: ${init.userCode}`);
97
+ openExternal(verificationUrl);
98
+ const tokens = await flow.authenticate(loginContext);
99
+ return reportLogin(tokens, storage, environmentId, options, io);
100
+ }
101
+ catch (error) {
102
+ (0, output_1.writeError)(io, error instanceof Error ? error.message : String(error));
103
+ return 1;
104
+ }
105
+ }
106
+ function reportLogin(tokens, storage, environmentId, options, io) {
107
+ const contextId = storage.resolvedContextId;
108
+ if (options.json) {
109
+ (0, output_1.writeJson)(io, {
110
+ authenticated: true,
111
+ contextId,
112
+ environmentId,
113
+ user: tokens.user,
114
+ organization: tokens.organization,
115
+ });
116
+ }
117
+ else {
118
+ (0, output_1.writeLine)(io, `Logged in as ${tokens.user.email}.`);
119
+ (0, output_1.writeLine)(io, `Active context: ${contextId}`);
120
+ }
121
+ return 0;
122
+ }
123
+ async function authLogout(args, options, io) {
124
+ let contextId;
125
+ let all = false;
126
+ for (let index = 0; index < args.length; index += 1) {
127
+ const arg = args[index];
128
+ if (arg === '--all') {
129
+ all = true;
130
+ continue;
131
+ }
132
+ if (arg === '--context') {
133
+ contextId = args[index + 1];
134
+ if (!contextId) {
135
+ (0, output_1.writeError)(io, 'Usage: tela auth logout [--context <context-id>|--all]');
136
+ return 1;
137
+ }
138
+ index += 1;
139
+ continue;
140
+ }
141
+ (0, output_1.writeError)(io, `Unknown auth logout option: ${arg}`);
142
+ return 1;
143
+ }
144
+ try {
145
+ const config = await (0, tela_local_config_1.loadConfig)();
146
+ const targets = all
147
+ ? Object.entries(config.contexts)
148
+ : [[(0, tela_local_config_1.resolveContext)(config, { contextId }).id, (0, tela_local_config_1.resolveContext)(config, { contextId }).context]];
149
+ const credentials = (0, credentials_1.createCliCredentialAdapter)();
150
+ for (const [, context] of targets)
151
+ await credentials.delete(context.credentialRef);
152
+ if (options.json)
153
+ (0, output_1.writeJson)(io, { loggedOut: targets.map(([id]) => id) });
154
+ else
155
+ (0, output_1.writeLine)(io, `Logged out of ${targets.length} Tela context${targets.length === 1 ? '' : 's'}.`);
156
+ return 0;
157
+ }
158
+ catch (error) {
159
+ (0, output_1.writeError)(io, error instanceof Error ? error.message : String(error));
160
+ return 1;
161
+ }
162
+ }
163
+ async function authWhoami(args, options, io) {
164
+ const selector = parseContextSelector(args, 'whoami');
165
+ if ('error' in selector) {
166
+ (0, output_1.writeError)(io, selector.error);
167
+ return 1;
168
+ }
169
+ try {
170
+ const config = await (0, tela_local_config_1.loadConfig)();
171
+ const resolved = (0, tela_local_config_1.resolveContext)(config, selector);
172
+ const credential = await (0, credentials_1.createCliCredentialAdapter)().get(resolved.context.credentialRef);
173
+ if (!credential)
174
+ throw new Error(`Missing credential for Tela context ${resolved.id}`);
175
+ const raw = isRecord(credential.raw) ? credential.raw : {};
176
+ const payload = {
177
+ contextId: resolved.id,
178
+ environmentId: resolved.context.environmentId,
179
+ workspaceId: resolved.context.workspaceId,
180
+ accountEmail: credential.accountEmail ?? resolved.context.accountEmail,
181
+ user: raw.user ?? null,
182
+ organization: raw.organization ?? null,
183
+ };
184
+ if (options.json || args.includes('--pretty')) {
185
+ (0, output_1.writeJson)(io, payload);
186
+ }
187
+ else {
188
+ (0, output_1.writeLine)(io, payload.accountEmail ?? 'Authenticated');
189
+ (0, output_1.writeLine)(io, `Context: ${payload.contextId}`);
190
+ (0, output_1.writeLine)(io, `Workspace: ${payload.workspaceId}`);
191
+ }
192
+ return 0;
193
+ }
194
+ catch (error) {
195
+ (0, output_1.writeError)(io, error instanceof Error ? error.message : String(error));
196
+ return 1;
197
+ }
198
+ }
199
+ async function authTokenList(options, io) {
200
+ const config = await (0, tela_local_config_1.loadConfig)();
201
+ const publicConfig = await (0, tela_local_config_1.toPublicConfig)(config, (0, credentials_1.createCliCredentialAdapter)());
202
+ const tokens = Object.values(publicConfig.contexts).map(context => ({
203
+ contextId: context.id,
204
+ environmentId: context.environmentId,
205
+ accountEmail: context.accountEmail,
206
+ credential: context.credential,
207
+ }));
208
+ const legacyCredentials = await discoverLegacyTelactlCredentials();
209
+ if (options.json) {
210
+ (0, output_1.writeJson)(io, { tokens, legacyCredentials });
211
+ }
212
+ else if (tokens.length === 0 && legacyCredentials.length === 0) {
213
+ (0, output_1.writeLine)(io, 'No Tela credentials configured.');
214
+ }
215
+ else {
216
+ for (const token of tokens)
217
+ (0, output_1.writeLine)(io, `${token.contextId} ${token.accountEmail ?? '-'} ${token.credential.ok ? 'valid' : token.credential.problem ?? 'unknown'}`);
218
+ for (const legacy of legacyCredentials)
219
+ (0, output_1.writeLine)(io, `legacy:${legacy.environmentId} discovered (not imported) ${legacy.path}`);
220
+ }
221
+ return 0;
222
+ }
223
+ async function discoverLegacyTelactlCredentials() {
224
+ const root = (0, node_path_1.join)((0, node_os_1.homedir)(), '.telactl');
225
+ try {
226
+ const names = await (0, promises_1.readdir)(root);
227
+ return names
228
+ .filter(name => name.startsWith('credentials.') && name.endsWith('.json'))
229
+ .map(name => ({
230
+ environmentId: name.slice('credentials.'.length, -'.json'.length),
231
+ path: (0, node_path_1.join)(root, name),
232
+ }))
233
+ .filter(item => item.environmentId.length > 0)
234
+ .sort((left, right) => left.environmentId.localeCompare(right.environmentId));
235
+ }
236
+ catch (error) {
237
+ if (isNotFound(error))
238
+ return [];
239
+ throw error;
240
+ }
241
+ }
18
242
  async function authToken(args, options, io) {
19
243
  const parsed = parseTokenOptions(args);
20
244
  if ('error' in parsed) {
@@ -60,7 +284,10 @@ async function authToken(args, options, io) {
60
284
  }
61
285
  async function resolveToken(options) {
62
286
  const config = await (0, tela_local_config_1.loadConfig)();
63
- const resolved = (0, tela_local_config_1.resolveContext)(config, { contextId: options.contextId });
287
+ const resolved = (0, tela_local_config_1.resolveContext)(config, {
288
+ contextId: options.contextId,
289
+ environmentId: options.environmentId,
290
+ });
64
291
  const credentials = (0, credentials_1.createCliCredentialAdapter)();
65
292
  const credentialRef = resolved.context.credentialRef;
66
293
  const originalCredential = await credentials.get(credentialRef);
@@ -109,6 +336,14 @@ function parseTokenOptions(args) {
109
336
  index += 1;
110
337
  continue;
111
338
  }
339
+ if (arg === '--environment' || arg === '--auth-env') {
340
+ const value = args[index + 1];
341
+ if (!value)
342
+ return { error: 'Usage: tela auth token --environment <environment-id>' };
343
+ options.environmentId = value;
344
+ index += 1;
345
+ continue;
346
+ }
112
347
  if (arg === '--kind') {
113
348
  const value = args[index + 1];
114
349
  if (value !== 'access-token' && value !== 'data-token') {
@@ -126,6 +361,133 @@ function parseTokenOptions(args) {
126
361
  }
127
362
  return options;
128
363
  }
364
+ function parseLoginOptions(args) {
365
+ const parsed = {
366
+ interactive: Boolean(node_process_1.default.stdin.isTTY),
367
+ };
368
+ for (let index = 0; index < args.length; index += 1) {
369
+ const arg = args[index];
370
+ if (arg === '--interactive') {
371
+ parsed.interactive = true;
372
+ continue;
373
+ }
374
+ if (arg === '--no-interactive') {
375
+ parsed.interactive = false;
376
+ continue;
377
+ }
378
+ if (arg === '--context' || arg === '--environment' || arg === '--auth-env' || arg === '--auth-url' || arg === '--device-code') {
379
+ const value = args[index + 1];
380
+ if (!value)
381
+ return { error: `Missing value for ${arg}.` };
382
+ if (arg === '--context')
383
+ parsed.contextId = value;
384
+ else if (arg === '--environment' || arg === '--auth-env')
385
+ parsed.environmentId = value;
386
+ else if (arg === '--auth-url')
387
+ parsed.authUrl = value;
388
+ else
389
+ parsed.deviceCode = value;
390
+ index += 1;
391
+ continue;
392
+ }
393
+ return { error: `Unknown auth login option: ${arg}` };
394
+ }
395
+ return parsed;
396
+ }
397
+ function parseContextSelector(args, action) {
398
+ const selector = {};
399
+ for (let index = 0; index < args.length; index += 1) {
400
+ const arg = args[index];
401
+ if (arg === '--pretty' || arg === '-p' || arg === '--interactive' || arg === '--no-interactive')
402
+ continue;
403
+ if (arg === '--context') {
404
+ selector.contextId = args[index + 1];
405
+ if (!selector.contextId)
406
+ return { error: `Usage: tela auth ${action} [--context <context-id>]` };
407
+ index += 1;
408
+ continue;
409
+ }
410
+ if (arg === '--environment' || arg === '--auth-env') {
411
+ selector.environmentId = args[index + 1];
412
+ if (!selector.environmentId)
413
+ return { error: `Usage: tela auth ${action} [--environment <environment-id>]` };
414
+ index += 1;
415
+ continue;
416
+ }
417
+ if (arg === '--auth-url') {
418
+ if (!args[index + 1])
419
+ return { error: `Missing value for ${arg}.` };
420
+ index += 1;
421
+ continue;
422
+ }
423
+ return { error: `Unknown auth ${action} option: ${arg}` };
424
+ }
425
+ return selector;
426
+ }
427
+ function formatDeviceFlowInstructions(input) {
428
+ const environmentOption = input.environmentId === 'production'
429
+ ? ''
430
+ : ` --environment ${shellArg(input.environmentId)}`;
431
+ const contextOption = input.contextId ? ` --context ${shellArg(input.contextId)}` : '';
432
+ const authUrlOption = input.authUrl ? ` --auth-url ${shellArg(input.authUrl)}` : '';
433
+ return [
434
+ 'To sign in, open this URL and authorize access:',
435
+ ` ${input.verificationUriComplete}`,
436
+ '',
437
+ 'If asked for a code, enter:',
438
+ ` ${input.userCode}`,
439
+ '',
440
+ 'Then run:',
441
+ ` tela auth login --device-code ${shellArg(input.deviceCode)}${environmentOption}${contextOption}${authUrlOption}`,
442
+ ].join('\n');
443
+ }
444
+ function openExternal(url) {
445
+ const command = node_process_1.default.platform === 'darwin'
446
+ ? 'open'
447
+ : node_process_1.default.platform === 'win32' ? 'rundll32' : 'xdg-open';
448
+ const args = node_process_1.default.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
449
+ const child = (0, node_child_process_1.spawn)(command, args, { detached: true, stdio: 'ignore' });
450
+ child.unref();
451
+ }
452
+ function resolveSafeVerificationUrl(rawUrl, { allowLoopbackHttp = false } = {}) {
453
+ const candidate = parseUrl(rawUrl);
454
+ if (!candidate || candidate.username || candidate.password)
455
+ return null;
456
+ if (candidate.protocol === 'https:')
457
+ return candidate.href;
458
+ if (candidate.protocol === 'http:' && allowLoopbackHttp && isLoopbackHostname(candidate.hostname))
459
+ return candidate.href;
460
+ return null;
461
+ }
462
+ function parseUrl(rawUrl) {
463
+ try {
464
+ return new URL(rawUrl);
465
+ }
466
+ catch {
467
+ return null;
468
+ }
469
+ }
470
+ function isLoopbackHostname(rawHostname) {
471
+ const hostname = rawHostname.toLowerCase().replace(/^\[|\]$/g, '');
472
+ return hostname === 'localhost'
473
+ || hostname.endsWith('.localhost')
474
+ || hostname === '::1'
475
+ || /^127(?:\.\d{1,3}){3}$/.test(hostname);
476
+ }
477
+ function shellArg(value) {
478
+ return `'${value.replaceAll("'", "'\\''")}'`;
479
+ }
480
+ function isRecord(value) {
481
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
482
+ }
483
+ function isNotFound(error) {
484
+ return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
485
+ }
129
486
  function writeAuthHelp(io) {
130
- (0, output_1.writeLine)(io, 'Usage: tela auth token [--context <context-id>] [--kind access-token|data-token] [--raw] [--json]');
487
+ (0, output_1.writeLine)(io, 'Usage: tela auth <command> [options]');
488
+ (0, output_1.writeLine)(io, ' login [--context <id>|--environment <id>] [--interactive|--no-interactive] [--device-code <code>]');
489
+ (0, output_1.writeLine)(io, ' logout [--context <id>|--all]');
490
+ (0, output_1.writeLine)(io, ' whoami [--context <id>] [--json]');
491
+ (0, output_1.writeLine)(io, ' token [--context <id>|--environment <id>] [--kind access-token|data-token] [--raw] [--json]');
492
+ (0, output_1.writeLine)(io, ' token list [--json]');
131
493
  }
@@ -1,38 +1,10 @@
1
+ import type { StoredCredential, TelaServiceIngredient } from '@meistrari/tela-local-config';
2
+ import { extractGitHubFilePath, joinUrl, parseHttpieArg, resolveGuidePath } from '@meistrari/tela-pantry';
1
3
  import type { CliIo, GlobalOptions } from '../output';
2
- export type CachedEnvironment = {
3
- slug: string;
4
- url: string;
5
- };
6
- export type CachedIngredient = {
7
- slug: string;
8
- name: string;
9
- description: string;
10
- type: 'api' | 'library' | 'skill';
11
- status: string;
12
- environments: CachedEnvironment[];
13
- };
14
- type ParsedArg = {
15
- kind: 'query';
16
- key: string;
17
- value: string;
18
- } | {
19
- kind: 'header';
20
- key: string;
21
- value: string;
22
- } | {
23
- kind: 'field';
24
- key: string;
25
- value: string;
26
- } | {
27
- kind: 'rawField';
28
- key: string;
29
- value: string;
30
- };
4
+ export { extractGitHubFilePath, joinUrl, parseHttpieArg, resolveGuidePath, };
5
+ export type CachedEnvironment = TelaServiceIngredient['environments'][number];
6
+ export type CachedIngredient = TelaServiceIngredient;
7
+ export declare function runServiceCommand(args: string[], options: GlobalOptions, io: CliIo): Promise<number>;
31
8
  export declare function runTelactlCommand(command: string, args: string[], options: GlobalOptions, io: CliIo): Promise<number>;
32
9
  export declare function runIngredientEnvCommand(args: string[], options: GlobalOptions, io: CliIo): Promise<number | null>;
33
- export declare function fetchIngredientCatalog(headers: Record<string, string>, fetchImpl?: typeof fetch): Promise<CachedIngredient[]>;
34
- export declare function joinUrl(base: string, routePath: string): string;
35
- export declare function parseHttpieArg(arg: string): ParsedArg | null;
36
- export declare function extractGitHubFilePath(url: string): string | null;
37
- export declare function resolveGuidePath(rootFilePath: string, providedPath?: string): string;
38
- export {};
10
+ export declare function fetchIngredientCatalog(credential: StoredCredential, baseUrl?: string): Promise<TelaServiceIngredient[]>;