codex-template 0.1.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.
@@ -0,0 +1,54 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export function destRoots({ target, scope = 'global', directory, home = os.homedir(), }) {
4
+ if (target !== 'cursor' && target !== 'codex') {
5
+ throw new Error(`Unknown target "${target}"`);
6
+ }
7
+ const project = scope === 'project';
8
+ if (project && !directory) {
9
+ throw new Error('Project installs require a directory');
10
+ }
11
+ const projectRoot = project ? path.resolve(directory) : null;
12
+ if (target === 'cursor') {
13
+ const base = project ? path.join(projectRoot, '.cursor') : path.join(home, '.cursor');
14
+ return {
15
+ target,
16
+ scope: project ? 'project' : 'global',
17
+ base,
18
+ skills: path.join(base, 'skills'),
19
+ commands: path.join(base, 'commands'),
20
+ agents: path.join(base, 'agents'),
21
+ mcpFile: path.join(base, 'mcp.json'),
22
+ manifest: path.join(base, '.aitmpl-manifest.json'),
23
+ };
24
+ }
25
+ const base = project ? path.join(projectRoot, '.codex') : path.join(home, '.codex');
26
+ const skills = project
27
+ ? path.join(projectRoot, '.agents', 'skills')
28
+ : path.join(home, '.agents', 'skills');
29
+ return {
30
+ target,
31
+ scope: project ? 'project' : 'global',
32
+ base,
33
+ skills,
34
+ commands: skills,
35
+ agents: path.join(base, 'agents'),
36
+ mcpFile: path.join(base, 'config.toml'),
37
+ manifest: path.join(base, '.aitmpl-manifest.json'),
38
+ };
39
+ }
40
+ export function componentDest({ target, roots, type, name, }) {
41
+ if (type === 'skill')
42
+ return { kind: 'dir', path: path.join(roots.skills, name) };
43
+ if (type === 'command' && target === 'cursor')
44
+ return { kind: 'file', path: path.join(roots.commands, `${name}.md`) };
45
+ if (type === 'command' && target === 'codex')
46
+ return { kind: 'dir', path: path.join(roots.commands, name) };
47
+ if (type === 'agent' && target === 'cursor')
48
+ return { kind: 'file', path: path.join(roots.agents, `${name}.md`) };
49
+ if (type === 'agent' && target === 'codex')
50
+ return { kind: 'file', path: path.join(roots.agents, `${name}.toml`) };
51
+ if (type === 'mcp')
52
+ return { kind: 'merge', path: roots.mcpFile };
53
+ throw new Error(`Unknown component type "${type}"`);
54
+ }
@@ -0,0 +1,80 @@
1
+ export type Target = 'cursor' | 'codex';
2
+ export type ComponentType = 'skill' | 'command' | 'agent' | 'mcp';
3
+ export type Scope = 'global' | 'project';
4
+ export interface SkillFile {
5
+ repoPath: string;
6
+ rel: string;
7
+ }
8
+ export interface ComponentRecord {
9
+ type: ComponentType;
10
+ id: string;
11
+ name: string;
12
+ category: string;
13
+ repoPath?: string;
14
+ repoDir?: string;
15
+ primaryPath: string;
16
+ files: SkillFile[];
17
+ }
18
+ export interface InstallRoots {
19
+ target: Target;
20
+ scope: Scope;
21
+ base: string;
22
+ skills: string;
23
+ commands: string;
24
+ agents: string;
25
+ mcpFile: string;
26
+ manifest: string;
27
+ }
28
+ export interface Dest {
29
+ kind: 'dir' | 'file' | 'merge';
30
+ path: string;
31
+ }
32
+ export interface ManifestEntry {
33
+ type: ComponentType;
34
+ id: string;
35
+ name: string;
36
+ mcpServers: string[];
37
+ }
38
+ export interface Manifest {
39
+ version: 1;
40
+ components: Record<string, ManifestEntry>;
41
+ }
42
+ export interface Catalog {
43
+ resolve(type: ComponentType, id: string): Promise<ComponentRecord>;
44
+ list(type?: ComponentType | null): Promise<ComponentRecord[]>;
45
+ search(type: ComponentType, query: string): Promise<ComponentRecord[]>;
46
+ descriptionFor(record: ComponentRecord): Promise<string>;
47
+ readFile(repoPath: string): Promise<Buffer | null>;
48
+ }
49
+ export interface CliOptions {
50
+ skill: string[];
51
+ command: string[];
52
+ agent: string[];
53
+ mcp: string[];
54
+ directory: string | null;
55
+ yes: boolean;
56
+ dryRun: boolean;
57
+ verbose: boolean;
58
+ debug: boolean;
59
+ list: true | ComponentType | null;
60
+ installed: boolean;
61
+ doctor: boolean;
62
+ search: {
63
+ type: ComponentType;
64
+ query: string;
65
+ } | null;
66
+ info: {
67
+ type: ComponentType;
68
+ id: string;
69
+ } | null;
70
+ remove: {
71
+ type: ComponentType;
72
+ id: string;
73
+ } | null;
74
+ update: {
75
+ all: true;
76
+ } | {
77
+ type: ComponentType;
78
+ id: string;
79
+ } | null;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export { runCli } from './cli/run.js';
2
+ export { installById as installComponent, removeInstalled as removeComponent } from './core/install.js';
3
+ export { collectInstalled as listInstalled } from './core/lifecycle.js';
4
+ export { doctor as checkInstall } from './core/doctor.js';
5
+ export { destRoots } from './core/paths.js';
6
+ export { createCatalog } from './services/github.js';
7
+ export type { Target, ComponentType, Catalog, CliOptions, InstallRoots } from './core/types.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { runCli } from './cli/run.js';
2
+ export { installById as installComponent, removeInstalled as removeComponent } from './core/install.js';
3
+ export { collectInstalled as listInstalled } from './core/lifecycle.js';
4
+ export { doctor as checkInstall } from './core/doctor.js';
5
+ export { destRoots } from './core/paths.js';
6
+ export { createCatalog } from './services/github.js';
@@ -0,0 +1,28 @@
1
+ import type { Catalog, ComponentRecord, ComponentType } from '../core/types.js';
2
+ interface TreeEntry {
3
+ type?: string;
4
+ path?: string;
5
+ }
6
+ interface CatalogIndex {
7
+ skill: Map<string, ComponentRecord>;
8
+ command: Map<string, ComponentRecord>;
9
+ agent: Map<string, ComponentRecord>;
10
+ mcp: Map<string, ComponentRecord>;
11
+ skillByName: Map<string, ComponentRecord[]>;
12
+ commandByName: Map<string, ComponentRecord[]>;
13
+ agentByName: Map<string, ComponentRecord[]>;
14
+ mcpByName: Map<string, ComponentRecord[]>;
15
+ }
16
+ export declare function rawUrl(repoPath: string): string;
17
+ export declare function githubBlobUrl(repoPath: string): string;
18
+ export declare function indexFromTree(tree: TreeEntry[]): CatalogIndex;
19
+ export declare function resolveFromIndex(index: CatalogIndex, type: ComponentType, id: string): ComponentRecord;
20
+ export declare function listFromIndex(index: CatalogIndex, type?: ComponentType | null): ComponentRecord[];
21
+ export declare function filterRecords(records: ComponentRecord[], query: string, descriptions?: Map<string, string>): ComponentRecord[];
22
+ export declare function descriptionsFromCatalog(json: unknown): Map<string, string>;
23
+ export declare function createCatalog({ fetchImpl, token, verbose, }?: {
24
+ fetchImpl?: typeof fetch;
25
+ token?: string;
26
+ verbose?: boolean;
27
+ }): Catalog;
28
+ export {};
@@ -0,0 +1,211 @@
1
+ import { BRANCH, CATALOG_GROUPS, COMPONENTS_PREFIX, REPO, USER_AGENT } from '../config/constants.js';
2
+ import { catalogId } from '../utils/ids.js';
3
+ export function rawUrl(repoPath) {
4
+ return `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${repoPath}`;
5
+ }
6
+ export function githubBlobUrl(repoPath) {
7
+ return `https://github.com/${REPO}/blob/${BRANCH}/${repoPath}`;
8
+ }
9
+ export function indexFromTree(tree) {
10
+ const index = {
11
+ skill: new Map(),
12
+ command: new Map(),
13
+ agent: new Map(),
14
+ mcp: new Map(),
15
+ skillByName: new Map(),
16
+ commandByName: new Map(),
17
+ agentByName: new Map(),
18
+ mcpByName: new Map(),
19
+ };
20
+ const skillFiles = new Map();
21
+ for (const entry of tree) {
22
+ if (!entry || entry.type !== 'blob' || typeof entry.path !== 'string')
23
+ continue;
24
+ if (!entry.path.startsWith(COMPONENTS_PREFIX))
25
+ continue;
26
+ const rest = entry.path.slice(COMPONENTS_PREFIX.length);
27
+ const parts = rest.split('/');
28
+ const kind = parts[0];
29
+ if (kind === 'skills' && parts.length >= 3) {
30
+ const category = parts[1];
31
+ const name = parts[2];
32
+ const rel = parts.slice(3).join('/');
33
+ if (!rel || rel.split('/').includes('..'))
34
+ continue;
35
+ const id = catalogId(category, name);
36
+ if (!skillFiles.has(id)) {
37
+ skillFiles.set(id, {
38
+ type: 'skill',
39
+ id,
40
+ name,
41
+ category,
42
+ repoDir: `${COMPONENTS_PREFIX}skills/${category}/${name}`,
43
+ primaryPath: `${COMPONENTS_PREFIX}skills/${category}/${name}/SKILL.md`,
44
+ files: [],
45
+ });
46
+ }
47
+ skillFiles.get(id)?.files.push({ repoPath: entry.path, rel });
48
+ continue;
49
+ }
50
+ if (parts.length !== 3)
51
+ continue;
52
+ const category = parts[1];
53
+ const file = parts[2];
54
+ if (kind === 'commands' && file.endsWith('.md'))
55
+ addFlat(index, 'command', category, file.replace(/\.md$/, ''), entry.path);
56
+ else if (kind === 'agents' && file.endsWith('.md'))
57
+ addFlat(index, 'agent', category, file.replace(/\.md$/, ''), entry.path);
58
+ else if (kind === 'mcps' && file.endsWith('.json'))
59
+ addFlat(index, 'mcp', category, file.replace(/\.json$/, ''), entry.path);
60
+ }
61
+ for (const record of skillFiles.values()) {
62
+ record.files.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
63
+ index.skill.set(record.id, record);
64
+ pushName(index.skillByName, record);
65
+ }
66
+ return index;
67
+ }
68
+ function addFlat(index, type, category, name, repoPath) {
69
+ const record = {
70
+ type,
71
+ id: catalogId(category, name),
72
+ name,
73
+ category,
74
+ repoPath,
75
+ primaryPath: repoPath,
76
+ files: [{ repoPath, rel: repoPath.split('/').pop() || name }],
77
+ };
78
+ index[type].set(record.id, record);
79
+ pushName(index[`${type}ByName`], record);
80
+ }
81
+ function pushName(map, record) {
82
+ const list = map.get(record.name) || [];
83
+ list.push(record);
84
+ map.set(record.name, list);
85
+ }
86
+ function label(type) {
87
+ if (type === 'mcp')
88
+ return 'MCP';
89
+ return type.charAt(0).toUpperCase() + type.slice(1);
90
+ }
91
+ export function resolveFromIndex(index, type, id) {
92
+ const map = index[type];
93
+ if (String(id).includes('/')) {
94
+ const hit = map.get(id);
95
+ if (!hit) {
96
+ const error = new Error(`${label(type)} "${id}" not found`);
97
+ throw error;
98
+ }
99
+ return hit;
100
+ }
101
+ const matches = index[`${type}ByName`].get(id) || [];
102
+ if (matches.length === 1)
103
+ return matches[0];
104
+ if (matches.length === 0)
105
+ throw new Error(`${label(type)} "${id}" not found`);
106
+ throw new Error(`${label(type)} "${id}" is ambiguous: ${matches.map((item) => item.id).sort().join(', ')}`);
107
+ }
108
+ export function listFromIndex(index, type) {
109
+ if (type)
110
+ return [...index[type].values()].sort((a, b) => a.id.localeCompare(b.id));
111
+ return ['skill', 'command', 'agent', 'mcp'].flatMap((key) => listFromIndex(index, key));
112
+ }
113
+ export function filterRecords(records, query, descriptions = new Map()) {
114
+ const needle = String(query || '').trim().toLowerCase();
115
+ if (!needle)
116
+ return records;
117
+ return records.filter((record) => {
118
+ const description = descriptions.get(`${record.type}:${record.id}`) || '';
119
+ return [record.id, record.name, record.category, description].join('\n').toLowerCase().includes(needle);
120
+ });
121
+ }
122
+ export function descriptionsFromCatalog(json) {
123
+ const map = new Map();
124
+ if (!json || typeof json !== 'object')
125
+ return map;
126
+ const record = json;
127
+ for (const [group, type] of Object.entries(CATALOG_GROUPS)) {
128
+ const items = record[group];
129
+ if (!Array.isArray(items))
130
+ continue;
131
+ for (const item of items) {
132
+ if (!item || typeof item !== 'object')
133
+ continue;
134
+ const entry = item;
135
+ const id = String(entry.path || '').replace(/\\/g, '/').replace(/\.(md|json)$/i, '');
136
+ if (!id)
137
+ continue;
138
+ map.set(`${type}:${id}`, typeof entry.description === 'string' ? entry.description.trim() : '');
139
+ }
140
+ }
141
+ return map;
142
+ }
143
+ export function createCatalog({ fetchImpl = globalThis.fetch, token = process.env.GITHUB_TOKEN, verbose = false, } = {}) {
144
+ let treePromise;
145
+ let descriptionPromise;
146
+ function log(message) {
147
+ if (verbose)
148
+ console.error(message);
149
+ }
150
+ async function github(url, accept) {
151
+ const headers = { 'User-Agent': USER_AGENT, Accept: accept };
152
+ if (token)
153
+ headers.Authorization = `Bearer ${token}`;
154
+ log(`GET ${url}`);
155
+ const response = await fetchImpl(url, { headers });
156
+ if (response.status === 403 || response.status === 429) {
157
+ throw new Error(`GitHub rate limit (${response.status}). Set GITHUB_TOKEN and retry.`);
158
+ }
159
+ return response;
160
+ }
161
+ async function getTree() {
162
+ if (!treePromise) {
163
+ treePromise = (async () => {
164
+ const response = await github(`https://api.github.com/repos/${REPO}/git/trees/${BRANCH}?recursive=1`, 'application/vnd.github+json');
165
+ if (!response.ok)
166
+ throw new Error(`GitHub tree fetch failed: HTTP ${response.status}`);
167
+ const data = await response.json();
168
+ if (data.truncated)
169
+ throw new Error('GitHub tree response was truncated. Set GITHUB_TOKEN and retry.');
170
+ return indexFromTree(data.tree || []);
171
+ })();
172
+ }
173
+ return treePromise;
174
+ }
175
+ async function descriptions() {
176
+ if (!descriptionPromise) {
177
+ descriptionPromise = (async () => {
178
+ const response = await github(rawUrl('docs/components.json'), 'application/octet-stream');
179
+ if (!response.ok)
180
+ return new Map();
181
+ return descriptionsFromCatalog(await response.json());
182
+ })().catch((error) => {
183
+ log(`description index unavailable: ${error instanceof Error ? error.message : error}`);
184
+ return new Map();
185
+ });
186
+ }
187
+ return descriptionPromise;
188
+ }
189
+ return {
190
+ async resolve(type, id) {
191
+ return resolveFromIndex(await getTree(), type, id);
192
+ },
193
+ async list(type) {
194
+ return listFromIndex(await getTree(), type);
195
+ },
196
+ async search(type, query) {
197
+ return filterRecords(listFromIndex(await getTree(), type), query, await descriptions());
198
+ },
199
+ async descriptionFor(record) {
200
+ return (await descriptions()).get(`${record.type}:${record.id}`) || '';
201
+ },
202
+ async readFile(repoPath) {
203
+ const response = await github(rawUrl(repoPath), 'application/octet-stream');
204
+ if (response.status === 404)
205
+ return null;
206
+ if (!response.ok)
207
+ throw new Error(`Download failed for ${repoPath}: HTTP ${response.status}`);
208
+ return Buffer.from(await response.arrayBuffer());
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,6 @@
1
+ export declare class UsageError extends Error {
2
+ readonly exitCode = 2;
3
+ constructor(message: string);
4
+ }
5
+ export declare function errorMessage(error: unknown): string;
6
+ export declare function formatError(error: unknown, debug: boolean): string;
@@ -0,0 +1,17 @@
1
+ export class UsageError extends Error {
2
+ exitCode = 2;
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'UsageError';
6
+ }
7
+ }
8
+ export function errorMessage(error) {
9
+ if (error instanceof Error)
10
+ return error.message;
11
+ return String(error);
12
+ }
13
+ export function formatError(error, debug) {
14
+ if (debug && error instanceof Error && error.stack)
15
+ return error.stack;
16
+ return errorMessage(error);
17
+ }
@@ -0,0 +1,6 @@
1
+ export declare function parseFrontmatter(text: string): {
2
+ data: Record<string, unknown>;
3
+ body: string;
4
+ };
5
+ export declare function stringifyFrontmatter(data: Record<string, unknown>, body: string): string;
6
+ export declare function descriptionText(value: unknown): string;
@@ -0,0 +1,32 @@
1
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
2
+ export function parseFrontmatter(text) {
3
+ const src = String(text).replace(/^\uFEFF/, '');
4
+ if (!src.startsWith('---'))
5
+ return { data: {}, body: src };
6
+ const match = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
7
+ if (!match)
8
+ return { data: {}, body: src };
9
+ let data = {};
10
+ try {
11
+ const parsed = parseYaml(match[1] ?? '');
12
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
13
+ data = parsed;
14
+ }
15
+ }
16
+ catch {
17
+ data = {};
18
+ }
19
+ return { data, body: src.slice(match[0].length) };
20
+ }
21
+ export function stringifyFrontmatter(data, body) {
22
+ const yaml = stringifyYaml(data).trimEnd();
23
+ const rest = String(body || '').replace(/^\n/, '');
24
+ return `---\n${yaml}\n---\n\n${rest.trim()}\n`;
25
+ }
26
+ export function descriptionText(value) {
27
+ if (typeof value === 'string')
28
+ return value.trim();
29
+ if (value == null)
30
+ return '';
31
+ return String(value).trim();
32
+ }
@@ -0,0 +1,19 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ export declare function pathExists(file: string): Promise<boolean>;
3
+ export declare function safeJoin(root: string, rel: string): string;
4
+ export declare function writeBytes(file: string, data: string | Buffer, { executable, dryRun }?: {
5
+ executable?: boolean;
6
+ dryRun?: boolean;
7
+ }): Promise<void>;
8
+ export declare function isExecutableRel(rel: string): boolean;
9
+ export declare function confirm(question: string, { yes, dryRun, input, output, }?: {
10
+ yes?: boolean;
11
+ dryRun?: boolean;
12
+ input?: Readable;
13
+ output?: Writable;
14
+ }): Promise<boolean>;
15
+ export declare function nearestExistingParent(file: string): Promise<string>;
16
+ export declare function isWritableDir(dir: string): Promise<boolean>;
17
+ export declare function listChildDirs(dir: string): Promise<string[]>;
18
+ export declare function listChildFiles(dir: string, extension?: string): Promise<string[]>;
19
+ export declare function backupOnce(file: string): Promise<void>;
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline/promises';
4
+ function isEnoent(error) {
5
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
6
+ }
7
+ export async function pathExists(file) {
8
+ try {
9
+ await fs.access(file);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export function safeJoin(root, rel) {
17
+ if (!rel || rel.split('/').includes('..')) {
18
+ throw new Error(`Refusing unsafe relative path "${rel}".`);
19
+ }
20
+ const dest = path.resolve(root, rel);
21
+ const base = path.resolve(root);
22
+ if (dest !== base && !dest.startsWith(base + path.sep)) {
23
+ throw new Error(`Refusing to write outside ${base}: ${rel}`);
24
+ }
25
+ return dest;
26
+ }
27
+ export async function writeBytes(file, data, { executable = false, dryRun = false } = {}) {
28
+ if (dryRun) {
29
+ console.log(`would write ${file}`);
30
+ return;
31
+ }
32
+ await fs.mkdir(path.dirname(file), { recursive: true });
33
+ await fs.writeFile(file, data);
34
+ if (executable)
35
+ await fs.chmod(file, 0o755);
36
+ }
37
+ export function isExecutableRel(rel) {
38
+ return rel.endsWith('.py') || rel.endsWith('.sh');
39
+ }
40
+ export async function confirm(question, { yes = false, dryRun = false, input = process.stdin, output = process.stdout, } = {}) {
41
+ if (yes || dryRun)
42
+ return true;
43
+ if (!('isTTY' in input) || !input.isTTY) {
44
+ console.log('skipped (not a TTY; pass --yes to overwrite)');
45
+ return false;
46
+ }
47
+ const rl = readline.createInterface({ input, output });
48
+ try {
49
+ const answer = await rl.question(question);
50
+ return /^y(es)?$/i.test(answer.trim());
51
+ }
52
+ finally {
53
+ rl.close();
54
+ }
55
+ }
56
+ export async function nearestExistingParent(file) {
57
+ let current = file;
58
+ for (;;) {
59
+ if (await pathExists(current))
60
+ return current;
61
+ const parent = path.dirname(current);
62
+ if (parent === current)
63
+ return current;
64
+ current = parent;
65
+ }
66
+ }
67
+ export async function isWritableDir(dir) {
68
+ const existing = await nearestExistingParent(dir);
69
+ try {
70
+ await fs.access(existing, fs.constants.W_OK);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export async function listChildDirs(dir) {
78
+ try {
79
+ const entries = await fs.readdir(dir, { withFileTypes: true });
80
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(dir, entry.name));
81
+ }
82
+ catch (error) {
83
+ if (isEnoent(error))
84
+ return [];
85
+ throw error;
86
+ }
87
+ }
88
+ export async function listChildFiles(dir, extension) {
89
+ try {
90
+ const entries = await fs.readdir(dir, { withFileTypes: true });
91
+ return entries
92
+ .filter((entry) => entry.isFile() && (!extension || entry.name.endsWith(extension)))
93
+ .map((entry) => path.join(dir, entry.name));
94
+ }
95
+ catch (error) {
96
+ if (isEnoent(error))
97
+ return [];
98
+ throw error;
99
+ }
100
+ }
101
+ export async function backupOnce(file) {
102
+ const backup = `${file}.bak`;
103
+ if (!(await pathExists(file)) || (await pathExists(backup)))
104
+ return;
105
+ await fs.copyFile(file, backup);
106
+ }
@@ -0,0 +1,6 @@
1
+ import type { ComponentType } from '../core/types.js';
2
+ export declare function splitIds(value: string | undefined): string[];
3
+ export declare function baseName(id: string): string;
4
+ export declare function normalizeType(type: string): ComponentType;
5
+ export declare function catalogId(category: string, name: string): string;
6
+ export declare function assertSafeId(id: string): void;
@@ -0,0 +1,30 @@
1
+ import { TYPE_ALIASES } from '../config/constants.js';
2
+ import { UsageError } from './errors.js';
3
+ export function splitIds(value) {
4
+ if (!value)
5
+ return [];
6
+ return String(value)
7
+ .split(',')
8
+ .map((part) => part.trim())
9
+ .filter(Boolean);
10
+ }
11
+ export function baseName(id) {
12
+ const parts = String(id).split('/').filter(Boolean);
13
+ return parts[parts.length - 1] || '';
14
+ }
15
+ export function normalizeType(type) {
16
+ const key = String(type || '').trim().toLowerCase();
17
+ const normalized = TYPE_ALIASES[key];
18
+ if (!normalized) {
19
+ throw new UsageError(`Unknown component type "${type}". Use skill, command, agent, or mcp.`);
20
+ }
21
+ return normalized;
22
+ }
23
+ export function catalogId(category, name) {
24
+ return `${category}/${name}`;
25
+ }
26
+ export function assertSafeId(id) {
27
+ if (!id || id.includes('..') || id.includes('\\') || id.startsWith('/') || id.includes('\0')) {
28
+ throw new UsageError(`Invalid component id "${id}". Use a catalog id such as creative-design/frontend-design.`);
29
+ }
30
+ }