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.
- package/.gemini/settings.json +39 -0
- package/dist/index.cjs +55937 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/index.js +151 -0
- package/lib/atomic-config.js +236 -0
- package/lib/config-adapter.js +231 -0
- package/lib/config-injector.js +80 -0
- package/lib/context.js +73 -0
- package/lib/diff.js +142 -0
- package/lib/env-manager.js +160 -0
- package/lib/sync-mcp-cli.js +345 -0
- package/lib/sync.js +227 -0
- package/lib/transform-gemini.js +119 -0
- package/package.json +43 -0
- package/src/adapters/base.ts +29 -0
- package/src/adapters/claude.ts +38 -0
- package/src/adapters/registry.ts +21 -0
- package/src/commands/help.ts +171 -0
- package/src/commands/install-project.ts +566 -0
- package/src/commands/install-service-skills.ts +251 -0
- package/src/commands/install.ts +534 -0
- package/src/commands/reset.ts +12 -0
- package/src/commands/status.ts +170 -0
- package/src/core/context.ts +141 -0
- package/src/core/diff.ts +143 -0
- package/src/core/interactive-plan.ts +165 -0
- package/src/core/manifest.ts +26 -0
- package/src/core/preflight.ts +142 -0
- package/src/core/rollback.ts +32 -0
- package/src/core/sync-executor.ts +399 -0
- package/src/index.ts +69 -0
- package/src/types/config.ts +51 -0
- package/src/types/models.ts +52 -0
- package/src/utils/atomic-config.ts +222 -0
- package/src/utils/banner.ts +194 -0
- package/src/utils/config-adapter.ts +90 -0
- package/src/utils/config-injector.ts +81 -0
- package/src/utils/env-manager.ts +193 -0
- package/src/utils/hash.ts +42 -0
- package/src/utils/repo-root.ts +39 -0
- package/src/utils/sync-mcp-cli.ts +467 -0
- package/src/utils/theme.ts +37 -0
- package/test/context.test.ts +33 -0
- package/test/hooks.test.ts +277 -0
- package/test/install-project.test.ts +235 -0
- package/test/install-service-skills.test.ts +111 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +17 -0
- 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
|
+
}
|