@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.
- 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/index.js +136440 -31415
- 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
|
@@ -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>;
|
package/dist/markdown.js
ADDED
|
@@ -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 === '
|
|
34
|
-
return await (0,
|
|
35
|
-
if (command === '
|
|
36
|
-
return await (0,
|
|
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
|
|
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, '
|
|
73
|
-
(0, output_1.writeLine)(io, ' api <slug> <METHOD> <PATH> [args...] [--env <slug>] [
|
|
74
|
-
(0, output_1.writeLine)(io, ' docs
|
|
75
|
-
(0, output_1.writeLine)(io, '
|
|
76
|
-
(0, output_1.writeLine)(io, '
|
|
77
|
-
(0, output_1.writeLine)(io, '
|
|
78
|
-
(0, output_1.writeLine)(io, '
|
|
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;
|
package/dist/session.js
ADDED
|
@@ -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
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -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>;
|