dxai-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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +272 -0
- package/package.json +64 -0
- package/src/auto-update.js +106 -0
- package/src/branding.js +80 -0
- package/src/cleanup.js +615 -0
- package/src/config-remover.js +325 -0
- package/src/config-writer.js +781 -0
- package/src/detect-project.js +316 -0
- package/src/detect.js +587 -0
- package/src/fs-atomic.js +35 -0
- package/src/handshake.js +123 -0
- package/src/index.js +966 -0
- package/src/inspect.js +283 -0
- package/src/manifest.js +179 -0
- package/src/mcp-cmd.js +282 -0
- package/src/net.js +72 -0
- package/src/profile.js +139 -0
- package/src/registry/automation-tools.js +6 -0
- package/src/registry/data/automation-tools.json +37 -0
- package/src/registry/data/mcp-servers.json +451 -0
- package/src/registry/data/skills.json +132 -0
- package/src/registry/loader.js +102 -0
- package/src/registry/mcp-registry.js +292 -0
- package/src/registry/mcp-servers.js +72 -0
- package/src/registry/skills.js +10 -0
- package/src/registry/stacks.js +769 -0
- package/src/registry/validate.js +209 -0
- package/src/rollback.js +182 -0
- package/src/runtime.js +40 -0
- package/src/select.js +72 -0
- package/src/update.js +126 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
// ── Stack Signal Mapping ──
|
|
6
|
+
|
|
7
|
+
const STACK_SIGNALS = {
|
|
8
|
+
react: { deps: ['react', 'next', 'react-dom', '@remix-run/react'] },
|
|
9
|
+
vue: { deps: ['vue', 'nuxt', '@nuxt/kit'] },
|
|
10
|
+
svelte: { deps: ['svelte', '@sveltejs/kit'] },
|
|
11
|
+
node: { deps: ['express', 'fastify', 'koa', 'hono', '@nestjs/core'] },
|
|
12
|
+
python: { files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'] },
|
|
13
|
+
go: { files: ['go.mod'] },
|
|
14
|
+
rust: { files: ['Cargo.toml'] },
|
|
15
|
+
mobile: { deps: ['react-native', 'expo'], files: ['pubspec.yaml'] },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ── Manifest Detection ──
|
|
19
|
+
|
|
20
|
+
function detectManifests(cwd) {
|
|
21
|
+
const manifests = { found: false, pkg: null, scripts: {}, deps: {}, devDeps: {}, workspaces: null };
|
|
22
|
+
|
|
23
|
+
const manifestFiles = ['package.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'pubspec.yaml'];
|
|
24
|
+
for (const f of manifestFiles) {
|
|
25
|
+
if (fs.existsSync(path.join(cwd, f))) {
|
|
26
|
+
manifests.found = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
32
|
+
if (fs.existsSync(pkgPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
35
|
+
manifests.pkg = pkg;
|
|
36
|
+
manifests.scripts = pkg.scripts || {};
|
|
37
|
+
manifests.deps = pkg.dependencies || {};
|
|
38
|
+
manifests.devDeps = pkg.devDependencies || {};
|
|
39
|
+
manifests.workspaces = pkg.workspaces || null;
|
|
40
|
+
} catch {
|
|
41
|
+
// Malformed package.json
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return manifests;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Stack Detection ──
|
|
49
|
+
|
|
50
|
+
function detectStacks(cwd, manifests) {
|
|
51
|
+
const allDeps = { ...manifests.deps, ...manifests.devDeps };
|
|
52
|
+
const detected = [];
|
|
53
|
+
|
|
54
|
+
for (const [stackId, signals] of Object.entries(STACK_SIGNALS)) {
|
|
55
|
+
let matched = false;
|
|
56
|
+
|
|
57
|
+
if (signals.deps) {
|
|
58
|
+
for (const dep of signals.deps) {
|
|
59
|
+
if (allDeps[dep]) {
|
|
60
|
+
matched = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!matched && signals.files) {
|
|
67
|
+
for (const f of signals.files) {
|
|
68
|
+
if (fs.existsSync(path.join(cwd, f))) {
|
|
69
|
+
matched = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (matched) detected.push(stackId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return detected;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Git Info Detection ──
|
|
82
|
+
|
|
83
|
+
// All git calls use execFileSync (argv form, no shell) with any line-counting
|
|
84
|
+
// done in JS — the previous `| head -1` / `| wc -l` pipes silently failed under
|
|
85
|
+
// Windows cmd.exe, zeroing the stats and skewing maturity classification.
|
|
86
|
+
function git(args, cwd) {
|
|
87
|
+
return execFileSync('git', args, { stdio: 'pipe', timeout: 5000, cwd }).toString().trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectGitInfo(cwd) {
|
|
91
|
+
const defaults = { isRepo: false, commitCount: 0, ageInDays: 0, contributorCount: 0 };
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
git(['rev-parse', '--is-inside-work-tree'], cwd);
|
|
95
|
+
} catch {
|
|
96
|
+
return defaults;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const info = { isRepo: true, commitCount: 0, ageInDays: 0, contributorCount: 0 };
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
info.commitCount = parseInt(git(['rev-list', '--count', 'HEAD'], cwd), 10) || 0;
|
|
103
|
+
} catch { /* empty repo or no commits */ }
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Root commit(s), then their timestamp — cheap even in huge repos.
|
|
107
|
+
const rootSha = git(['rev-list', '--max-parents=0', 'HEAD'], cwd).split('\n')[0];
|
|
108
|
+
if (rootSha) {
|
|
109
|
+
const firstTs = git(['show', '-s', '--format=%ct', rootSha], cwd);
|
|
110
|
+
const ageMs = Date.now() - parseInt(firstTs, 10) * 1000;
|
|
111
|
+
info.ageInDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
112
|
+
}
|
|
113
|
+
} catch { /* no commits */ }
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const authors = git(['shortlog', '-sn', '--no-merges', 'HEAD'], cwd);
|
|
117
|
+
info.contributorCount = authors ? authors.split('\n').length : 0;
|
|
118
|
+
} catch { /* no commits */ }
|
|
119
|
+
|
|
120
|
+
return info;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Tooling Detection ──
|
|
124
|
+
|
|
125
|
+
function detectTooling(cwd, pkg) {
|
|
126
|
+
const tooling = {
|
|
127
|
+
linter: null,
|
|
128
|
+
formatter: null,
|
|
129
|
+
testFramework: null,
|
|
130
|
+
hasTestDir: false,
|
|
131
|
+
ci: null,
|
|
132
|
+
packageManager: null,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const exists = (f) => fs.existsSync(path.join(cwd, f));
|
|
136
|
+
|
|
137
|
+
const linterChecks = [
|
|
138
|
+
{ type: 'eslint', files: ['.eslintrc', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yml', 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', 'eslint.config.ts'] },
|
|
139
|
+
{ type: 'biome', files: ['biome.json', 'biome.jsonc'] },
|
|
140
|
+
{ type: 'ruff', files: ['ruff.toml', '.ruff.toml'] },
|
|
141
|
+
{ type: 'golangci-lint', files: ['.golangci.yml', '.golangci.yaml'] },
|
|
142
|
+
];
|
|
143
|
+
for (const check of linterChecks) {
|
|
144
|
+
for (const f of check.files) {
|
|
145
|
+
if (exists(f)) {
|
|
146
|
+
tooling.linter = { type: check.type, configFile: f };
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (tooling.linter) break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const formatterChecks = [
|
|
154
|
+
{ type: 'prettier', files: ['.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json', '.prettierrc.yml', 'prettier.config.js', 'prettier.config.mjs', 'prettier.config.cjs'] },
|
|
155
|
+
{ type: 'biome', files: ['biome.json', 'biome.jsonc'] },
|
|
156
|
+
{ type: 'rustfmt', files: ['rustfmt.toml', '.rustfmt.toml'] },
|
|
157
|
+
];
|
|
158
|
+
for (const check of formatterChecks) {
|
|
159
|
+
for (const f of check.files) {
|
|
160
|
+
if (exists(f)) {
|
|
161
|
+
tooling.formatter = { type: check.type, configFile: f };
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (tooling.formatter) break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const allDeps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
169
|
+
if (allDeps['vitest']) {
|
|
170
|
+
tooling.testFramework = { type: 'vitest' };
|
|
171
|
+
} else if (allDeps['jest']) {
|
|
172
|
+
tooling.testFramework = { type: 'jest' };
|
|
173
|
+
} else if (allDeps['mocha']) {
|
|
174
|
+
tooling.testFramework = { type: 'mocha' };
|
|
175
|
+
} else if (exists('pytest.ini') || exists('pyproject.toml')) {
|
|
176
|
+
// Check pyproject.toml for pytest config
|
|
177
|
+
if (exists('pyproject.toml')) {
|
|
178
|
+
try {
|
|
179
|
+
const content = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf-8');
|
|
180
|
+
if (content.includes('[tool.pytest') || content.includes('pytest')) {
|
|
181
|
+
tooling.testFramework = { type: 'pytest' };
|
|
182
|
+
}
|
|
183
|
+
} catch { /* ignore */ }
|
|
184
|
+
}
|
|
185
|
+
if (!tooling.testFramework && exists('pytest.ini')) {
|
|
186
|
+
tooling.testFramework = { type: 'pytest' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const testDirs = ['test', 'tests', '__tests__', 'spec'];
|
|
191
|
+
tooling.hasTestDir = testDirs.some((d) => exists(d));
|
|
192
|
+
|
|
193
|
+
if (exists('.github/workflows')) {
|
|
194
|
+
tooling.ci = { type: 'github-actions', path: '.github/workflows/' };
|
|
195
|
+
} else if (exists('.gitlab-ci.yml')) {
|
|
196
|
+
tooling.ci = { type: 'gitlab-ci', path: '.gitlab-ci.yml' };
|
|
197
|
+
} else if (exists('.circleci')) {
|
|
198
|
+
tooling.ci = { type: 'circleci', path: '.circleci/' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (exists('pnpm-lock.yaml')) {
|
|
202
|
+
tooling.packageManager = 'pnpm';
|
|
203
|
+
} else if (exists('yarn.lock')) {
|
|
204
|
+
tooling.packageManager = 'yarn';
|
|
205
|
+
} else if (exists('bun.lockb') || exists('bun.lock')) {
|
|
206
|
+
tooling.packageManager = 'bun';
|
|
207
|
+
} else if (exists('package-lock.json')) {
|
|
208
|
+
tooling.packageManager = 'npm';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return tooling;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Command Extraction ──
|
|
215
|
+
|
|
216
|
+
function extractCommands(pkg, packageManager) {
|
|
217
|
+
const commands = {};
|
|
218
|
+
if (!pkg?.scripts) return commands;
|
|
219
|
+
|
|
220
|
+
const pm = packageManager || 'npm';
|
|
221
|
+
const runPrefix = pm === 'npm' ? 'npm run' : pm;
|
|
222
|
+
|
|
223
|
+
const knownScripts = {
|
|
224
|
+
dev: ['dev', 'start:dev', 'serve'],
|
|
225
|
+
test: ['test', 'test:unit', 'test:all'],
|
|
226
|
+
build: ['build'],
|
|
227
|
+
lint: ['lint', 'lint:fix'],
|
|
228
|
+
format: ['format', 'fmt'],
|
|
229
|
+
typecheck: ['typecheck', 'type-check', 'types:check', 'check-types'],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (const [key, scriptNames] of Object.entries(knownScripts)) {
|
|
233
|
+
for (const name of scriptNames) {
|
|
234
|
+
if (pkg.scripts[name]) {
|
|
235
|
+
// Use shorthand for common npm scripts
|
|
236
|
+
if (pm === 'npm' && (name === 'test' || name === 'start')) {
|
|
237
|
+
commands[key] = `npm ${name}`;
|
|
238
|
+
} else {
|
|
239
|
+
commands[key] = `${runPrefix} ${name}`;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return commands;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Maturity Classification ──
|
|
250
|
+
|
|
251
|
+
function classifyMaturity(exists, git) {
|
|
252
|
+
if (!exists || git.commitCount < 5) return 'greenfield';
|
|
253
|
+
if (git.commitCount <= 50 && git.ageInDays < 90) return 'early';
|
|
254
|
+
if (git.commitCount > 500 || git.ageInDays > 365 || git.contributorCount > 5) return 'mature';
|
|
255
|
+
return 'established';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Monorepo Detection ──
|
|
259
|
+
|
|
260
|
+
function detectMonorepo(cwd, manifests) {
|
|
261
|
+
const result = { detected: false, type: null };
|
|
262
|
+
|
|
263
|
+
if (manifests.workspaces) {
|
|
264
|
+
result.detected = true;
|
|
265
|
+
result.type = 'npm-workspaces';
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml'))) {
|
|
270
|
+
result.detected = true;
|
|
271
|
+
result.type = 'pnpm-workspaces';
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (fs.existsSync(path.join(cwd, 'lerna.json'))) {
|
|
276
|
+
result.detected = true;
|
|
277
|
+
result.type = 'lerna';
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (fs.existsSync(path.join(cwd, 'nx.json'))) {
|
|
282
|
+
result.detected = true;
|
|
283
|
+
result.type = 'nx';
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (fs.existsSync(path.join(cwd, 'turbo.json'))) {
|
|
288
|
+
result.detected = true;
|
|
289
|
+
result.type = 'turborepo';
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Main Export ──
|
|
297
|
+
|
|
298
|
+
export function detectProject(cwd) {
|
|
299
|
+
const manifests = detectManifests(cwd);
|
|
300
|
+
const git = detectGitInfo(cwd);
|
|
301
|
+
const detectedStacks = detectStacks(cwd, manifests);
|
|
302
|
+
const tooling = detectTooling(cwd, manifests.pkg);
|
|
303
|
+
const commands = extractCommands(manifests.pkg, tooling.packageManager);
|
|
304
|
+
const monorepo = detectMonorepo(cwd, manifests);
|
|
305
|
+
const maturity = classifyMaturity(manifests.found, git);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
exists: manifests.found,
|
|
309
|
+
maturity,
|
|
310
|
+
detectedStacks,
|
|
311
|
+
git,
|
|
312
|
+
tooling,
|
|
313
|
+
monorepo,
|
|
314
|
+
commands,
|
|
315
|
+
};
|
|
316
|
+
}
|