xtrm-cli 2.1.4

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.
Files changed (50) hide show
  1. package/.gemini/settings.json +39 -0
  2. package/dist/index.cjs +55937 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2 -0
  5. package/index.js +151 -0
  6. package/lib/atomic-config.js +236 -0
  7. package/lib/config-adapter.js +231 -0
  8. package/lib/config-injector.js +80 -0
  9. package/lib/context.js +73 -0
  10. package/lib/diff.js +142 -0
  11. package/lib/env-manager.js +160 -0
  12. package/lib/sync-mcp-cli.js +345 -0
  13. package/lib/sync.js +227 -0
  14. package/lib/transform-gemini.js +119 -0
  15. package/package.json +43 -0
  16. package/src/adapters/base.ts +29 -0
  17. package/src/adapters/claude.ts +38 -0
  18. package/src/adapters/registry.ts +21 -0
  19. package/src/commands/help.ts +171 -0
  20. package/src/commands/install-project.ts +566 -0
  21. package/src/commands/install-service-skills.ts +251 -0
  22. package/src/commands/install.ts +534 -0
  23. package/src/commands/reset.ts +12 -0
  24. package/src/commands/status.ts +170 -0
  25. package/src/core/context.ts +141 -0
  26. package/src/core/diff.ts +143 -0
  27. package/src/core/interactive-plan.ts +165 -0
  28. package/src/core/manifest.ts +26 -0
  29. package/src/core/preflight.ts +142 -0
  30. package/src/core/rollback.ts +32 -0
  31. package/src/core/sync-executor.ts +399 -0
  32. package/src/index.ts +69 -0
  33. package/src/types/config.ts +51 -0
  34. package/src/types/models.ts +52 -0
  35. package/src/utils/atomic-config.ts +222 -0
  36. package/src/utils/banner.ts +194 -0
  37. package/src/utils/config-adapter.ts +90 -0
  38. package/src/utils/config-injector.ts +81 -0
  39. package/src/utils/env-manager.ts +193 -0
  40. package/src/utils/hash.ts +42 -0
  41. package/src/utils/repo-root.ts +39 -0
  42. package/src/utils/sync-mcp-cli.ts +467 -0
  43. package/src/utils/theme.ts +37 -0
  44. package/test/context.test.ts +33 -0
  45. package/test/hooks.test.ts +277 -0
  46. package/test/install-project.test.ts +235 -0
  47. package/test/install-service-skills.test.ts +111 -0
  48. package/tsconfig.json +22 -0
  49. package/tsup.config.ts +17 -0
  50. package/vitest.config.ts +9 -0
package/lib/context.js ADDED
@@ -0,0 +1,73 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import Conf from 'conf';
5
+ import prompts from 'prompts';
6
+ import kleur from 'kleur';
7
+
8
+ // Initialize configuration (persists sync mode preference only)
9
+ const config = new Conf({
10
+ projectName: 'jaggers-config-manager',
11
+ defaults: {
12
+ syncMode: 'copy' // 'copy' or 'symlink'
13
+ }
14
+ });
15
+
16
+ // Define known paths including user requested ones
17
+ const CANDIDATE_PATHS = [
18
+ { label: '.claude', path: path.join(os.homedir(), '.claude') },
19
+ { label: '.gemini', path: path.join(os.homedir(), '.gemini') },
20
+ { label: '.qwen', path: path.join(os.homedir(), '.qwen') },
21
+ { label: '~/.gemini/antigravity', path: path.join(os.homedir(), '.gemini', 'antigravity') },
22
+ // Standard XDG/Windows paths
23
+ process.env.APPDATA ? { label: 'AppData/Claude', path: path.join(process.env.APPDATA, 'Claude') } : null
24
+ ].filter(Boolean);
25
+
26
+ export async function getContext() {
27
+ // 1. Identify Existing vs Missing Paths
28
+ const choices = [];
29
+
30
+ for (const c of CANDIDATE_PATHS) {
31
+ const exists = await fs.pathExists(c.path);
32
+ const icon = exists ? '[X]' : '[ ]';
33
+ const desc = exists ? 'Found' : 'Not found (will create)';
34
+
35
+ choices.push({
36
+ title: `${icon} ${c.label} (${c.path})`,
37
+ description: desc,
38
+ value: c.path,
39
+ selected: exists // Pre-select existing environments
40
+ });
41
+ }
42
+
43
+ // 2. Prompt user with Multiselect
44
+ const response = await prompts({
45
+ type: 'multiselect',
46
+ name: 'targets',
47
+ message: 'Select target environment(s):',
48
+ choices: choices,
49
+ hint: '- Space to select. Return to submit',
50
+ instructions: false
51
+ });
52
+
53
+ if (!response.targets || response.targets.length === 0) {
54
+ console.log(kleur.gray('No targets selected. Exiting.'));
55
+ process.exit(0);
56
+ }
57
+
58
+ // 3. Ensure directories exist for selected targets
59
+ for (const target of response.targets) {
60
+ await fs.ensureDir(target);
61
+ }
62
+
63
+ return {
64
+ targets: response.targets, // Array of path strings
65
+ syncMode: config.get('syncMode'),
66
+ config
67
+ };
68
+ }
69
+
70
+ export function resetContext() {
71
+ config.clear();
72
+ console.log(kleur.yellow('Configuration cleared.'));
73
+ }
package/lib/diff.js ADDED
@@ -0,0 +1,142 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import crypto from 'crypto';
4
+
5
+ /**
6
+ * Calculate MD5 hash of a file or directory
7
+ */
8
+ async function getHash(targetPath) {
9
+ if (!fs.existsSync(targetPath)) return null;
10
+
11
+ const stats = await fs.stat(targetPath);
12
+
13
+ if (stats.isDirectory()) {
14
+ const children = await fs.readdir(targetPath);
15
+ const childHashes = await Promise.all(
16
+ children.sort().map(async child => {
17
+ const h = await getHash(path.join(targetPath, child));
18
+ return `${child}:${h}`;
19
+ })
20
+ );
21
+ return crypto.createHash('md5').update(childHashes.join('|')).digest('hex');
22
+ } else {
23
+ const content = await fs.readFile(targetPath);
24
+ return crypto.createHash('md5').update(content).digest('hex');
25
+ }
26
+ }
27
+
28
+ async function getNewestMtime(targetPath) {
29
+ const stats = await fs.stat(targetPath);
30
+ let maxTime = stats.mtimeMs;
31
+
32
+ if (stats.isDirectory()) {
33
+ const children = await fs.readdir(targetPath);
34
+ for (const child of children) {
35
+ const childPath = path.join(targetPath, child);
36
+ const childTime = await getNewestMtime(childPath);
37
+ if (childTime > maxTime) maxTime = childTime;
38
+ }
39
+ }
40
+ return maxTime;
41
+ }
42
+
43
+ export async function calculateDiff(repoRoot, systemRoot) {
44
+ const isClaude = systemRoot.includes('.claude') || systemRoot.includes('Claude');
45
+ const isQwen = systemRoot.includes('.qwen') || systemRoot.includes('Qwen');
46
+ const isGemini = systemRoot.includes('.gemini') || systemRoot.includes('Gemini');
47
+
48
+ const changeSet = {
49
+ skills: { missing: [], outdated: [], drifted: [], total: 0 },
50
+ hooks: { missing: [], outdated: [], drifted: [], total: 0 },
51
+ config: { missing: [], outdated: [], drifted: [], total: 0 },
52
+ commands: { missing: [], outdated: [], drifted: [], total: 0 },
53
+ 'qwen-commands': { missing: [], outdated: [], drifted: [], total: 0 },
54
+ 'antigravity-workflows': { missing: [], outdated: [], drifted: [], total: 0 }
55
+ };
56
+
57
+ // 1. Folders: Skills & Hooks & Commands (for different environments)
58
+ const folders = ['skills', 'hooks'];
59
+ if (isQwen) {
60
+ folders.push('qwen-commands');
61
+ } else if (isGemini) {
62
+ folders.push('commands', 'antigravity-workflows');
63
+ } else if (!isClaude) {
64
+ folders.push('commands');
65
+ }
66
+
67
+ for (const category of folders) {
68
+ let repoPath;
69
+ if (category === 'commands') {
70
+ // Commands are always in .gemini/commands in repo
71
+ repoPath = path.join(repoRoot, '.gemini', 'commands');
72
+ } else if (category === 'qwen-commands') {
73
+ // Qwen commands are in .qwen/commands in repo
74
+ repoPath = path.join(repoRoot, '.qwen', 'commands');
75
+ } else if (category === 'antigravity-workflows') {
76
+ // Antigravity workflows are in .gemini/antigravity/global_workflows in repo
77
+ repoPath = path.join(repoRoot, '.gemini', 'antigravity', 'global_workflows');
78
+ } else {
79
+ repoPath = path.join(repoRoot, category);
80
+ }
81
+
82
+ let systemPath;
83
+ if (category === 'qwen-commands') {
84
+ systemPath = path.join(systemRoot, 'commands');
85
+ } else if (category === 'antigravity-workflows') {
86
+ systemPath = path.join(systemRoot, '.gemini', 'antigravity', 'global_workflows');
87
+ } else if (category === 'commands') {
88
+ systemPath = path.join(systemRoot, category);
89
+ } else {
90
+ systemPath = path.join(systemRoot, category);
91
+ }
92
+
93
+ if (!fs.existsSync(repoPath)) continue;
94
+
95
+ const items = await fs.readdir(repoPath);
96
+ changeSet[category].total = items.length;
97
+
98
+ for (const item of items) {
99
+ const itemRepoPath = path.join(repoPath, item);
100
+ const itemSystemPath = path.join(systemPath, item);
101
+
102
+ await compareItem(category, item, itemRepoPath, itemSystemPath, changeSet);
103
+ }
104
+ }
105
+
106
+ // 2. Config Files (Explicit Mapping)
107
+ const configMapping = {
108
+ 'settings.json': { repo: 'config/settings.json', sys: 'settings.json' }
109
+ };
110
+
111
+ for (const [name, paths] of Object.entries(configMapping)) {
112
+ const itemRepoPath = path.join(repoRoot, paths.repo);
113
+ const itemSystemPath = path.join(systemRoot, paths.sys);
114
+
115
+ if (fs.existsSync(itemRepoPath)) {
116
+ await compareItem('config', name, itemRepoPath, itemSystemPath, changeSet);
117
+ }
118
+ }
119
+
120
+ return changeSet;
121
+ }
122
+
123
+ async function compareItem(category, item, repoPath, systemPath, changeSet) {
124
+ if (!fs.existsSync(systemPath)) {
125
+ changeSet[category].missing.push(item);
126
+ return;
127
+ }
128
+
129
+ const repoHash = await getHash(repoPath);
130
+ const systemHash = await getHash(systemPath);
131
+
132
+ if (repoHash !== systemHash) {
133
+ const repoMtime = await getNewestMtime(repoPath);
134
+ const systemMtime = await getNewestMtime(systemPath);
135
+
136
+ if (systemMtime > repoMtime + 2000) {
137
+ changeSet[category].drifted.push(item);
138
+ } else {
139
+ changeSet[category].outdated.push(item);
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,160 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import kleur from 'kleur';
5
+ import dotenv from 'dotenv';
6
+
7
+ /**
8
+ * Environment file location: ~/.config/jaggers-agent-tools/.env
9
+ */
10
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'jaggers-agent-tools');
11
+ const ENV_FILE = path.join(CONFIG_DIR, '.env');
12
+ const ENV_EXAMPLE_FILE = path.join(CONFIG_DIR, '.env.example');
13
+
14
+ /**
15
+ * Required environment variables for MCP servers
16
+ */
17
+ const REQUIRED_ENV_VARS = {
18
+ 'CONTEXT7_API_KEY': {
19
+ description: 'Context7 MCP server API key',
20
+ example: 'ctx7sk-your-api-key-here',
21
+ getUrl: () => 'https://context7.com/'
22
+ }
23
+ };
24
+
25
+ /**
26
+ * Ensure config directory and .env file exist
27
+ */
28
+ export function ensureEnvFile() {
29
+ // Create config directory if missing
30
+ if (!fs.existsSync(CONFIG_DIR)) {
31
+ fs.ensureDirSync(CONFIG_DIR);
32
+ console.log(kleur.gray(` Created config directory: ${CONFIG_DIR}`));
33
+ }
34
+
35
+ // Create .env.example if missing
36
+ if (!fs.existsSync(ENV_EXAMPLE_FILE)) {
37
+ createEnvExample();
38
+ }
39
+
40
+ // Create .env if missing
41
+ if (!fs.existsSync(ENV_FILE)) {
42
+ createEnvFile();
43
+ return false; // File was created (user needs to fill it)
44
+ }
45
+
46
+ return true; // File already exists
47
+ }
48
+
49
+ /**
50
+ * Create .env.example file
51
+ */
52
+ function createEnvExample() {
53
+ const content = [
54
+ '# Jaggers Agent Tools - Environment Variables',
55
+ '# Copy this file to .env and fill in your actual values',
56
+ '',
57
+ ...Object.entries(REQUIRED_ENV_VARS).map(([key, config]) => {
58
+ return [
59
+ `# ${config.description}`,
60
+ `# Get your key from: ${config.getUrl()}`,
61
+ `${key}=${config.example}`,
62
+ ''
63
+ ].join('\n');
64
+ }).join('\n'),
65
+ '# See config/.env.example in the repository for all available options',
66
+ ''
67
+ ];
68
+
69
+ fs.writeFileSync(ENV_EXAMPLE_FILE, content);
70
+ console.log(kleur.gray(` Created example file: ${ENV_EXAMPLE_FILE}`));
71
+ }
72
+
73
+ /**
74
+ * Create empty .env file with header
75
+ */
76
+ function createEnvFile() {
77
+ const content = [
78
+ '# Jaggers Agent Tools - Environment Variables',
79
+ '# Generated automatically by jaggers-agent-tools CLI',
80
+ '',
81
+ '# Copy values from .env.example and fill in your actual keys',
82
+ '',
83
+ ].join('\n');
84
+
85
+ fs.writeFileSync(ENV_FILE, content);
86
+ console.log(kleur.green(` Created environment file: ${ENV_FILE}`));
87
+ }
88
+
89
+ /**
90
+ * Load environment variables from .env file
91
+ * Also loads from process.env (which takes precedence)
92
+ */
93
+ export function loadEnvFile() {
94
+ if (fs.existsSync(ENV_FILE)) {
95
+ const envConfig = dotenv.parse(fs.readFileSync(ENV_FILE));
96
+
97
+ // Merge with process.env (process.env takes precedence)
98
+ for (const [key, value] of Object.entries(envConfig)) {
99
+ if (!process.env[key]) {
100
+ process.env[key] = value;
101
+ }
102
+ }
103
+
104
+ return envConfig;
105
+ }
106
+
107
+ return {};
108
+ }
109
+
110
+ /**
111
+ * Check if required environment variables are set
112
+ * Returns array of missing variable names
113
+ */
114
+ export function checkRequiredEnvVars() {
115
+ const missing = [];
116
+
117
+ for (const [key, config] of Object.entries(REQUIRED_ENV_VARS)) {
118
+ if (!process.env[key]) {
119
+ missing.push(key);
120
+ }
121
+ }
122
+
123
+ return missing;
124
+ }
125
+
126
+ /**
127
+ * Prompt user about missing environment variables
128
+ * Returns true if user wants to proceed anyway
129
+ */
130
+ export function handleMissingEnvVars(missing) {
131
+ if (missing.length === 0) {
132
+ return true;
133
+ }
134
+
135
+ console.log(kleur.yellow('\n ⚠️ Missing environment variables:'));
136
+ for (const key of missing) {
137
+ const config = REQUIRED_ENV_VARS[key];
138
+ console.log(kleur.yellow(` - ${key}: ${config.description}`));
139
+ console.log(kleur.dim(` Get your key from: ${config.getUrl()}`));
140
+ }
141
+
142
+ console.log(kleur.yellow(`\n Please edit: ${ENV_FILE}`));
143
+ console.log(kleur.gray(` Or copy from example: ${ENV_EXAMPLE_FILE}`));
144
+
145
+ return false; // Don't proceed automatically
146
+ }
147
+
148
+ /**
149
+ * Get the path to the .env file
150
+ */
151
+ export function getEnvFilePath() {
152
+ return ENV_FILE;
153
+ }
154
+
155
+ /**
156
+ * Get the path to the config directory
157
+ */
158
+ export function getConfigDir() {
159
+ return CONFIG_DIR;
160
+ }