@agent-nexus/csreg 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,171 @@
1
+ import { Command } from 'commander';
2
+ import { resolve, join, basename } from 'node:path';
3
+ import { existsSync, statSync, readdirSync } from 'node:fs';
4
+ import { parseManifest, validateManifest } from '../lib/manifest.js';
5
+ import { findClaudeSkillsDir, discoverSkillDirs } from '../lib/discovery.js';
6
+ import { success, error as errorOut, warn } from '../lib/output.js';
7
+ import { handleError, CliError } from '../lib/errors.js';
8
+
9
+ const MAX_FILE_COUNT = 100;
10
+ const MAX_FILE_SIZE = 500 * 1024; // 500KB
11
+ const MAX_TOTAL_SIZE = 2 * 1024 * 1024; // 2MB
12
+
13
+ function collectFiles(dir: string, prefix = ''): Array<{ path: string; size: number }> {
14
+ const files: Array<{ path: string; size: number }> = [];
15
+ const entries = readdirSync(dir, { withFileTypes: true });
16
+
17
+ for (const entry of entries) {
18
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
19
+
20
+ const fullPath = join(dir, entry.name);
21
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
22
+
23
+ if (entry.isDirectory()) {
24
+ files.push(...collectFiles(fullPath, relativePath));
25
+ } else {
26
+ const stat = statSync(fullPath);
27
+ files.push({ path: relativePath, size: stat.size });
28
+ }
29
+ }
30
+
31
+ return files;
32
+ }
33
+
34
+ export function runValidation(dir: string): { valid: boolean; errors: string[]; warnings: string[] } {
35
+ const errors: string[] = [];
36
+ const warnings: string[] = [];
37
+
38
+ // Check SKILL.md exists
39
+ if (!existsSync(join(dir, 'SKILL.md'))) {
40
+ errors.push('Missing SKILL.md — every skill must have a SKILL.md file with YAML frontmatter.');
41
+ return { valid: false, errors, warnings };
42
+ }
43
+
44
+ // Parse and validate frontmatter
45
+ const manifest = parseManifest(dir);
46
+ const manifestErrors = validateManifest(manifest);
47
+ errors.push(...manifestErrors);
48
+
49
+ // Check that the body (instructions) is not empty
50
+ if (!manifest._body) {
51
+ warnings.push('SKILL.md has no instructions body after the frontmatter. Add skill instructions below the --- delimiter.');
52
+ }
53
+
54
+ // Registry-specific: version and scope are required for publishing
55
+ if (!manifest.version) {
56
+ warnings.push('No "version" in frontmatter. Required for publishing to the registry (e.g., version: "1.0.0").');
57
+ }
58
+ if (!manifest.scope) {
59
+ warnings.push('No "scope" in frontmatter. Required for publishing to the registry (e.g., scope: my-team).');
60
+ }
61
+
62
+ // Check directory name matches skill name
63
+ const dirName = dir.split('/').pop();
64
+ if (manifest.name && dirName && dirName !== manifest.name) {
65
+ warnings.push(`Directory name "${dirName}" doesn't match skill name "${manifest.name}". They should match per the Agent Skills spec.`);
66
+ }
67
+
68
+ // Collect and check files
69
+ const files = collectFiles(dir);
70
+
71
+ if (files.length > MAX_FILE_COUNT) {
72
+ errors.push(`Too many files: ${files.length} (max ${MAX_FILE_COUNT}).`);
73
+ }
74
+
75
+ let totalSize = 0;
76
+ for (const file of files) {
77
+ totalSize += file.size;
78
+ if (file.size > MAX_FILE_SIZE) {
79
+ errors.push(`File too large: ${file.path} (${(file.size / 1024).toFixed(1)}KB, max 500KB).`);
80
+ }
81
+ }
82
+
83
+ if (totalSize > MAX_TOTAL_SIZE) {
84
+ errors.push(`Total size too large: ${(totalSize / 1024 / 1024).toFixed(2)}MB (max 2MB).`);
85
+ }
86
+
87
+ return { valid: errors.length === 0, errors, warnings };
88
+ }
89
+
90
+ export const validateCommand = new Command('validate')
91
+ .description('Validate a skill package')
92
+ .argument('[dir]', 'Skill directory', '.')
93
+ .option('--all', 'Validate all skills in .claude/skills/')
94
+ .action(async (dir: string, opts: { all?: boolean }) => {
95
+ try {
96
+ // --- Mode: validate --all ---
97
+ if (opts.all) {
98
+ const skillsDir = findClaudeSkillsDir();
99
+ if (!skillsDir) {
100
+ throw new CliError('Cannot find .claude/skills/ directory.', [
101
+ 'Run this from a project with a .claude/ directory.',
102
+ ]);
103
+ }
104
+
105
+ const skillDirs = discoverSkillDirs(skillsDir);
106
+ if (skillDirs.length === 0) {
107
+ throw new CliError(`No skills found in ${skillsDir}/`, [
108
+ 'Skills must be directories containing a SKILL.md file.',
109
+ ]);
110
+ }
111
+
112
+ console.log(`Validating ${skillDirs.length} skill(s) in ${skillsDir}/\n`);
113
+
114
+ let passed = 0;
115
+ let failed = 0;
116
+
117
+ for (const skillDir of skillDirs) {
118
+ const name = basename(skillDir);
119
+ const result = runValidation(skillDir);
120
+
121
+ if (result.valid) {
122
+ success(`${name}`);
123
+ passed++;
124
+ } else {
125
+ errorOut(`${name}`);
126
+ for (const e of result.errors) {
127
+ console.error(` ${e}`);
128
+ }
129
+ failed++;
130
+ }
131
+
132
+ for (const w of result.warnings) {
133
+ warn(` ${name}: ${w}`);
134
+ }
135
+ }
136
+
137
+ console.log('');
138
+ if (failed === 0) {
139
+ success(`All ${passed} skill(s) are valid.`);
140
+ } else {
141
+ throw new CliError(`${failed}/${passed + failed} skill(s) failed validation.`);
142
+ }
143
+ return;
144
+ }
145
+
146
+ // --- Mode: validate single ---
147
+ const resolved = resolve(dir);
148
+ if (!existsSync(resolved)) {
149
+ throw new CliError(`Directory not found: ${resolved}`);
150
+ }
151
+
152
+ const result = runValidation(resolved);
153
+
154
+ for (const w of result.warnings) {
155
+ warn(w);
156
+ }
157
+
158
+ if (result.valid) {
159
+ success('Skill is valid.');
160
+ } else {
161
+ for (const e of result.errors) {
162
+ errorOut(e);
163
+ }
164
+ throw new CliError('Validation failed.', [
165
+ 'Fix the errors above and try again.',
166
+ ]);
167
+ }
168
+ } catch (err) {
169
+ handleError(err);
170
+ }
171
+ });
@@ -0,0 +1,56 @@
1
+ import { Command } from 'commander';
2
+ import { ApiClient } from '../api-client.js';
3
+ import { formatTable } from '../lib/output.js';
4
+ import { handleError, CliError } from '../lib/errors.js';
5
+
6
+ interface VersionEntry {
7
+ version: string;
8
+ status: string;
9
+ createdAt: string;
10
+ fileCount: number;
11
+ }
12
+
13
+ interface VersionsResponse {
14
+ versions: VersionEntry[];
15
+ }
16
+
17
+ export const versionsCommand = new Command('versions')
18
+ .description('List all versions of a skill')
19
+ .argument('<ref>', 'Skill reference (scope/name)')
20
+ .action(async (ref: string) => {
21
+ try {
22
+ const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
23
+ const slashIndex = cleaned.indexOf('/');
24
+ if (slashIndex < 0) {
25
+ throw new CliError(`Invalid skill reference: ${ref}`, [
26
+ 'Use the format: @scope/name',
27
+ ]);
28
+ }
29
+
30
+ const scope = cleaned.slice(0, slashIndex);
31
+ const name = cleaned.slice(slashIndex + 1);
32
+
33
+ const client = new ApiClient();
34
+ const data = await client.get<VersionsResponse>(
35
+ `/api/v1/skills/${scope}/${name}/versions`,
36
+ );
37
+
38
+ if (data.versions.length === 0) {
39
+ console.log('No versions found.');
40
+ return;
41
+ }
42
+
43
+ const rows = data.versions.map(v => [
44
+ v.version,
45
+ v.status,
46
+ new Date(v.createdAt).toLocaleDateString(),
47
+ String(v.fileCount),
48
+ ]);
49
+
50
+ console.log('');
51
+ console.log(formatTable(['Version', 'Status', 'Published', 'Files'], rows));
52
+ console.log('');
53
+ } catch (err) {
54
+ handleError(err);
55
+ }
56
+ });
@@ -0,0 +1,24 @@
1
+ import { Command } from 'commander';
2
+ import { ApiClient } from '../api-client.js';
3
+ import { success } from '../lib/output.js';
4
+ import { handleError, CliError } from '../lib/errors.js';
5
+ import { getAuthToken } from '../config.js';
6
+
7
+ export const whoamiCommand = new Command('whoami')
8
+ .description('Display the currently authenticated user')
9
+ .action(async () => {
10
+ try {
11
+ if (!getAuthToken()) {
12
+ throw new CliError('Not logged in.', ['Run `csreg login` to authenticate.']);
13
+ }
14
+
15
+ const client = new ApiClient();
16
+ const user = await client.get<{ email: string; displayName: string }>(
17
+ '/api/v1/users/me',
18
+ );
19
+
20
+ success(`Logged in as ${user.displayName} (${user.email})`);
21
+ } catch (err) {
22
+ handleError(err);
23
+ }
24
+ });
package/src/config.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ export interface Config {
6
+ apiUrl?: string;
7
+ token?: string;
8
+ }
9
+
10
+ const CONFIG_DIR = join(homedir(), '.config', 'csreg');
11
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
12
+
13
+ export function getConfig(): Config {
14
+ if (!existsSync(CONFIG_PATH)) {
15
+ return {};
16
+ }
17
+ try {
18
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
19
+ return JSON.parse(raw) as Config;
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ export function setConfig(updates: Partial<Config>): void {
26
+ const current = getConfig();
27
+ const merged = { ...current, ...updates };
28
+ mkdirSync(CONFIG_DIR, { recursive: true });
29
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
30
+ }
31
+
32
+ export function getApiUrl(): string {
33
+ return process.env.CSREG_API_URL ?? getConfig().apiUrl ?? 'http://localhost:3000';
34
+ }
35
+
36
+ export function getAuthToken(): string | undefined {
37
+ return process.env.CSREG_TOKEN ?? getConfig().token;
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { loginCommand } from './commands/login.js';
4
+ import { logoutCommand } from './commands/logout.js';
5
+ import { whoamiCommand } from './commands/whoami.js';
6
+ import { initCommand } from './commands/init.js';
7
+ import { validateCommand } from './commands/validate.js';
8
+ import { packCommand } from './commands/pack.js';
9
+ import { pushCommand } from './commands/push.js';
10
+ import { pullCommand } from './commands/pull.js';
11
+ import { infoCommand } from './commands/info.js';
12
+ import { versionsCommand } from './commands/versions.js';
13
+ import { searchCommand } from './commands/search.js';
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('csreg')
19
+ .description('Claude Skills Registry CLI')
20
+ .version('0.1.0');
21
+
22
+ program.addCommand(loginCommand);
23
+ program.addCommand(logoutCommand);
24
+ program.addCommand(whoamiCommand);
25
+ program.addCommand(initCommand);
26
+ program.addCommand(validateCommand);
27
+ program.addCommand(packCommand);
28
+ program.addCommand(pushCommand);
29
+ program.addCommand(pullCommand);
30
+ program.addCommand(infoCommand);
31
+ program.addCommand(versionsCommand);
32
+ program.addCommand(searchCommand);
33
+
34
+ program.parse();
@@ -0,0 +1,59 @@
1
+ import { statSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import { join, basename } from 'node:path';
5
+ import { create as tarCreate, extract as tarExtract } from 'tar';
6
+ import { CliError } from './errors.js';
7
+
8
+ export interface PackResult {
9
+ path: string;
10
+ sha256: string;
11
+ size: number;
12
+ }
13
+
14
+ async function computeSha256(filePath: string): Promise<string> {
15
+ const data = await readFile(filePath);
16
+ return createHash('sha256').update(data).digest('hex');
17
+ }
18
+
19
+ export async function pack(dir: string, outputPath?: string): Promise<PackResult> {
20
+ const dirName = basename(dir);
21
+ const archivePath = outputPath ?? join(dir, '..', `${dirName}.tar.gz`);
22
+
23
+ await tarCreate(
24
+ {
25
+ gzip: true,
26
+ file: archivePath,
27
+ cwd: join(dir, '..'),
28
+ },
29
+ [dirName],
30
+ );
31
+
32
+ const sha256 = await computeSha256(archivePath);
33
+ const stat = statSync(archivePath);
34
+
35
+ return {
36
+ path: archivePath,
37
+ sha256,
38
+ size: stat.size,
39
+ };
40
+ }
41
+
42
+ export async function extract(
43
+ archivePath: string,
44
+ outputDir: string,
45
+ expectedSha256: string,
46
+ ): Promise<void> {
47
+ const actualSha256 = await computeSha256(archivePath);
48
+ if (actualSha256 !== expectedSha256) {
49
+ throw new CliError(
50
+ `SHA-256 mismatch: expected ${expectedSha256}, got ${actualSha256}`,
51
+ ['The archive may be corrupted or tampered with.', 'Try downloading again.'],
52
+ );
53
+ }
54
+
55
+ await tarExtract({
56
+ file: archivePath,
57
+ cwd: outputDir,
58
+ });
59
+ }
@@ -0,0 +1,51 @@
1
+ import { resolve, join, dirname } from 'node:path';
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+
4
+ /**
5
+ * Find the .claude/skills directory by walking up from cwd.
6
+ */
7
+ export function findClaudeSkillsDir(): string | null {
8
+ let dir = resolve('.');
9
+
10
+ // If we're already inside a .claude directory, use it
11
+ const parts = dir.split('/');
12
+ const claudeIdx = parts.lastIndexOf('.claude');
13
+ if (claudeIdx >= 0) {
14
+ const claudeRoot = parts.slice(0, claudeIdx + 1).join('/');
15
+ return join(claudeRoot, 'skills');
16
+ }
17
+
18
+ // Walk up looking for .claude/
19
+ while (dir !== dirname(dir)) {
20
+ const claudeDir = join(dir, '.claude');
21
+ if (existsSync(claudeDir)) {
22
+ return join(claudeDir, 'skills');
23
+ }
24
+ dir = dirname(dir);
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Discover all skill directories (containing SKILL.md) within a parent directory.
32
+ * Scans one level deep: parentDir/<name>/SKILL.md
33
+ */
34
+ export function discoverSkillDirs(parentDir: string): string[] {
35
+ if (!existsSync(parentDir)) return [];
36
+
37
+ const dirs: string[] = [];
38
+ const entries = readdirSync(parentDir, { withFileTypes: true });
39
+
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory()) continue;
42
+ if (entry.name.startsWith('.')) continue;
43
+
44
+ const skillMd = join(parentDir, entry.name, 'SKILL.md');
45
+ if (existsSync(skillMd)) {
46
+ dirs.push(join(parentDir, entry.name));
47
+ }
48
+ }
49
+
50
+ return dirs.sort();
51
+ }
@@ -0,0 +1,29 @@
1
+ import chalk from 'chalk';
2
+
3
+ export class CliError extends Error {
4
+ public suggestions: string[];
5
+
6
+ constructor(message: string, suggestions: string[] = []) {
7
+ super(message);
8
+ this.name = 'CliError';
9
+ this.suggestions = suggestions;
10
+ }
11
+ }
12
+
13
+ export function handleError(err: unknown): void {
14
+ if (err instanceof CliError) {
15
+ console.error(chalk.red('Error:') + ' ' + err.message);
16
+ if (err.suggestions.length > 0) {
17
+ console.error('');
18
+ console.error(chalk.yellow('Suggestions:'));
19
+ for (const suggestion of err.suggestions) {
20
+ console.error(' ' + chalk.dim('-') + ' ' + suggestion);
21
+ }
22
+ }
23
+ } else if (err instanceof Error) {
24
+ console.error(chalk.red('Error:') + ' ' + err.message);
25
+ } else {
26
+ console.error(chalk.red('Error:') + ' ' + String(err));
27
+ }
28
+ process.exit(1);
29
+ }
@@ -0,0 +1,143 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { CliError } from './errors.js';
5
+
6
+ /**
7
+ * Manifest parsed from SKILL.md YAML frontmatter.
8
+ * Follows the Agent Skills specification used by Claude Code.
9
+ * @see https://agentskills.io/specification
10
+ */
11
+ export interface Manifest {
12
+ name: string;
13
+ description?: string;
14
+ 'allowed-tools'?: string;
15
+ 'argument-hint'?: string;
16
+ 'disable-model-invocation'?: boolean;
17
+ 'user-invocable'?: boolean;
18
+ model?: string;
19
+ context?: string;
20
+ agent?: string;
21
+ license?: string;
22
+ compatibility?: string;
23
+ metadata?: Record<string, unknown>;
24
+ // Registry-specific fields (not part of the Claude Code spec, added by our registry)
25
+ version?: string;
26
+ scope?: string;
27
+ tags?: string[];
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ const SKILL_FILE = 'SKILL.md';
32
+
33
+ /**
34
+ * Parse SKILL.md frontmatter from a skill directory.
35
+ *
36
+ * Expected format:
37
+ * ```
38
+ * ---
39
+ * name: my-skill
40
+ * description: Does something useful
41
+ * ---
42
+ *
43
+ * Skill instructions here...
44
+ * ```
45
+ */
46
+ export function parseManifest(dir: string): Manifest & { _body: string } {
47
+ const skillPath = join(dir, SKILL_FILE);
48
+ if (!existsSync(skillPath)) {
49
+ throw new CliError(`No ${SKILL_FILE} found in ${dir}`, [
50
+ 'Run `csreg init` to create a new skill.',
51
+ 'A valid skill requires a SKILL.md file with YAML frontmatter.',
52
+ ]);
53
+ }
54
+
55
+ const raw = readFileSync(skillPath, 'utf-8');
56
+
57
+ // Parse YAML frontmatter between --- delimiters
58
+ const frontmatterMatch = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
59
+ if (!frontmatterMatch) {
60
+ throw new CliError(`${SKILL_FILE} is missing YAML frontmatter.`, [
61
+ 'SKILL.md must start with --- delimited YAML frontmatter.',
62
+ 'Example:\n ---\n name: my-skill\n description: Does something\n ---\n \n Your instructions here.',
63
+ ]);
64
+ }
65
+
66
+ const [, frontmatterRaw, body] = frontmatterMatch;
67
+
68
+ let parsed: unknown;
69
+ try {
70
+ parsed = parseYaml(frontmatterRaw);
71
+ } catch (err) {
72
+ throw new CliError(`Failed to parse ${SKILL_FILE} frontmatter: ${String(err)}`, [
73
+ 'Check that the YAML between --- delimiters is valid.',
74
+ ]);
75
+ }
76
+
77
+ if (!parsed || typeof parsed !== 'object') {
78
+ throw new CliError(`${SKILL_FILE} frontmatter is empty or not an object.`, [
79
+ 'Add at least a "name" and "description" field.',
80
+ ]);
81
+ }
82
+
83
+ return { ...(parsed as Manifest), _body: body.trim() };
84
+ }
85
+
86
+ /**
87
+ * Validate a skill manifest against the Claude Code skills spec.
88
+ */
89
+ export function validateManifest(manifest: Manifest): string[] {
90
+ const errors: string[] = [];
91
+
92
+ // name is required
93
+ if (!manifest.name || typeof manifest.name !== 'string') {
94
+ errors.push('Missing or invalid "name" field in frontmatter.');
95
+ } else {
96
+ if (manifest.name.length > 64) {
97
+ errors.push('"name" must be at most 64 characters.');
98
+ }
99
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.name)) {
100
+ errors.push('"name" must be lowercase alphanumeric with hyphens, not starting or ending with a hyphen.');
101
+ }
102
+ if (manifest.name.includes('--')) {
103
+ errors.push('"name" must not contain consecutive hyphens (--).');
104
+ }
105
+ }
106
+
107
+ // description is strongly recommended
108
+ if (!manifest.description || typeof manifest.description !== 'string') {
109
+ errors.push('Missing "description" field. Claude uses this to decide when to invoke the skill.');
110
+ } else if (manifest.description.length > 1024) {
111
+ errors.push('"description" must be at most 1024 characters.');
112
+ }
113
+
114
+ // Registry-specific: version is required for publishing
115
+ if (manifest.version !== undefined) {
116
+ const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
117
+ if (!semverRegex.test(manifest.version)) {
118
+ errors.push('"version" must be valid semver (e.g., 1.0.0).');
119
+ }
120
+ }
121
+
122
+ // Registry-specific: scope is required for publishing
123
+ if (manifest.scope !== undefined && typeof manifest.scope === 'string') {
124
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(manifest.scope)) {
125
+ errors.push('"scope" must be lowercase alphanumeric with hyphens.');
126
+ }
127
+ }
128
+
129
+ // Validate optional typed fields
130
+ if (manifest['allowed-tools'] !== undefined && typeof manifest['allowed-tools'] !== 'string') {
131
+ errors.push('"allowed-tools" must be a string (space-delimited list of tools).');
132
+ }
133
+
134
+ if (manifest['disable-model-invocation'] !== undefined && typeof manifest['disable-model-invocation'] !== 'boolean') {
135
+ errors.push('"disable-model-invocation" must be a boolean.');
136
+ }
137
+
138
+ if (manifest['user-invocable'] !== undefined && typeof manifest['user-invocable'] !== 'boolean') {
139
+ errors.push('"user-invocable" must be a boolean.');
140
+ }
141
+
142
+ return errors;
143
+ }
@@ -0,0 +1,34 @@
1
+ import chalk from 'chalk';
2
+ import ora, { type Ora } from 'ora';
3
+ import Table from 'cli-table3';
4
+
5
+ export function formatTable(headers: string[], rows: string[][]): string {
6
+ const table = new Table({
7
+ head: headers.map(h => chalk.bold.cyan(h)),
8
+ style: { head: [], border: [] },
9
+ });
10
+ for (const row of rows) {
11
+ table.push(row);
12
+ }
13
+ return table.toString();
14
+ }
15
+
16
+ export function spinner(text: string): Ora {
17
+ return ora({ text, color: 'cyan' }).start();
18
+ }
19
+
20
+ export function success(msg: string): void {
21
+ console.log(chalk.green('✓') + ' ' + msg);
22
+ }
23
+
24
+ export function error(msg: string): void {
25
+ console.error(chalk.red('✗') + ' ' + msg);
26
+ }
27
+
28
+ export function warn(msg: string): void {
29
+ console.log(chalk.yellow('!') + ' ' + msg);
30
+ }
31
+
32
+ export function info(msg: string): void {
33
+ console.log(chalk.blue('i') + ' ' + msg);
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "declaration": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ dts: true,
7
+ clean: true,
8
+ noExternal: [],
9
+ // Externalize all node_modules — they're available at runtime
10
+ external: [
11
+ "commander",
12
+ "chalk",
13
+ "ora",
14
+ "yaml",
15
+ "semver",
16
+ "tar",
17
+ "inquirer",
18
+ "@inquirer/prompts",
19
+ "cli-table3",
20
+ "glob",
21
+ ],
22
+ });