@prave/cli 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,56 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { api } from '../lib/api.js';
5
+ import { CONFIG } from '../lib/config.js';
6
+ import { loadCredentials } from '../lib/credentials.js';
7
+ import { log } from '../utils/logger.js';
8
+ /**
9
+ * `prave diff <slug>` — line-level comparison between the local
10
+ * ~/.claude/skills/<slug>/SKILL.md and the registry's current
11
+ * version. We use a tiny LCS-based diff so the CLI stays dep-light.
12
+ */
13
+ export async function diffCommand(slug) {
14
+ const localPath = join(CONFIG.skillsDir, slug, 'SKILL.md');
15
+ let local = '';
16
+ try {
17
+ local = await readFile(localPath, 'utf8');
18
+ }
19
+ catch {
20
+ log.warn(`No local Skill at ${localPath} — run \`prave install ${slug}\` first.`);
21
+ return;
22
+ }
23
+ const session = await loadCredentials();
24
+ const { data: skill } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, Boolean(session));
25
+ const remote = skill.content ?? '';
26
+ if (local === remote) {
27
+ log.success(`${slug} is in sync with the registry.`);
28
+ return;
29
+ }
30
+ console.log(chalk.bold(`--- local ${localPath}`));
31
+ console.log(chalk.bold(`+++ remote prave://${slug}`));
32
+ for (const line of unifiedDiff(local, remote))
33
+ console.log(line);
34
+ }
35
+ /** Tiny line diff: not a full Myers, but enough to spot drift visually. */
36
+ function unifiedDiff(a, b) {
37
+ const aLines = a.split('\n');
38
+ const bLines = b.split('\n');
39
+ const out = [];
40
+ const max = Math.max(aLines.length, bLines.length);
41
+ for (let i = 0; i < max; i++) {
42
+ const l = aLines[i];
43
+ const r = bLines[i];
44
+ if (l === r) {
45
+ if (l !== undefined)
46
+ out.push(` ${l}`);
47
+ }
48
+ else {
49
+ if (l !== undefined)
50
+ out.push(chalk.red(`- ${l}`));
51
+ if (r !== undefined)
52
+ out.push(chalk.green(`+ ${r}`));
53
+ }
54
+ }
55
+ return out;
56
+ }
@@ -0,0 +1,24 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { api } from '../lib/api.js';
3
+ import { loadCredentials } from '../lib/credentials.js';
4
+ import { log } from '../utils/logger.js';
5
+ /**
6
+ * `prave export <slug>` — print or save a Skill's SKILL.md to disk
7
+ * without touching ~/.claude/skills/. Useful for CI pipelines that want
8
+ * to bundle Skills into other repos.
9
+ */
10
+ export async function exportCommand(slug, opts = {}) {
11
+ const session = await loadCredentials();
12
+ const { data: skill } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, Boolean(session));
13
+ const content = skill.content ?? '';
14
+ if (!content.trim()) {
15
+ log.warn(`${slug} has no content`);
16
+ return;
17
+ }
18
+ if (opts.out) {
19
+ await writeFile(opts.out, content, 'utf8');
20
+ log.success(`Wrote ${opts.out} (${content.length} bytes)`);
21
+ return;
22
+ }
23
+ process.stdout.write(content);
24
+ }
@@ -0,0 +1,64 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { api } from '../lib/api.js';
6
+ import { CONFIG } from '../lib/config.js';
7
+ import { log } from '../utils/logger.js';
8
+ /**
9
+ * Scans CONFIG.skillsDir for SKILL.md files. Without --upload, only prints
10
+ * the overview — no network call, no consent required.
11
+ */
12
+ export async function importCommand(opts) {
13
+ const spinner = ora(`Scanning ${CONFIG.skillsDir}…`).start();
14
+ let entries = [];
15
+ try {
16
+ entries = await readdir(CONFIG.skillsDir);
17
+ }
18
+ catch {
19
+ spinner.fail(`Skills directory not found: ${CONFIG.skillsDir}`);
20
+ return;
21
+ }
22
+ const skills = [];
23
+ for (const name of entries) {
24
+ const dir = join(CONFIG.skillsDir, name);
25
+ if (!(await stat(dir).catch(() => null))?.isDirectory())
26
+ continue;
27
+ const skillFile = join(dir, 'SKILL.md');
28
+ try {
29
+ const content = await readFile(skillFile, 'utf8');
30
+ const { size } = await stat(skillFile);
31
+ skills.push({ slug: name, path: skillFile, content, sizeBytes: size });
32
+ }
33
+ catch {
34
+ /* no SKILL.md here */
35
+ }
36
+ }
37
+ spinner.succeed(`Found ${skills.length} local skills.`);
38
+ for (const s of skills) {
39
+ console.log(` ${chalk.cyan('•')} ${s.slug} ${chalk.dim(`(${s.sizeBytes}B)`)}`);
40
+ }
41
+ if (!opts.upload) {
42
+ log.dim('\nRe-run with --upload to push these to your Prave account.');
43
+ return;
44
+ }
45
+ const visibility = opts.private ? 'private' : 'public';
46
+ const uploadSpinner = ora(`Uploading ${skills.length} skills as ${visibility}…`).start();
47
+ let ok = 0;
48
+ for (const s of skills) {
49
+ try {
50
+ await api.post('/api/v1/skills', {
51
+ name: s.slug,
52
+ slug: s.slug,
53
+ content: s.content,
54
+ visibility,
55
+ license: 'MIT',
56
+ }, true);
57
+ ok += 1;
58
+ }
59
+ catch (err) {
60
+ uploadSpinner.warn(`Failed: ${s.slug} — ${err.message}`);
61
+ }
62
+ }
63
+ uploadSpinner.succeed(`Uploaded ${ok}/${skills.length} skills.`);
64
+ }
@@ -0,0 +1,91 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { api, ApiError } from '../lib/api.js';
6
+ import { CONFIG } from '../lib/config.js';
7
+ import { loadCredentials } from '../lib/credentials.js';
8
+ import { log } from '../utils/logger.js';
9
+ /**
10
+ * `prave install <slug>` — pulls SKILL.md to ~/.claude/skills/<slug>/.
11
+ *
12
+ * • Resolves the dependency tree by default and installs each leaf
13
+ * before the parent (deepest first). `--no-deps` skips this.
14
+ * • Records an `installs` row server-side when a session is present
15
+ * so the dashboard "Installs" tab + trending sort stay accurate.
16
+ * • Detects paywall errors (402) early and prints a friendly hint
17
+ * instead of letting the API error bubble.
18
+ */
19
+ export async function installCommand(slug, opts = {}) {
20
+ const spinner = ora(`Resolving ${slug}…`).start();
21
+ try {
22
+ const slugs = opts.noDeps ? [slug] : await resolveOrder(slug);
23
+ spinner.text = `Installing ${slugs.length} skill${slugs.length === 1 ? '' : 's'}…`;
24
+ const session = await loadCredentials();
25
+ for (const s of slugs) {
26
+ spinner.text = `↓ ${s}`;
27
+ await pullOne(s, { hasSession: Boolean(session), force: Boolean(opts.force) });
28
+ }
29
+ spinner.succeed(`Installed ${slugs.length} skill${slugs.length === 1 ? '' : 's'} → ${CONFIG.skillsDir}`);
30
+ if (slugs.length > 1) {
31
+ log.dim(` chain: ${slugs.join(' → ')}`);
32
+ }
33
+ }
34
+ catch (err) {
35
+ spinner.fail(formatError(err));
36
+ process.exitCode = 1;
37
+ }
38
+ }
39
+ async function pullOne(slug, ctx) {
40
+ const { data: skill } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, ctx.hasSession);
41
+ if (skill.price_cents > 0 && skill.purchased === false && skill.is_owner === false) {
42
+ throw new ApiError(`${chalk.bold(skill.slug)} is paid (${formatPrice(skill.price_cents, skill.currency)}). Buy it on prave.app first.`, 402);
43
+ }
44
+ if (!skill.content && !ctx.force) {
45
+ throw new ApiError(`${skill.slug} has no SKILL.md content yet`, 400);
46
+ }
47
+ const targetDir = join(CONFIG.skillsDir, skill.slug);
48
+ await mkdir(targetDir, { recursive: true });
49
+ await writeFile(join(targetDir, 'SKILL.md'), skill.content ?? '', 'utf8');
50
+ if (ctx.hasSession) {
51
+ await api
52
+ .post(`/api/v1/skills/${encodeURIComponent(skill.slug)}/install`, {}, true)
53
+ .catch(() => { });
54
+ }
55
+ }
56
+ async function resolveOrder(rootSlug) {
57
+ const session = await loadCredentials();
58
+ let tree = null;
59
+ try {
60
+ const res = await api.get(`/api/v1/skills/${encodeURIComponent(rootSlug)}/dependencies/tree`, Boolean(session));
61
+ tree = res.data;
62
+ }
63
+ catch {
64
+ return [rootSlug];
65
+ }
66
+ if (!tree?.nodes.length)
67
+ return [rootSlug];
68
+ if (tree.conflicts.length) {
69
+ log.warn(`${tree.conflicts.length} dependency conflict(s) — installing latest available`);
70
+ }
71
+ const ordered = tree.nodes
72
+ .filter((n) => !n.missing)
73
+ .sort((a, b) => b.depth - a.depth)
74
+ .map((n) => n.slug);
75
+ const seen = new Set();
76
+ const out = [];
77
+ for (const s of ordered)
78
+ if (!seen.has(s))
79
+ (seen.add(s), out.push(s));
80
+ if (!out.includes(rootSlug))
81
+ out.push(rootSlug);
82
+ return out;
83
+ }
84
+ function formatPrice(cents, currency) {
85
+ return `${(cents / 100).toFixed(2)} ${currency}`;
86
+ }
87
+ function formatError(err) {
88
+ if (err instanceof ApiError)
89
+ return err.message;
90
+ return err instanceof Error ? err.message : 'Install failed';
91
+ }
@@ -0,0 +1,34 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { api } from '../lib/api.js';
5
+ import { CONFIG } from '../lib/config.js';
6
+ import { log } from '../utils/logger.js';
7
+ export async function listCommand(opts) {
8
+ if (opts.remote) {
9
+ const { data: skills } = await api.get('/api/v1/skills?limit=50', true);
10
+ if (skills.length === 0) {
11
+ log.dim('No skills in your Prave account yet.');
12
+ return;
13
+ }
14
+ for (const s of skills) {
15
+ console.log(` ${chalk.cyan('•')} ${s.slug} ${chalk.dim(`(${s.visibility})`)}`);
16
+ }
17
+ return;
18
+ }
19
+ try {
20
+ const entries = await readdir(CONFIG.skillsDir);
21
+ let count = 0;
22
+ for (const name of entries) {
23
+ const dir = join(CONFIG.skillsDir, name);
24
+ if ((await stat(dir).catch(() => null))?.isDirectory()) {
25
+ console.log(` ${chalk.cyan('•')} ${name}`);
26
+ count += 1;
27
+ }
28
+ }
29
+ log.dim(`\n${count} local skill${count === 1 ? '' : 's'} in ${CONFIG.skillsDir}`);
30
+ }
31
+ catch {
32
+ log.warn(`No skills directory at ${CONFIG.skillsDir}`);
33
+ }
34
+ }
@@ -0,0 +1,47 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import open from 'open';
3
+ import ora from 'ora';
4
+ import { api, ApiError } from '../lib/api.js';
5
+ import { CONFIG } from '../lib/config.js';
6
+ import { saveCredentials } from '../lib/credentials.js';
7
+ import { log } from '../utils/logger.js';
8
+ /**
9
+ * `prave login` — device-code flow against the Prave API / Supabase session.
10
+ */
11
+ export async function loginCommand() {
12
+ const { data: start } = await api.post('/api/v1/cli/login');
13
+ const url = `${CONFIG.webUrl}${start.verification_url}`;
14
+ log.info('Opening browser to authorize this device…');
15
+ log.dim(url);
16
+ try {
17
+ await open(url);
18
+ }
19
+ catch {
20
+ log.warn('Could not auto-open browser. Open the URL above manually.');
21
+ }
22
+ const spinner = ora('Waiting for authorization…').start();
23
+ const deadline = Date.now() + CONFIG.pollTimeoutMs;
24
+ while (Date.now() < deadline) {
25
+ await sleep(CONFIG.pollIntervalMs);
26
+ try {
27
+ const { data, status } = await api.get(`/api/v1/cli/token?device_code=${start.device_code}`);
28
+ if (status === 202)
29
+ continue;
30
+ await saveCredentials({
31
+ access_token: data.access_token,
32
+ refresh_token: data.refresh_token,
33
+ user_id: data.user_id,
34
+ });
35
+ spinner.succeed('Logged in.');
36
+ return;
37
+ }
38
+ catch (err) {
39
+ if (err instanceof ApiError && err.status === 410) {
40
+ spinner.fail('Device code expired. Run `prave login` again.');
41
+ return;
42
+ }
43
+ // Any other error: keep polling until the deadline.
44
+ }
45
+ }
46
+ spinner.fail('Authorization timed out.');
47
+ }
@@ -0,0 +1,6 @@
1
+ import { clearCredentials } from '../lib/credentials.js';
2
+ import { log } from '../utils/logger.js';
3
+ export async function logoutCommand() {
4
+ await clearCredentials();
5
+ log.success('Logged out.');
6
+ }
@@ -0,0 +1,13 @@
1
+ import chalk from 'chalk';
2
+ import { api } from '../lib/api.js';
3
+ import { log } from '../utils/logger.js';
4
+ export async function searchCommand(query) {
5
+ const { data: skills } = await api.get(`/api/v1/skills?q=${encodeURIComponent(query)}&limit=25`);
6
+ if (skills.length === 0) {
7
+ log.dim(`No skills match "${query}".`);
8
+ return;
9
+ }
10
+ for (const s of skills) {
11
+ console.log(` ${chalk.cyan('•')} ${s.slug} ${chalk.dim(s.description ?? '')} ${chalk.magenta(`↓ ${s.install_count}`)}`);
12
+ }
13
+ }
@@ -0,0 +1,48 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { CONFIG } from '../lib/config.js';
6
+ import { log } from '../utils/logger.js';
7
+ import { installCommand } from './install.js';
8
+ /**
9
+ * `prave sync` — re-pulls every locally installed Skill from the
10
+ * registry. Picks up SKILL.md edits without the user having to remember
11
+ * each slug. Skips deps (each top-level Skill already has its tree
12
+ * resolved on the original install).
13
+ */
14
+ export async function syncCommand() {
15
+ const spinner = ora('Scanning local Skills…').start();
16
+ let entries = [];
17
+ try {
18
+ entries = await readdir(CONFIG.skillsDir);
19
+ }
20
+ catch {
21
+ spinner.warn(`No Skills directory at ${CONFIG.skillsDir}`);
22
+ return;
23
+ }
24
+ const slugs = [];
25
+ for (const name of entries) {
26
+ const dir = join(CONFIG.skillsDir, name);
27
+ if ((await stat(dir).catch(() => null))?.isDirectory())
28
+ slugs.push(name);
29
+ }
30
+ if (!slugs.length) {
31
+ spinner.warn('No installed Skills to sync.');
32
+ return;
33
+ }
34
+ spinner.succeed(`Found ${slugs.length} installed Skill${slugs.length === 1 ? '' : 's'}.`);
35
+ let updated = 0;
36
+ let failed = 0;
37
+ for (const slug of slugs) {
38
+ try {
39
+ await installCommand(slug, { noDeps: true });
40
+ updated++;
41
+ }
42
+ catch {
43
+ failed++;
44
+ console.log(chalk.red(` ✗ ${slug}`));
45
+ }
46
+ }
47
+ log.dim(`\nSynced ${updated} · failed ${failed}`);
48
+ }
@@ -0,0 +1,21 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import ora from 'ora';
4
+ import { CONFIG } from '../lib/config.js';
5
+ /**
6
+ * `prave uninstall <slug>` — removes ~/.claude/skills/<slug> recursively.
7
+ * No remote call: uninstalls are local-only; the install record stays
8
+ * for analytics + history.
9
+ */
10
+ export async function uninstallCommand(slug) {
11
+ const dir = join(CONFIG.skillsDir, slug);
12
+ const spinner = ora(`Removing ${slug}…`).start();
13
+ try {
14
+ await rm(dir, { recursive: true, force: true });
15
+ spinner.succeed(`Removed ${dir}`);
16
+ }
17
+ catch (err) {
18
+ spinner.fail(`Couldn’t remove ${slug}: ${err.message}`);
19
+ process.exitCode = 1;
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ import { loadCredentials } from '../lib/credentials.js';
2
+ import { log } from '../utils/logger.js';
3
+ export async function whoamiCommand() {
4
+ const creds = await loadCredentials();
5
+ if (!creds) {
6
+ log.warn('Not logged in. Run `prave login`.');
7
+ process.exitCode = 1;
8
+ return;
9
+ }
10
+ log.kv('user_id', creds.user_id);
11
+ if (creds.email)
12
+ log.kv('email', creds.email);
13
+ }
package/dist/index.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { diffCommand } from './commands/diff.js';
4
+ import { exportCommand } from './commands/export.js';
5
+ import { importCommand } from './commands/import.js';
6
+ import { installCommand } from './commands/install.js';
7
+ import { listCommand } from './commands/list.js';
8
+ import { loginCommand } from './commands/login.js';
9
+ import { logoutCommand } from './commands/logout.js';
10
+ import { searchCommand } from './commands/search.js';
11
+ import { syncCommand } from './commands/sync.js';
12
+ import { uninstallCommand } from './commands/uninstall.js';
13
+ import { whoamiCommand } from './commands/whoami.js';
14
+ const program = new Command()
15
+ .name('prave')
16
+ .description('Prave — Developer platform for Claude Skills')
17
+ .version('0.1.0');
18
+ program.command('login').description('Authenticate this machine').action(loginCommand);
19
+ program.command('logout').description('Remove stored credentials').action(logoutCommand);
20
+ program.command('whoami').description('Show the signed-in user').action(whoamiCommand);
21
+ program
22
+ .command('import')
23
+ .description('Scan ~/.claude/skills/ — optionally upload to Prave')
24
+ .option('--upload', 'upload scanned skills')
25
+ .option('--private', 'upload as private (requires --upload)')
26
+ .action(importCommand);
27
+ program
28
+ .command('install <slug>')
29
+ .description('Install a Skill into ~/.claude/skills/ (resolves deps)')
30
+ .option('--no-deps', 'skip transitive dependency resolution')
31
+ .option('--force', 'install even if SKILL.md is empty')
32
+ .action(installCommand);
33
+ program
34
+ .command('uninstall <slug>')
35
+ .description('Remove a locally installed Skill')
36
+ .action(uninstallCommand);
37
+ program
38
+ .command('sync')
39
+ .description('Pull updates for every locally installed Skill')
40
+ .action(syncCommand);
41
+ program
42
+ .command('list')
43
+ .description('List installed Skills (default) or remote ones')
44
+ .option('--remote', 'list Skills from your Prave account')
45
+ .action(listCommand);
46
+ program.command('search <query>').description('Search public Skills').action(searchCommand);
47
+ program
48
+ .command('export <slug>')
49
+ .description('Print or save a Skill\'s SKILL.md without installing')
50
+ .option('-o, --out <file>', 'write to file instead of stdout')
51
+ .action(exportCommand);
52
+ program
53
+ .command('diff <slug>')
54
+ .description('Show local vs registry diff for an installed Skill')
55
+ .action(diffCommand);
56
+ program.parseAsync().catch((err) => {
57
+ console.error(err.message);
58
+ process.exit(1);
59
+ });
@@ -0,0 +1,39 @@
1
+ import { request } from 'undici';
2
+ import { CONFIG } from './config.js';
3
+ import { loadCredentials } from './credentials.js';
4
+ export class ApiError extends Error {
5
+ status;
6
+ constructor(message, status) {
7
+ super(message);
8
+ this.status = status;
9
+ this.name = 'ApiError';
10
+ }
11
+ }
12
+ async function call(method, path, body, withAuth = false) {
13
+ const headers = { 'Content-Type': 'application/json' };
14
+ if (withAuth) {
15
+ const creds = await loadCredentials();
16
+ if (!creds)
17
+ throw new ApiError('Not logged in. Run `prave login`.', 401);
18
+ headers.Authorization = `Bearer ${creds.access_token}`;
19
+ }
20
+ const { statusCode, body: resBody } = await request(`${CONFIG.apiUrl}${path}`, {
21
+ method,
22
+ headers,
23
+ body: body ? JSON.stringify(body) : undefined,
24
+ });
25
+ const text = await resBody.text();
26
+ if (statusCode === 204)
27
+ return { data: undefined, status: statusCode };
28
+ const payload = text ? JSON.parse(text) : { success: true, data: null, error: null };
29
+ if (statusCode >= 400 || payload.success === false) {
30
+ throw new ApiError(payload.error ?? `HTTP ${statusCode}`, statusCode);
31
+ }
32
+ return { data: payload.data, status: statusCode };
33
+ }
34
+ export const api = {
35
+ get: (path, withAuth = false) => call('GET', path, undefined, withAuth),
36
+ post: (path, body, withAuth = false) => call('POST', path, body, withAuth),
37
+ put: (path, body, withAuth = false) => call('PUT', path, body, withAuth),
38
+ del: (path, withAuth = false) => call('DELETE', path, undefined, withAuth),
39
+ };
@@ -0,0 +1,12 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const CONFIG = {
4
+ apiUrl: process.env.PRAVE_API_URL ?? 'https://api.prave.app',
5
+ webUrl: process.env.PRAVE_WEB_URL ?? 'https://prave.app',
6
+ skillsDir: process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), '.claude', 'skills'),
7
+ praveDir: join(homedir(), '.prave'),
8
+ credentialsPath: join(homedir(), '.prave', 'credentials.json'),
9
+ configPath: join(homedir(), '.prave', 'config.json'),
10
+ pollIntervalMs: 2_000,
11
+ pollTimeoutMs: 10 * 60_000,
12
+ };
@@ -0,0 +1,25 @@
1
+ import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { CONFIG } from './config.js';
4
+ export async function saveCredentials(creds) {
5
+ await mkdir(dirname(CONFIG.credentialsPath), { recursive: true });
6
+ await writeFile(CONFIG.credentialsPath, JSON.stringify(creds, null, 2), 'utf8');
7
+ await chmod(CONFIG.credentialsPath, 0o600);
8
+ }
9
+ export async function loadCredentials() {
10
+ try {
11
+ const raw = await readFile(CONFIG.credentialsPath, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function clearCredentials() {
19
+ try {
20
+ await unlink(CONFIG.credentialsPath);
21
+ }
22
+ catch {
23
+ /* already gone */
24
+ }
25
+ }
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk';
2
+ export const log = {
3
+ info: (msg) => console.log(msg),
4
+ success: (msg) => console.log(chalk.green('✓'), msg),
5
+ warn: (msg) => console.log(chalk.yellow('!'), msg),
6
+ error: (msg) => console.error(chalk.red('✗'), msg),
7
+ dim: (msg) => console.log(chalk.dim(msg)),
8
+ kv: (k, v) => console.log(chalk.dim(`${k.padEnd(14)}`), v),
9
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@prave/cli",
3
+ "version": "0.1.0",
4
+ "description": "Prave CLI — import, export, install, sync Claude Skills.",
5
+ "type": "module",
6
+ "bin": {
7
+ "prave": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "commander": "^12.1.0",
16
+ "open": "^10.1.0",
17
+ "ora": "^8.0.1",
18
+ "undici": "^6.18.0",
19
+ "@prave/shared": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.12.7",
23
+ "tsx": "^4.11.0",
24
+ "typescript": "^5.4.5"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "dev": "tsx src/index.ts --help",
31
+ "cli": "tsx src/index.ts",
32
+ "build": "tsc -p tsconfig.json",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit",
34
+ "lint": "tsc -p tsconfig.json --noEmit"
35
+ }
36
+ }