@meistrari/tela-cli 1.1.0 → 1.2.1

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.
@@ -0,0 +1,17 @@
1
+ export type MarkdownTarget = {
2
+ isTTY?: boolean;
3
+ columns?: number;
4
+ };
5
+ /**
6
+ * Render markdown for a terminal, or hand it back untouched.
7
+ *
8
+ * Guides are markdown source, and markdown source is what a pipe or a file
9
+ * should receive — reflowed text and ANSI escapes would corrupt anything
10
+ * reading the output as data. Formatting is therefore reserved for an
11
+ * interactive terminal, where nobody is parsing it.
12
+ *
13
+ * `NO_COLOR` opts out too: it signals a reader that wants plain text, and
14
+ * partial rendering — restructured layout without the styling that made it
15
+ * legible — is worse than none.
16
+ */
17
+ export declare function renderMarkdown(markdown: string, target?: MarkdownTarget, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderMarkdown = renderMarkdown;
7
+ const node_process_1 = __importDefault(require("node:process"));
8
+ const DEFAULT_WIDTH = 80;
9
+ const MAX_WIDTH = 100;
10
+ /**
11
+ * Fence languages highlight.js has a working grammar for but does not register
12
+ * under the name people write. Registering them as real aliases means the fence
13
+ * keeps its own name and gets proper highlighting.
14
+ *
15
+ * Each of these was checked against the grammar rather than assumed:
16
+ *
17
+ * - `jsonc` — the `json` grammar already accepts line and block comments and
18
+ * trailing commas, and marks them up correctly.
19
+ * - `mts`/`cts` — TypeScript's own module-file extensions.
20
+ * - `dotenv`/`env` — `ini` marks up `KEY=value`, `#` comments and quoted values.
21
+ *
22
+ * `json5` is deliberately absent: the `json` grammar rejects unquoted keys and
23
+ * single-quoted strings outright, so it would highlight nothing.
24
+ */
25
+ const LANGUAGE_ALIASES = {
26
+ jsonc: 'json',
27
+ mts: 'typescript',
28
+ cts: 'typescript',
29
+ dotenv: 'ini',
30
+ env: 'ini',
31
+ };
32
+ /**
33
+ * Render markdown for a terminal, or hand it back untouched.
34
+ *
35
+ * Guides are markdown source, and markdown source is what a pipe or a file
36
+ * should receive — reflowed text and ANSI escapes would corrupt anything
37
+ * reading the output as data. Formatting is therefore reserved for an
38
+ * interactive terminal, where nobody is parsing it.
39
+ *
40
+ * `NO_COLOR` opts out too: it signals a reader that wants plain text, and
41
+ * partial rendering — restructured layout without the styling that made it
42
+ * legible — is worse than none.
43
+ */
44
+ async function renderMarkdown(markdown, target = node_process_1.default.stdout, env = node_process_1.default.env) {
45
+ if (!target.isTTY || env.NO_COLOR)
46
+ return markdown;
47
+ try {
48
+ return await formatMarkdown(markdown, terminalWidth(target));
49
+ }
50
+ catch {
51
+ // A renderer failure must not cost the reader their document.
52
+ return markdown;
53
+ }
54
+ }
55
+ async function formatMarkdown(markdown, width) {
56
+ // Imported lazily: marked-terminal pulls in a syntax highlighter and costs
57
+ // ~54ms to load, which every other `tela` command would otherwise pay for
58
+ // a renderer it never uses.
59
+ const { Marked } = await import('marked');
60
+ // @ts-expect-error marked-terminal ships no declarations, and
61
+ // @types/marked-terminal is still on v6 while the runtime here is v7;
62
+ // MarkedTerminal above describes the one export used.
63
+ const { markedTerminal } = await import('marked-terminal');
64
+ const { supportsLanguage } = await import('cli-highlight');
65
+ await registerLanguageAliases();
66
+ // A fresh instance rather than the `marked` singleton: `marked.use()` is
67
+ // global, and this must not change how anything else parses markdown.
68
+ const renderer = new Marked();
69
+ renderer.use(markedTerminal({ width, reflowText: true, tab: 2 }));
70
+ renderer.use({
71
+ /**
72
+ * Anything still unrecognised — `mermaid`, `hcl`, whatever a README
73
+ * invents — renders as plain code. highlight.js would otherwise print
74
+ * "Could not find the language 'x'" to stderr and carry on, turning an
75
+ * ordinary fence into what reads like a CLI error.
76
+ */
77
+ walkTokens(token) {
78
+ if (token.type !== 'code')
79
+ return;
80
+ // marked types `lang` loosely across the token union.
81
+ const fence = token;
82
+ const declared = typeof fence.lang === 'string' ? fence.lang : '';
83
+ const language = declared.trim().split(/\s+/)[0] ?? '';
84
+ if (language && !supportsLanguage(language))
85
+ fence.lang = '';
86
+ },
87
+ });
88
+ const rendered = renderer.parse(markdown);
89
+ if (typeof rendered !== 'string')
90
+ throw new TypeError('marked returned a promise');
91
+ return rendered;
92
+ }
93
+ let aliasesRegistered = false;
94
+ /**
95
+ * Teach highlight.js the alias names, once per process.
96
+ *
97
+ * `cli-highlight` resolves the same `highlight.js` singleton this imports, so
98
+ * registering here is what makes `supportsLanguage('jsonc')` true and gets the
99
+ * fence highlighted under its own name.
100
+ */
101
+ async function registerLanguageAliases() {
102
+ if (aliasesRegistered)
103
+ return;
104
+ aliasesRegistered = true;
105
+ const hljs = (await import('highlight.js')).default;
106
+ for (const [alias, language] of Object.entries(LANGUAGE_ALIASES)) {
107
+ if (!hljs.getLanguage(alias) && hljs.getLanguage(language))
108
+ hljs.registerAliases([alias], { languageName: language });
109
+ }
110
+ }
111
+ function terminalWidth(target) {
112
+ const columns = target.columns ?? DEFAULT_WIDTH;
113
+ // Long measures are hard to read, and a very narrow terminal reflows to
114
+ // nothing useful.
115
+ return Math.max(40, Math.min(columns, MAX_WIDTH));
116
+ }
package/dist/program.js CHANGED
@@ -3,14 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runCli = runCli;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
- const tela_agent_1 = require("@meistrari/tela-agent");
7
- const cli_1 = require("@meistrari/workflow-code/cli");
8
6
  const output_1 = require("./output");
9
7
  const auth_1 = require("./commands/auth");
10
8
  const context_1 = require("./commands/context");
11
9
  const doctor_auth_1 = require("./commands/doctor-auth");
12
10
  const env_1 = require("./commands/env");
13
11
  const ingredients_1 = require("./commands/ingredients");
12
+ const skills_1 = require("./commands/skills");
14
13
  async function runCli(argv, io) {
15
14
  const { args, options } = parseGlobalOptions(argv);
16
15
  const [command, ...rest] = args;
@@ -30,10 +29,18 @@ async function runCli(argv, io) {
30
29
  return await (0, env_1.runEnvCommand)(rest, options, io);
31
30
  if (command === 'doctor')
32
31
  return await (0, doctor_auth_1.runDoctorCommand)(rest, options, io);
33
- if (command === 'agent')
34
- return await (0, tela_agent_1.runAgentCli)(rest, io, options);
35
- if (command === 'workflow')
36
- return await (0, cli_1.runWorkflowCli)(rest, io, options);
32
+ if (command === 'service')
33
+ return await (0, ingredients_1.runServiceCommand)(rest, options, io);
34
+ if (command === 'skills' || command === 'skill')
35
+ return await (0, skills_1.runSkillsCommand)(rest, options, io);
36
+ if (command === 'agent') {
37
+ const { runAgentCli } = await import('@meistrari/tela-agent');
38
+ return await runAgentCli(rest, io, options);
39
+ }
40
+ if (command === 'workflow') {
41
+ const { runWorkflowCli } = await import('@meistrari/workflow-code/cli');
42
+ return await runWorkflowCli(rest, io, options);
43
+ }
37
44
  if (command === 'update' || command === 'api' || command === 'docs' || command === 'whoami' || command === 'token') {
38
45
  return await (0, ingredients_1.runTelactlCommand)(command, rest, options, io);
39
46
  }
@@ -60,7 +67,7 @@ function writeHelp(io) {
60
67
  (0, output_1.writeLine)(io, 'Usage: tela <command> [args]');
61
68
  (0, output_1.writeLine)(io);
62
69
  (0, output_1.writeLine)(io, 'Commands:');
63
- (0, output_1.writeLine)(io, ' auth token [--context <context-id>] [--kind access-token|data-token] [--raw] [--json]');
70
+ (0, output_1.writeLine)(io, ' auth login|logout|whoami|token [options]');
64
71
  (0, output_1.writeLine)(io, ' context current [--json]');
65
72
  (0, output_1.writeLine)(io, ' context list [--json]');
66
73
  (0, output_1.writeLine)(io, ' context show <context-id> [--json]');
@@ -69,13 +76,14 @@ function writeHelp(io) {
69
76
  (0, output_1.writeLine)(io, ' env list <ingredient-slug> [-p|--pretty|--json]');
70
77
  (0, output_1.writeLine)(io, ' env set <ingredient-slug> <environment>');
71
78
  (0, output_1.writeLine)(io, ' doctor auth [--json]');
72
- (0, output_1.writeLine)(io, ' update [--context <context-id>] [--json]');
73
- (0, output_1.writeLine)(io, ' api <slug> <METHOD> <PATH> [args...] [--env <slug>] [--body <json>] [-p|--pretty]');
74
- (0, output_1.writeLine)(io, ' docs endpoints <slug> [-p|--pretty] [--json]');
75
- (0, output_1.writeLine)(io, ' docs reference <slug> [METHOD PATH|file-path] [-p|--pretty] [--json]');
76
- (0, output_1.writeLine)(io, ' docs guide <slug> [path] [-p|--pretty]');
77
- (0, output_1.writeLine)(io, ' whoami [--context <context-id>] [-p|--pretty]');
78
- (0, output_1.writeLine)(io, ' token [--context <context-id>]');
79
+ (0, output_1.writeLine)(io, ' service list|show|api|docs|env [options]');
80
+ (0, output_1.writeLine)(io, ' service api <slug> <METHOD> <PATH> [args...] [--service-env <slug>] [-p|--pretty]');
81
+ (0, output_1.writeLine)(io, ' service docs reference <slug> [METHOD PATH|file-path] [-p|--pretty] [--json]');
82
+ (0, output_1.writeLine)(io, ' skills list|search|files|cat [options]');
83
+ (0, output_1.writeLine)(io, ' skills install <slug...> [-f|--force] [--no-symlink] [--ignore-scripts] [--json]');
84
+ (0, output_1.writeLine)(io, ' skills upgrade [slug...] [--ignore-scripts] [--json]');
85
+ (0, output_1.writeLine)(io, ' skills uninstall <slug...> [--ignore-scripts] [--json]');
86
+ (0, output_1.writeLine)(io, ' api|docs|whoami|token ... Legacy compatibility aliases');
79
87
  (0, output_1.writeLine)(io, ' version');
80
88
  (0, output_1.writeLine)(io, ' workflow init [--directory <path>] [--config <path>] [--workflow <key>...] [--yes] [--json]');
81
89
  (0, output_1.writeLine)(io, ' workflow compile [target] [--config <path>] [--env <path>] [--json]');
@@ -0,0 +1,28 @@
1
+ import type { CredentialRef, ResolvedContext, StoredCredential, TelaLocalConfig } from '@meistrari/tela-local-config';
2
+ import type { PantryClient } from '@meistrari/tela-pantry';
3
+ export type Session = {
4
+ resolved: ResolvedContext;
5
+ credentialRef: CredentialRef;
6
+ credential: StoredCredential;
7
+ refreshed: boolean;
8
+ };
9
+ /** What opening a Pantry client needs in order to pick a context. */
10
+ export type CatalogFlags = {
11
+ contextId?: string;
12
+ environmentId?: string;
13
+ };
14
+ export type PantrySession = {
15
+ client: PantryClient;
16
+ baseUrl: string;
17
+ };
18
+ /**
19
+ * Open an authenticated Pantry client for the selected context.
20
+ *
21
+ * There is no local catalog: Pantry runs in one environment and answers in
22
+ * ~200ms, so every command reads it live rather than keeping a copy that has to
23
+ * be invalidated.
24
+ */
25
+ export declare function loadPantry(flags: CatalogFlags): Promise<PantrySession>;
26
+ export declare function loadPantryForContext(resolved: ResolvedContext, config?: TelaLocalConfig): Promise<PantrySession>;
27
+ export declare function loadSession(contextId?: string, environmentId?: string): Promise<Session>;
28
+ export declare function resolvePantryBaseUrl(resolved: ResolvedContext): string;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadPantry = loadPantry;
4
+ exports.loadPantryForContext = loadPantryForContext;
5
+ exports.loadSession = loadSession;
6
+ exports.resolvePantryBaseUrl = resolvePantryBaseUrl;
7
+ const tela_local_config_1 = require("@meistrari/tela-local-config");
8
+ const tela_pantry_1 = require("@meistrari/tela-pantry");
9
+ const credentials_1 = require("./credentials");
10
+ const ENV_ACCESS_TOKEN_REF = {
11
+ provider: 'env',
12
+ service: 'env',
13
+ account: 'TELA_ACCESS_TOKEN',
14
+ kind: 'user-session',
15
+ };
16
+ /**
17
+ * Open an authenticated Pantry client for the selected context.
18
+ *
19
+ * There is no local catalog: Pantry runs in one environment and answers in
20
+ * ~200ms, so every command reads it live rather than keeping a copy that has to
21
+ * be invalidated.
22
+ */
23
+ async function loadPantry(flags) {
24
+ const config = await (0, tela_local_config_1.loadConfig)();
25
+ const resolved = (0, tela_local_config_1.resolveContext)(config, {
26
+ contextId: flags.contextId,
27
+ environmentId: flags.environmentId,
28
+ });
29
+ return await loadPantryForContext(resolved, config);
30
+ }
31
+ async function loadPantryForContext(resolved, config) {
32
+ const loadedConfig = config ?? await (0, tela_local_config_1.loadConfig)();
33
+ const baseUrl = resolvePantryBaseUrl(resolved);
34
+ const credential = await loadPantryCredential(loadedConfig, resolved);
35
+ return { client: (0, tela_pantry_1.createPantryClient)(baseUrl, credential), baseUrl };
36
+ }
37
+ async function loadPantryCredential(config, selected) {
38
+ const credentials = (0, credentials_1.createCliCredentialAdapter)();
39
+ const candidates = pantryCredentialCandidates(config, selected);
40
+ for (const candidate of candidates) {
41
+ const originalCredential = await credentials.get(candidate.credentialRef);
42
+ if (!originalCredential)
43
+ continue;
44
+ const credential = originalCredential.kind === 'user-session'
45
+ ? await (0, tela_local_config_1.refreshUserSessionCredential)(candidate.resolved.environment, candidate.credentialRef, originalCredential, credentials)
46
+ : originalCredential;
47
+ if (credential.accessToken)
48
+ return credential;
49
+ }
50
+ throw new Error(`Pantry discovery requires a user-session access token for Tela environment '${selected.context.environmentId}'. `
51
+ + 'Run `tela auth login` or set TELA_ACCESS_TOKEN.');
52
+ }
53
+ function pantryCredentialCandidates(config, selected) {
54
+ const candidates = [];
55
+ const seen = new Set();
56
+ const addContext = (contextId) => {
57
+ if (!contextId || seen.has(contextId))
58
+ return;
59
+ const context = config.contexts[contextId];
60
+ if (!context || context.environmentId !== selected.context.environmentId)
61
+ return;
62
+ if (context.credentialRef.kind !== 'user-session')
63
+ return;
64
+ const environment = config.environments[context.environmentId];
65
+ if (!environment)
66
+ return;
67
+ seen.add(contextId);
68
+ candidates.push({
69
+ resolved: {
70
+ id: contextId,
71
+ context,
72
+ environment,
73
+ },
74
+ credentialRef: context.credentialRef,
75
+ });
76
+ };
77
+ if (selected.context.credentialRef.kind === 'user-session')
78
+ addContext(selected.id);
79
+ addContext(config.activeContextId);
80
+ for (const contextId of Object.keys(config.contexts))
81
+ addContext(contextId);
82
+ candidates.push({ resolved: selected, credentialRef: ENV_ACCESS_TOKEN_REF });
83
+ return candidates;
84
+ }
85
+ async function loadSession(contextId, environmentId) {
86
+ const config = await (0, tela_local_config_1.loadConfig)();
87
+ const resolved = (0, tela_local_config_1.resolveContext)(config, { contextId, environmentId });
88
+ const credentials = (0, credentials_1.createCliCredentialAdapter)();
89
+ const credentialRef = resolved.context.credentialRef;
90
+ const originalCredential = await credentials.get(credentialRef);
91
+ if (!originalCredential)
92
+ throw new Error(`Missing credential for Tela context ${resolved.id}`);
93
+ const credential = originalCredential.kind === 'user-session'
94
+ ? await (0, tela_local_config_1.refreshUserSessionCredential)(resolved.environment, credentialRef, originalCredential, credentials)
95
+ : originalCredential;
96
+ return {
97
+ resolved,
98
+ credentialRef,
99
+ credential,
100
+ refreshed: credential !== originalCredential,
101
+ };
102
+ }
103
+ function resolvePantryBaseUrl(resolved) {
104
+ const baseUrl = resolved.environment.services.pantry;
105
+ if (baseUrl)
106
+ return baseUrl;
107
+ throw new Error(`Tela environment '${resolved.context.environmentId}' does not configure a Pantry service URL. `
108
+ + 'Select a context whose environment defines services.pantry.');
109
+ }
@@ -0,0 +1,87 @@
1
+ import type { SkillFileContent } from '@meistrari/tela-pantry';
2
+ export type SkillRoots = {
3
+ /** Where the skill's files actually live. */
4
+ installRoot: string;
5
+ /** Where a symlink back to the install directory is created. */
6
+ linkRoot: string;
7
+ };
8
+ export type StagedSkillFiles = {
9
+ commit: () => Promise<void>;
10
+ rollback: () => Promise<void>;
11
+ };
12
+ /**
13
+ * One real copy under `~/.agents/skills`, symlinked into `~/.claude/skills`.
14
+ *
15
+ * Agent tooling reads a fixed pair of locations, so there is nowhere else worth
16
+ * installing to; the roots stay parameterised only so tests can work inside a
17
+ * temporary home.
18
+ */
19
+ export declare function resolveSkillRoots(home?: string): SkillRoots;
20
+ /**
21
+ * A slug reaches `path.join` and, eventually, a recursive delete. `path.join`
22
+ * collapses `..`, so validation has to happen before the join — and both
23
+ * clauses are needed: `isSafeSkillPath` is an archive-path predicate that
24
+ * accepts `a/b`, which as a slug is a traversal into a sibling directory.
25
+ */
26
+ export declare function assertSafeSlug(slug: string, action: string): void;
27
+ export declare function resolveInstallDir(roots: SkillRoots, slug: string): string;
28
+ /**
29
+ * The install guard: is something already sitting at the target that this CLI
30
+ * did not put there?
31
+ *
32
+ * Compares the *recorded directory* rather than asking whether the slug is
33
+ * registered at all. A registration pointing somewhere else is not permission
34
+ * to replace what is at this path — which may be a directory the user created
35
+ * by hand, or one another tool owns.
36
+ */
37
+ export declare function isUnownedInstallDir(target: string, registeredDir: string | null): boolean;
38
+ /**
39
+ * Write the verified files to `target`, replacing whatever is there.
40
+ *
41
+ * Nothing is deleted while the target is at risk. Everything is extracted to a
42
+ * staging directory first, the previous install is renamed aside, the staged
43
+ * copy is renamed in, and only then is the old copy dropped. Every destructive
44
+ * step is a rename — which either happens or does not — and each has an
45
+ * inverse, so an interrupted install can never leave a half-written skill where
46
+ * a working one used to be.
47
+ */
48
+ export declare function materializeSkillFiles(target: string, files: readonly SkillFileContent[]): Promise<void>;
49
+ export declare function stageSkillFiles(target: string, files: readonly SkillFileContent[]): Promise<StagedSkillFiles>;
50
+ export type LinkOutcome = {
51
+ linked: string[];
52
+ /** Set when linking was skipped, explaining what is in the way. */
53
+ skipped: string | null;
54
+ };
55
+ /**
56
+ * Symlink the install directory into the link root.
57
+ *
58
+ * A real directory already at the link path is left alone unless `force` says
59
+ * otherwise: it is something another tool or the user put there, and replacing
60
+ * it silently would delete files this CLI never wrote. A symlink pointing
61
+ * somewhere other than the install directory gets the same treatment.
62
+ */
63
+ export declare function linkSkill(roots: SkillRoots, slug: string, target: string, force: boolean): Promise<LinkOutcome>;
64
+ /**
65
+ * Remove a symlink this CLI created that still points at the install directory.
66
+ *
67
+ * Every other case is reported and skipped. The link may since have been
68
+ * replaced by a real directory belonging to something else, and uninstalling a
69
+ * skill is not licence to delete it.
70
+ */
71
+ export declare function removeManagedSymlink(linkPath: string, target: string): Promise<{
72
+ removed: boolean;
73
+ reason: string | null;
74
+ }>;
75
+ export type ScriptOutcome = {
76
+ ok: boolean;
77
+ output: string;
78
+ };
79
+ /**
80
+ * Run a skill's declared install or uninstall script, in the skill's own
81
+ * directory.
82
+ *
83
+ * Both streams are captured and handed back rather than inherited: the script
84
+ * is arbitrary shell from the catalog, and letting it write to stdout would
85
+ * corrupt `--json` output. The caller relays them as diagnostics on stderr.
86
+ */
87
+ export declare function runSkillScript(script: string, cwd: string): Promise<ScriptOutcome>;