dotai-cli 1.0.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,82 @@
1
+ import chalk from 'chalk';
2
+ import { spawn } from 'child_process';
3
+ import { platform } from 'os';
4
+ import Enquirer from 'enquirer';
5
+ const { prompt } = Enquirer;
6
+ import { listCentralSkills, getCentralSkill } from '../lib/skills.js';
7
+ import { getSkillsRepoDir } from '../lib/paths.js';
8
+
9
+ /**
10
+ * Open a skill folder or file in the system default editor/file manager
11
+ */
12
+ export async function openCommand(skillName, options) {
13
+ let skill;
14
+
15
+ // If no skill name, show interactive picker
16
+ if (!skillName) {
17
+ const skills = await listCentralSkills();
18
+ if (skills.length === 0) {
19
+ console.log(chalk.yellow('No skills found.'));
20
+ console.log(`Create one with: ${chalk.cyan('dotai skill create')}\n`);
21
+ return;
22
+ }
23
+
24
+ const answer = await prompt({
25
+ type: 'select',
26
+ name: 'skill',
27
+ message: 'Select a skill to open:',
28
+ choices: skills.map(s => ({
29
+ name: s.name,
30
+ message: `${s.name} - ${chalk.dim(s.description || 'No description')}`,
31
+ value: s.name
32
+ }))
33
+ });
34
+ skillName = answer.skill;
35
+ }
36
+
37
+ skill = await getCentralSkill(skillName);
38
+
39
+ if (!skill) {
40
+ console.error(chalk.red(`Skill '${skillName}' not found`));
41
+ return;
42
+ }
43
+
44
+ const targetPath = options.file ? skill.filePath : skill.path;
45
+ const openCmd = getOpenCommand();
46
+
47
+ console.log(chalk.dim(`Opening: ${targetPath}\n`));
48
+
49
+ spawn(openCmd, [targetPath], {
50
+ detached: true,
51
+ stdio: 'ignore'
52
+ }).unref();
53
+ }
54
+
55
+ /**
56
+ * Get the platform-specific command to open files/folders
57
+ */
58
+ function getOpenCommand() {
59
+ switch (platform()) {
60
+ case 'darwin':
61
+ return 'open';
62
+ case 'win32':
63
+ return 'explorer';
64
+ default:
65
+ return 'xdg-open';
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Open the entire skills repository folder
71
+ */
72
+ export async function openRepoCommand() {
73
+ const repoPath = getSkillsRepoDir();
74
+ const openCmd = getOpenCommand();
75
+
76
+ console.log(chalk.dim(`Opening skills repository: ${repoPath}\n`));
77
+
78
+ spawn(openCmd, [repoPath], {
79
+ detached: true,
80
+ stdio: 'ignore'
81
+ }).unref();
82
+ }
@@ -0,0 +1,71 @@
1
+ import chalk from 'chalk';
2
+ import Enquirer from 'enquirer';
3
+ const { prompt } = Enquirer;
4
+ import ora from 'ora';
5
+ import { listCentralSkills, installSkill, getSkillInstallStatus } from '../lib/skills.js';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { providers, getProviderIds } from '../providers/index.js';
8
+
9
+ export async function syncCommand(options) {
10
+ console.log(chalk.bold('\n🔄 Sync skills to providers\n'));
11
+
12
+ const config = await loadConfig();
13
+ const skills = await listCentralSkills();
14
+
15
+ if (skills.length === 0) {
16
+ console.log(chalk.yellow('No skills to sync.'));
17
+ console.log(`Create skills with: ${chalk.cyan('dotai skill create')}\n`);
18
+ return;
19
+ }
20
+
21
+ // Determine target providers
22
+ let targetProviders = options.providers
23
+ ? options.providers.split(',').map(p => p.trim())
24
+ : config.enabledProviders;
25
+
26
+ if (options.all) {
27
+ targetProviders = getProviderIds();
28
+ }
29
+
30
+ // Determine scope
31
+ const scope = options.project ? 'project' : 'global';
32
+
33
+ console.log(chalk.dim(`Syncing ${skills.length} skill(s) to ${targetProviders.length} provider(s)...\n`));
34
+
35
+ const results = {
36
+ installed: 0,
37
+ skipped: 0,
38
+ failed: 0
39
+ };
40
+
41
+ for (const skill of skills) {
42
+ const spinner = ora(`Syncing ${skill.name}...`).start();
43
+
44
+ try {
45
+ const installResults = await installSkill(skill.name, targetProviders, scope);
46
+
47
+ const successes = installResults.filter(r => r.success).length;
48
+ const failures = installResults.filter(r => !r.success).length;
49
+
50
+ results.installed += successes;
51
+ results.failed += failures;
52
+
53
+ if (failures === 0) {
54
+ spinner.succeed(`${skill.name} → ${successes} provider(s)`);
55
+ } else {
56
+ spinner.warn(`${skill.name} → ${successes} ok, ${failures} failed`);
57
+ }
58
+ } catch (err) {
59
+ spinner.fail(`${skill.name}: ${err.message}`);
60
+ results.failed++;
61
+ }
62
+ }
63
+
64
+ console.log('');
65
+ console.log(chalk.bold('Summary:'));
66
+ console.log(chalk.green(` ✓ Installed: ${results.installed}`));
67
+ if (results.failed > 0) {
68
+ console.log(chalk.red(` ✗ Failed: ${results.failed}`));
69
+ }
70
+ console.log('');
71
+ }
@@ -0,0 +1,92 @@
1
+ import chalk from 'chalk';
2
+ import Enquirer from 'enquirer';
3
+ const { prompt } = Enquirer;
4
+ import ora from 'ora';
5
+ import { listCentralSkills, uninstallSkillFromProvider, getSkillInstallStatus } from '../lib/skills.js';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { providers, getProviderIds } from '../providers/index.js';
8
+
9
+ export async function uninstallCommand(skillName, options) {
10
+ console.log(chalk.bold('\n🗑️ Uninstall skill from providers\n'));
11
+
12
+ const config = await loadConfig();
13
+
14
+ // If no skill name, show interactive picker
15
+ if (!skillName) {
16
+ const skills = await listCentralSkills();
17
+ if (skills.length === 0) {
18
+ console.log(chalk.yellow('No skills found.'));
19
+ return;
20
+ }
21
+
22
+ const answer = await prompt({
23
+ type: 'select',
24
+ name: 'skill',
25
+ message: 'Select a skill to uninstall:',
26
+ choices: skills.map(s => ({
27
+ name: s.name,
28
+ message: `${s.name} - ${chalk.dim(s.description || 'No description')}`,
29
+ value: s.name
30
+ }))
31
+ });
32
+ skillName = answer.skill;
33
+ }
34
+
35
+ // Determine which providers to uninstall from
36
+ let targetProviders = options.providers
37
+ ? options.providers.split(',').map(p => p.trim())
38
+ : config.enabledProviders;
39
+
40
+ if (options.all) {
41
+ targetProviders = getProviderIds();
42
+ }
43
+
44
+ // Determine scope
45
+ const scope = options.project ? 'project' : 'global';
46
+
47
+ // Confirm if not using --yes flag
48
+ if (!options.yes) {
49
+ const providerNames = targetProviders.map(id => providers[id]?.name || id).join(', ');
50
+ const answer = await prompt({
51
+ type: 'confirm',
52
+ name: 'confirm',
53
+ message: `Uninstall '${skillName}' from ${providerNames}?`,
54
+ initial: false
55
+ });
56
+
57
+ if (!answer.confirm) {
58
+ console.log(chalk.yellow('Cancelled.'));
59
+ return;
60
+ }
61
+ }
62
+
63
+ console.log(chalk.dim(`Uninstalling '${skillName}' from ${scope} scope...\n`));
64
+
65
+ let removedCount = 0;
66
+
67
+ for (const providerId of targetProviders) {
68
+ const provider = providers[providerId];
69
+ if (!provider) continue;
70
+
71
+ try {
72
+ const result = await uninstallSkillFromProvider(skillName, providerId, scope);
73
+
74
+ if (result.removed) {
75
+ console.log(chalk.green(` ✓ ${provider.name}`));
76
+ removedCount++;
77
+ } else {
78
+ console.log(chalk.dim(` - ${provider.name} (not installed)`));
79
+ }
80
+ } catch (err) {
81
+ console.log(chalk.red(` ✗ ${provider.name}: ${err.message}`));
82
+ }
83
+ }
84
+
85
+ console.log('');
86
+ if (removedCount > 0) {
87
+ console.log(chalk.green(`Removed from ${removedCount} provider(s)`));
88
+ } else {
89
+ console.log(chalk.yellow('Nothing to remove'));
90
+ }
91
+ console.log('');
92
+ }
package/src/index.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { initConfig } from './lib/config.js';
6
+ import { createCommand } from './commands/create.js';
7
+ import { installCommand } from './commands/install.js';
8
+ import { listCommand, listProvidersCommand } from './commands/list.js';
9
+ import { syncCommand } from './commands/sync.js';
10
+ import { uninstallCommand } from './commands/uninstall.js';
11
+ import { configCommand, enableCommand, disableCommand } from './commands/config.js';
12
+ import { openCommand, openRepoCommand } from './commands/open.js';
13
+
14
+ const program = new Command();
15
+
16
+ // Initialize config on startup
17
+ await initConfig();
18
+
19
+ program
20
+ .name('dotai')
21
+ .description('Dotfiles for AI - manage skills & MCP servers across all your AI coding assistants')
22
+ .version('1.0.0');
23
+
24
+ // Skill subcommand
25
+ const skill = program.command('skill').description('Manage AI agent skills');
26
+
27
+ skill
28
+ .command('create [name]')
29
+ .description('Create a new skill (opens editor for instructions)')
30
+ .option('-d, --description <desc>', 'Short description of the skill')
31
+ .action(createCommand);
32
+
33
+ skill
34
+ .command('install [skill]')
35
+ .description('Install a skill to providers')
36
+ .option('-p, --providers <list>', 'Comma-separated list of providers')
37
+ .option('-a, --all', 'Install to all providers')
38
+ .option('--project', 'Install to project scope (default: global)')
39
+ .option('-i, --interactive', 'Interactively select providers')
40
+ .action(installCommand);
41
+
42
+ skill
43
+ .command('uninstall [skill]')
44
+ .description('Uninstall a skill from providers')
45
+ .option('-p, --providers <list>', 'Comma-separated list of providers')
46
+ .option('-a, --all', 'Uninstall from all providers')
47
+ .option('--project', 'Uninstall from project scope (default: global)')
48
+ .option('-y, --yes', 'Skip confirmation')
49
+ .action(uninstallCommand);
50
+
51
+ skill
52
+ .command('list')
53
+ .alias('ls')
54
+ .description('List all skills in your repository')
55
+ .option('-v, --verbose', 'Show installation status')
56
+ .action(listCommand);
57
+
58
+ skill
59
+ .command('sync')
60
+ .description('Sync all skills to enabled providers')
61
+ .option('-p, --providers <list>', 'Comma-separated list of providers')
62
+ .option('-a, --all', 'Sync to all providers')
63
+ .option('--project', 'Sync to project scope (default: global)')
64
+ .action(syncCommand);
65
+
66
+ skill
67
+ .command('open [skill]')
68
+ .description('Open a skill folder or file')
69
+ .option('-f, --file', 'Open SKILL.md file directly')
70
+ .action(openCommand);
71
+
72
+ // MCP subcommand (placeholder for now)
73
+ const mcp = program.command('mcp').description('Manage MCP servers (coming soon)');
74
+
75
+ mcp
76
+ .command('add')
77
+ .description('Add an MCP server (coming soon)')
78
+ .action(() => {
79
+ console.log(chalk.yellow('\nMCP management coming soon!\n'));
80
+ console.log(chalk.dim('This will let you configure MCP servers once and sync to:'));
81
+ console.log(chalk.dim(' • Claude Code • Claude Desktop • Cursor'));
82
+ console.log(chalk.dim(' • VS Code • Cline • Windsurf\n'));
83
+ });
84
+
85
+ mcp
86
+ .command('list')
87
+ .description('List MCP servers (coming soon)')
88
+ .action(() => {
89
+ console.log(chalk.yellow('\nMCP management coming soon!\n'));
90
+ });
91
+
92
+ // Config commands
93
+ program
94
+ .command('providers')
95
+ .description('List all supported providers')
96
+ .action(listProvidersCommand);
97
+
98
+ program
99
+ .command('config')
100
+ .description('Configure dotai settings')
101
+ .option('-s, --show', 'Show current configuration')
102
+ .action(configCommand);
103
+
104
+ program
105
+ .command('enable <provider>')
106
+ .description('Enable a provider')
107
+ .action(enableCommand);
108
+
109
+ program
110
+ .command('disable <provider>')
111
+ .description('Disable a provider')
112
+ .action(disableCommand);
113
+
114
+ program
115
+ .command('repo')
116
+ .description('Open the dotai repository folder')
117
+ .action(openRepoCommand);
118
+
119
+ // Show help if no command
120
+ program.parse();
121
+
122
+ if (!process.argv.slice(2).length) {
123
+ console.log(chalk.bold(`
124
+ ╔═══════════════════════════════════════════════════════════╗
125
+ ║ dotai v1.0.0 ║
126
+ ║ Dotfiles for AI - Skills & MCP in one place ║
127
+ ╚═══════════════════════════════════════════════════════════╝
128
+ `));
129
+
130
+ console.log(chalk.bold(' Skills - manage across Claude, Gemini, Cursor, Codex & more:'));
131
+ console.log(` ${chalk.cyan('dotai skill create')} Create a new skill`);
132
+ console.log(` ${chalk.cyan('dotai skill install <name>')} Install to all providers`);
133
+ console.log(` ${chalk.cyan('dotai skill list')} List your skills`);
134
+ console.log(` ${chalk.cyan('dotai skill sync')} Sync all skills\n`);
135
+
136
+ console.log(chalk.bold(' MCP Servers - coming soon:'));
137
+ console.log(` ${chalk.dim('dotai mcp add')} Add an MCP server`);
138
+ console.log(` ${chalk.dim('dotai mcp sync')} Sync to all apps\n`);
139
+
140
+ console.log(chalk.dim(' Run "dotai --help" for all commands\n'));
141
+ }
@@ -0,0 +1,90 @@
1
+ import fs from 'fs-extra';
2
+ import { getConfigDir, getConfigFilePath, getSkillsRepoDir } from './paths.js';
3
+ import { getProviderIds } from '../providers/index.js';
4
+
5
+ const defaultConfig = {
6
+ enabledProviders: getProviderIds(),
7
+ defaultScope: 'global', // 'global' or 'project'
8
+ skillsRepo: null, // Custom skills repo path (null = use default)
9
+ createdAt: new Date().toISOString()
10
+ };
11
+
12
+ /**
13
+ * Initialize config directory and files
14
+ */
15
+ export async function initConfig() {
16
+ const configDir = getConfigDir();
17
+ const skillsRepoDir = getSkillsRepoDir();
18
+ const configFile = getConfigFilePath();
19
+
20
+ // Create directories
21
+ await fs.ensureDir(configDir);
22
+ await fs.ensureDir(skillsRepoDir);
23
+
24
+ // Create config file if it doesn't exist
25
+ if (!await fs.pathExists(configFile)) {
26
+ await fs.writeJson(configFile, defaultConfig, { spaces: 2 });
27
+ }
28
+
29
+ return loadConfig();
30
+ }
31
+
32
+ /**
33
+ * Load config from file
34
+ */
35
+ export async function loadConfig() {
36
+ const configFile = getConfigFilePath();
37
+
38
+ if (!await fs.pathExists(configFile)) {
39
+ return { ...defaultConfig };
40
+ }
41
+
42
+ try {
43
+ const config = await fs.readJson(configFile);
44
+ return { ...defaultConfig, ...config };
45
+ } catch (err) {
46
+ console.error('Error loading config:', err.message);
47
+ return { ...defaultConfig };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Save config to file
53
+ */
54
+ export async function saveConfig(config) {
55
+ const configFile = getConfigFilePath();
56
+ await fs.ensureDir(getConfigDir());
57
+ await fs.writeJson(configFile, config, { spaces: 2 });
58
+ }
59
+
60
+ /**
61
+ * Update specific config values
62
+ */
63
+ export async function updateConfig(updates) {
64
+ const config = await loadConfig();
65
+ const newConfig = { ...config, ...updates, updatedAt: new Date().toISOString() };
66
+ await saveConfig(newConfig);
67
+ return newConfig;
68
+ }
69
+
70
+ /**
71
+ * Enable a provider
72
+ */
73
+ export async function enableProvider(providerId) {
74
+ const config = await loadConfig();
75
+ if (!config.enabledProviders.includes(providerId)) {
76
+ config.enabledProviders.push(providerId);
77
+ await saveConfig(config);
78
+ }
79
+ return config;
80
+ }
81
+
82
+ /**
83
+ * Disable a provider
84
+ */
85
+ export async function disableProvider(providerId) {
86
+ const config = await loadConfig();
87
+ config.enabledProviders = config.enabledProviders.filter(id => id !== providerId);
88
+ await saveConfig(config);
89
+ return config;
90
+ }
@@ -0,0 +1,60 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { platform } from 'process';
4
+
5
+ /**
6
+ * Get the user's home directory (cross-platform)
7
+ */
8
+ export function getHomeDir() {
9
+ return homedir();
10
+ }
11
+
12
+ /**
13
+ * Get the dotai config directory
14
+ * Mac/Linux: ~/.dotai
15
+ * Windows: %USERPROFILE%\.dotai
16
+ */
17
+ export function getConfigDir() {
18
+ return join(getHomeDir(), '.dotai');
19
+ }
20
+
21
+ /**
22
+ * Get the central skills repository directory
23
+ */
24
+ export function getSkillsRepoDir() {
25
+ return join(getConfigDir(), 'skills');
26
+ }
27
+
28
+ /**
29
+ * Get the config file path
30
+ */
31
+ export function getConfigFilePath() {
32
+ return join(getConfigDir(), 'config.json');
33
+ }
34
+
35
+ /**
36
+ * Check if running on Windows
37
+ */
38
+ export function isWindows() {
39
+ return platform === 'win32';
40
+ }
41
+
42
+ /**
43
+ * Normalize path for the current OS
44
+ */
45
+ export function normalizePath(p) {
46
+ if (isWindows()) {
47
+ return p.replace(/\//g, '\\');
48
+ }
49
+ return p;
50
+ }
51
+
52
+ /**
53
+ * Expand ~ to home directory
54
+ */
55
+ export function expandHome(p) {
56
+ if (p.startsWith('~')) {
57
+ return join(getHomeDir(), p.slice(1));
58
+ }
59
+ return p;
60
+ }