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
@@ -0,0 +1,193 @@
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
+ interface EnvVarConfig {
15
+ description: string;
16
+ example: string;
17
+ getUrl: () => string;
18
+ }
19
+
20
+ /**
21
+ * Required environment variables for MCP servers
22
+ */
23
+ const REQUIRED_ENV_VARS: Record<string, EnvVarConfig> = {
24
+ CONTEXT7_API_KEY: {
25
+ description: 'Context7 MCP server API key',
26
+ example: 'ctx7sk-your-api-key-here',
27
+ getUrl: () => 'https://context7.com/',
28
+ },
29
+ };
30
+
31
+ /**
32
+ * Ensure config directory and .env file exist
33
+ */
34
+ export function ensureEnvFile(): boolean {
35
+ // Create config directory if missing
36
+ if (!fs.existsSync(CONFIG_DIR)) {
37
+ fs.ensureDirSync(CONFIG_DIR);
38
+ console.log(kleur.gray(` Created config directory: ${CONFIG_DIR}`));
39
+ }
40
+
41
+ // Create .env.example if missing
42
+ if (!fs.existsSync(ENV_EXAMPLE_FILE)) {
43
+ createEnvExample();
44
+ }
45
+
46
+ // Create .env if missing
47
+ if (!fs.existsSync(ENV_FILE)) {
48
+ createEnvFile();
49
+ return false; // File was created (user needs to fill it)
50
+ }
51
+
52
+ return true; // File already exists
53
+ }
54
+
55
+ function createEnvExample(): void {
56
+ const content = [
57
+ '# Jaggers Agent Tools - Environment Variables',
58
+ '# Copy this file to .env and fill in your actual values',
59
+ '',
60
+ ...Object.entries(REQUIRED_ENV_VARS).map(([key, config]) => {
61
+ return [
62
+ `# ${config.description}`,
63
+ `# Get your key from: ${config.getUrl()}`,
64
+ `${key}=${config.example}`,
65
+ '',
66
+ ].join('\n');
67
+ }),
68
+ '# See config/.env.example in the repository for all available options',
69
+ '',
70
+ ].join('\n');
71
+
72
+ fs.writeFileSync(ENV_EXAMPLE_FILE, content);
73
+ console.log(kleur.gray(` Created example file: ${ENV_EXAMPLE_FILE}`));
74
+ }
75
+
76
+ function createEnvFile(): void {
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(): Record<string, string> {
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
+ * Pass an optional `subset` of var names to check only those.
113
+ * Known vars (in REQUIRED_ENV_VARS) get rich metadata; unknown vars are
114
+ * still checked so server-defined ${VAR} references are never silently dropped.
115
+ * Returns array of missing variable names.
116
+ */
117
+ export function checkRequiredEnvVars(subset?: string[]): string[] {
118
+ const missing: string[] = [];
119
+ const keysToCheck = subset ?? Object.keys(REQUIRED_ENV_VARS);
120
+
121
+ for (const key of keysToCheck) {
122
+ if (!process.env[key]) {
123
+ missing.push(key);
124
+ }
125
+ }
126
+
127
+ return missing;
128
+ }
129
+
130
+ /**
131
+ * Prompt user to enter missing environment variables interactively.
132
+ * Saves provided values to .env and sets them in process.env.
133
+ * Returns true if all missing vars were provided (sync can continue).
134
+ */
135
+ export async function handleMissingEnvVars(missing: string[]): Promise<boolean> {
136
+ if (missing.length === 0) {
137
+ return true;
138
+ }
139
+
140
+ // @ts-ignore
141
+ const prompts = (await import('prompts')).default;
142
+
143
+ const answers: Record<string, string> = {};
144
+
145
+ for (const key of missing) {
146
+ const config = REQUIRED_ENV_VARS[key];
147
+ if (config) {
148
+ console.log(kleur.yellow(`\n ⚠️ ${config.description} is required`));
149
+ console.log(kleur.dim(` Get your key from: ${config.getUrl()}`));
150
+ } else {
151
+ console.log(kleur.yellow(`\n ⚠️ ${key} is required by a selected MCP server`));
152
+ }
153
+
154
+ const { value } = await prompts({
155
+ type: 'text',
156
+ name: 'value',
157
+ message: `Enter ${key}:`,
158
+ validate: (v: string) => v.trim().length > 0 || 'Value cannot be empty'
159
+ });
160
+
161
+ if (!value) {
162
+ console.log(kleur.gray(` Skipped — ${key} not provided. MCP server will be skipped.`));
163
+ return false;
164
+ }
165
+
166
+ answers[key] = value.trim();
167
+ process.env[key] = value.trim();
168
+ }
169
+
170
+ // Persist to .env file
171
+ let envContent = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
172
+ for (const [key, value] of Object.entries(answers)) {
173
+ const line = `${key}=${value}`;
174
+ const regex = new RegExp(`^${key}=.*$`, 'm');
175
+ if (regex.test(envContent)) {
176
+ envContent = envContent.replace(regex, line);
177
+ } else {
178
+ envContent += `\n${line}\n`;
179
+ }
180
+ }
181
+ fs.writeFileSync(ENV_FILE, envContent);
182
+ console.log(kleur.green(` ✓ Saved to ${ENV_FILE}`));
183
+
184
+ return true;
185
+ }
186
+
187
+ export function getEnvFilePath(): string {
188
+ return ENV_FILE;
189
+ }
190
+
191
+ export function getConfigDir(): string {
192
+ return CONFIG_DIR;
193
+ }
@@ -0,0 +1,42 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs-extra';
3
+ import { join } from 'path';
4
+
5
+ export async function hashFile(filePath: string): Promise<string> {
6
+ const content = await fs.readFile(filePath);
7
+ return createHash('md5').update(content).digest('hex');
8
+ }
9
+
10
+ export async function hashDirectory(dirPath: string): Promise<string> {
11
+ if (!(await fs.pathExists(dirPath))) return '';
12
+ const stats = await fs.stat(dirPath);
13
+
14
+ if (!stats.isDirectory()) {
15
+ return hashFile(dirPath);
16
+ }
17
+
18
+ const children = await fs.readdir(dirPath);
19
+ const childHashes = await Promise.all(
20
+ children.sort().map(async (child) => {
21
+ const h = await hashDirectory(join(dirPath, child));
22
+ return `${child}:${h}`;
23
+ })
24
+ );
25
+ return createHash('md5').update(childHashes.join('|')).digest('hex');
26
+ }
27
+
28
+ export async function getNewestMtime(targetPath: string): Promise<number> {
29
+ if (!(await fs.pathExists(targetPath))) return 0;
30
+
31
+ const stats = await fs.stat(targetPath);
32
+ let maxTime = 0;
33
+
34
+ if (stats.isDirectory()) {
35
+ const children = await fs.readdir(targetPath);
36
+ for (const child of children) {
37
+ const childTime = await getNewestMtime(join(targetPath, child));
38
+ if (childTime > maxTime) maxTime = childTime;
39
+ }
40
+ }
41
+ return maxTime;
42
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ async function walkUp(startDir: string): Promise<string | null> {
5
+ let dir = path.resolve(startDir);
6
+
7
+ while (true) {
8
+ const skillsPath = path.join(dir, 'skills');
9
+ const hooksPath = path.join(dir, 'hooks');
10
+
11
+ if (await fs.pathExists(skillsPath) && await fs.pathExists(hooksPath)) {
12
+ return dir;
13
+ }
14
+
15
+ const parent = path.dirname(dir);
16
+ if (parent === dir) return null;
17
+ dir = parent;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Finds the jaggers-agent-tools repo root by:
23
+ * 1. Walking up from process.cwd() — works when run inside a cloned repo.
24
+ * 2. Walking up from __dirname — works when run via `npx`, where the
25
+ * package is extracted to a temp/cache directory that contains skills/ & hooks/.
26
+ */
27
+ export async function findRepoRoot(): Promise<string> {
28
+ const fromCwd = await walkUp(process.cwd());
29
+ if (fromCwd) return fromCwd;
30
+
31
+ // __dirname is cli/dist/ inside the package — two levels up is the repo root.
32
+ const fromBundle = await walkUp(path.resolve(__dirname, '..', '..'));
33
+ if (fromBundle) return fromBundle;
34
+
35
+ throw new Error(
36
+ 'Could not locate jaggers-agent-tools repo root.\n' +
37
+ 'Run via `npx -y github:Jaggerxtrm/jaggers-agent-tools` or from within the cloned repository.'
38
+ );
39
+ }