@bachbntdev/devcmd 0.0.0-stage → 2.7.1
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 +15 -0
- package/README.md +849 -2
- package/dev +33 -0
- package/package.json +47 -4
- package/src/cli.js +217 -0
- package/src/commands/completion.js +97 -0
- package/src/commands/devices.js +26 -0
- package/src/commands/docker.js +35 -0
- package/src/commands/env.js +52 -0
- package/src/commands/frameworks.js +36 -0
- package/src/commands/history.js +59 -0
- package/src/commands/inspect.js +48 -0
- package/src/commands/lifecycle.js +70 -0
- package/src/config/index.js +35 -0
- package/src/config/user.js +101 -0
- package/src/constants.js +23 -0
- package/src/devices/android.js +89 -0
- package/src/devices/index.js +14 -0
- package/src/devices/ios.js +47 -0
- package/src/generators/clean-project.js +52 -0
- package/src/generators/python-project.js +175 -0
- package/src/generators/reset-project.js +16 -0
- package/src/handlers/clone.js +73 -0
- package/src/handlers/devices.js +78 -0
- package/src/handlers/history.js +75 -0
- package/src/handlers/json.js +68 -0
- package/src/handlers/lifecycle.js +119 -0
- package/src/handlers/mcp.js +10 -0
- package/src/handlers/scaffold.js +166 -0
- package/src/handlers/system.js +135 -0
- package/src/handlers/tools.js +66 -0
- package/src/handlers/workflow.js +121 -0
- package/src/mcp/processes.js +164 -0
- package/src/mcp/server.js +32 -0
- package/src/mcp/tools.js +389 -0
- package/src/openers/built-ins.json +80 -0
- package/src/openers/engine.js +51 -0
- package/src/openers/index.js +26 -0
- package/src/openers/loader.js +89 -0
- package/src/openers/schema.js +81 -0
- package/src/projects/adapters/android.js +24 -0
- package/src/projects/adapters/common.js +24 -0
- package/src/projects/adapters/flutter.js +29 -0
- package/src/projects/adapters/index.js +30 -0
- package/src/projects/adapters/ios.js +35 -0
- package/src/projects/adapters/node.js +112 -0
- package/src/projects/adapters/python.js +90 -0
- package/src/projects/adapters/recipe.js +13 -0
- package/src/projects/detect.js +187 -0
- package/src/projects/profile.js +175 -0
- package/src/projects/recent.js +50 -0
- package/src/projects/workspaces.js +96 -0
- package/src/recipes/action-runner.js +24 -0
- package/src/recipes/built-ins.json +417 -0
- package/src/recipes/engine.js +159 -0
- package/src/recipes/index.js +34 -0
- package/src/recipes/loader.js +82 -0
- package/src/recipes/schema.js +225 -0
- package/src/runner/captured.js +125 -0
- package/src/runner/check.js +93 -0
- package/src/runner/command.js +43 -0
- package/src/runner/parallel.js +110 -0
- package/src/runner/process.js +80 -0
- package/src/runtime/execute.js +99 -0
- package/src/utils/args.js +219 -0
- package/src/utils/interpolate.js +10 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
6
|
+
const PLATFORMS = new Set(['darwin', 'linux', 'win32']);
|
|
7
|
+
const TARGETS = new Set(['project', 'android', 'xcode']);
|
|
8
|
+
const FORBIDDEN_NAMES = new Set(['recent', 'list', 'validate']);
|
|
9
|
+
const FORBIDDEN_SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'cmd', 'cmd.exe', 'powershell', 'pwsh']);
|
|
10
|
+
|
|
11
|
+
function assertObject(value, label) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
13
|
+
throw new Error(`${label} must be an object.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateName(value, label) {
|
|
18
|
+
if (!NAME_PATTERN.test(value || '') || FORBIDDEN_NAMES.has(value)) {
|
|
19
|
+
throw new Error(`${label} must use lowercase letters, numbers, and underscores and must not be reserved.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateCommand(command, label) {
|
|
24
|
+
assertObject(command, label);
|
|
25
|
+
if (Object.keys(command).some((key) => !['executable', 'args', 'cwd'].includes(key))) {
|
|
26
|
+
throw new Error(`${label} contains unsupported fields.`);
|
|
27
|
+
}
|
|
28
|
+
if (typeof command.executable !== 'string' || !command.executable.trim()) {
|
|
29
|
+
throw new Error(`${label}.executable must be a non-empty string.`);
|
|
30
|
+
}
|
|
31
|
+
if (command.executable.includes('{')) {
|
|
32
|
+
throw new Error(`${label}.executable cannot contain placeholders.`);
|
|
33
|
+
}
|
|
34
|
+
if (FORBIDDEN_SHELLS.has(path.basename(command.executable).toLowerCase())) {
|
|
35
|
+
throw new Error(`${label} cannot use a shell executable.`);
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== 'string')) {
|
|
38
|
+
throw new Error(`${label}.args must be an array of strings.`);
|
|
39
|
+
}
|
|
40
|
+
if (command.cwd !== undefined && typeof command.cwd !== 'string') {
|
|
41
|
+
throw new Error(`${label}.cwd must be a string.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateOpener(opener) {
|
|
46
|
+
assertObject(opener, 'Opener');
|
|
47
|
+
const allowed = ['name', 'aliases', 'description', 'target', 'platforms'];
|
|
48
|
+
const unknown = Object.keys(opener).filter((key) => !allowed.includes(key));
|
|
49
|
+
if (unknown.length > 0) throw new Error(`Opener contains unsupported fields: ${unknown.join(', ')}`);
|
|
50
|
+
|
|
51
|
+
validateName(opener.name, 'Opener name');
|
|
52
|
+
if (opener.aliases !== undefined) {
|
|
53
|
+
if (!Array.isArray(opener.aliases) || opener.aliases.length === 0) {
|
|
54
|
+
throw new Error(`Opener ${opener.name} aliases must be a non-empty array.`);
|
|
55
|
+
}
|
|
56
|
+
const aliases = new Set();
|
|
57
|
+
for (const alias of opener.aliases) {
|
|
58
|
+
validateName(alias, `Opener ${opener.name} alias`);
|
|
59
|
+
if (alias === opener.name || aliases.has(alias)) {
|
|
60
|
+
throw new Error(`Opener ${opener.name} contains a duplicate alias: ${alias}`);
|
|
61
|
+
}
|
|
62
|
+
aliases.add(alias);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (typeof opener.description !== 'string' || !opener.description.trim()) {
|
|
66
|
+
throw new Error(`Opener ${opener.name} requires a description.`);
|
|
67
|
+
}
|
|
68
|
+
if (!TARGETS.has(opener.target || 'project')) {
|
|
69
|
+
throw new Error(`Opener ${opener.name} target must be one of: ${[...TARGETS].join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
assertObject(opener.platforms, `Opener ${opener.name} platforms`);
|
|
72
|
+
const entries = Object.entries(opener.platforms);
|
|
73
|
+
if (entries.length === 0) throw new Error(`Opener ${opener.name} requires at least one platform.`);
|
|
74
|
+
for (const [platform, command] of entries) {
|
|
75
|
+
if (!PLATFORMS.has(platform)) throw new Error(`Opener ${opener.name} has unsupported platform: ${platform}`);
|
|
76
|
+
validateCommand(command, `Opener ${opener.name} platform ${platform}`);
|
|
77
|
+
}
|
|
78
|
+
return opener;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { validateOpener };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { createCommand } = require('../../runner/command');
|
|
4
|
+
const { unsupported } = require('./common');
|
|
5
|
+
|
|
6
|
+
function gradle(project, tasks) {
|
|
7
|
+
const executable = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
|
8
|
+
return createCommand(executable, tasks, { cwd: project.root });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function build(action, project) {
|
|
12
|
+
if (action === 'install' || action === 'run') return [gradle(project, ['installDebug'])];
|
|
13
|
+
if (action === 'test') return [gradle(project, ['test'])];
|
|
14
|
+
if (action === 'build') return [gradle(project, ['assembleDebug'])];
|
|
15
|
+
if (action === 'clean') return [gradle(project, ['clean'])];
|
|
16
|
+
if (action === 'check') return [gradle(project, ['lint', 'test'])];
|
|
17
|
+
return unsupported(action, project);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
build,
|
|
22
|
+
id: 'android',
|
|
23
|
+
matches: (project) => project.type === 'android',
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createCommand } = require('../../runner/command');
|
|
5
|
+
|
|
6
|
+
function internalCleaner(project) {
|
|
7
|
+
const cleaner = path.join(__dirname, '..', '..', 'generators', 'clean-project.js');
|
|
8
|
+
return [createCommand(process.execPath, [cleaner, project.root, project.type])];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function internalResetter(project) {
|
|
12
|
+
const resetter = path.join(__dirname, '..', '..', 'generators', 'reset-project.js');
|
|
13
|
+
return [createCommand(process.execPath, [resetter, project.root, project.type])];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function unsupported(action, project) {
|
|
17
|
+
throw new Error(`dev ${action} is not supported for detected project type: ${project.type}.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
internalCleaner,
|
|
22
|
+
internalResetter,
|
|
23
|
+
unsupported,
|
|
24
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { createCommand } = require('../../runner/command');
|
|
4
|
+
const { unsupported } = require('./common');
|
|
5
|
+
|
|
6
|
+
function command(project, args) {
|
|
7
|
+
return createCommand('flutter', args, { cwd: project.root });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function build(action, project) {
|
|
11
|
+
if (action === 'install') return [command(project, ['pub', 'get'])];
|
|
12
|
+
if (action === 'run') return [command(project, ['run'])];
|
|
13
|
+
if (action === 'test') return [command(project, ['test'])];
|
|
14
|
+
if (action === 'build') return [command(project, ['build', 'apk'])];
|
|
15
|
+
if (action === 'reset') return [command(project, ['clean']), command(project, ['pub', 'get'])];
|
|
16
|
+
if (action === 'clean') return [command(project, ['clean'])];
|
|
17
|
+
if (action === 'lint') return [command(project, ['analyze'])];
|
|
18
|
+
if (action === 'format') return [createCommand('dart', ['format', '.'], { cwd: project.root })];
|
|
19
|
+
if (action === 'outdated') return [command(project, ['pub', 'outdated'])];
|
|
20
|
+
if (action === 'upgrade') return [command(project, ['pub', 'upgrade'])];
|
|
21
|
+
if (action === 'check') return [command(project, ['analyze']), command(project, ['test'])];
|
|
22
|
+
return unsupported(action, project);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
build,
|
|
27
|
+
id: 'flutter',
|
|
28
|
+
matches: (project) => project.type === 'flutter',
|
|
29
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const node = require('./node');
|
|
4
|
+
const python = require('./python');
|
|
5
|
+
const flutter = require('./flutter');
|
|
6
|
+
const android = require('./android');
|
|
7
|
+
const ios = require('./ios');
|
|
8
|
+
const recipe = require('./recipe');
|
|
9
|
+
|
|
10
|
+
const adapters = [node, python, flutter, android, ios];
|
|
11
|
+
|
|
12
|
+
function getProjectAdapter(project) {
|
|
13
|
+
if (project.recipe?.lifecycle?.adapter) {
|
|
14
|
+
const configured = adapters.find((candidate) => candidate.id === project.recipe.lifecycle.adapter);
|
|
15
|
+
if (!configured) {
|
|
16
|
+
throw new Error(`Unknown lifecycle adapter: ${project.recipe.lifecycle.adapter}.`);
|
|
17
|
+
}
|
|
18
|
+
return configured;
|
|
19
|
+
}
|
|
20
|
+
if (recipe.matches(project)) return recipe;
|
|
21
|
+
const adapter = adapters.find((candidate) => candidate.matches(project));
|
|
22
|
+
if (!adapter) throw new Error(`No lifecycle adapter is available for project type: ${project.type}.`);
|
|
23
|
+
return adapter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
adapters,
|
|
28
|
+
getProjectAdapter,
|
|
29
|
+
recipe,
|
|
30
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { createCommand } = require('../../runner/command');
|
|
4
|
+
const { unsupported } = require('./common');
|
|
5
|
+
|
|
6
|
+
function baseArgs(project) {
|
|
7
|
+
return project.xcode.kind === 'workspace'
|
|
8
|
+
? ['-workspace', project.xcode.file]
|
|
9
|
+
: ['-project', project.xcode.file];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function xcodebuild(project, action) {
|
|
13
|
+
return createCommand('xcodebuild', [...baseArgs(project), action], { cwd: project.root });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function build(action, project) {
|
|
17
|
+
if (action === 'install') {
|
|
18
|
+
return [createCommand('xcodebuild', [...baseArgs(project), '-resolvePackageDependencies'], { cwd: project.root })];
|
|
19
|
+
}
|
|
20
|
+
if (action === 'reset') {
|
|
21
|
+
return [xcodebuild(project, 'clean'), createCommand('xcodebuild', [...baseArgs(project), '-resolvePackageDependencies'], { cwd: project.root })];
|
|
22
|
+
}
|
|
23
|
+
if (['build', 'test', 'clean'].includes(action)) return [xcodebuild(project, action)];
|
|
24
|
+
if (action === 'check') return [xcodebuild(project, 'analyze'), xcodebuild(project, 'build')];
|
|
25
|
+
if (action === 'run') {
|
|
26
|
+
throw new Error('Automatic iOS run requires a scheme and destination. Use dev open ios for now.');
|
|
27
|
+
}
|
|
28
|
+
return unsupported(action, project);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
build,
|
|
33
|
+
id: 'ios',
|
|
34
|
+
matches: (project) => project.type === 'ios',
|
|
35
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { createCommand } = require('../../runner/command');
|
|
6
|
+
const { resolveWorkspace } = require('../workspaces');
|
|
7
|
+
const { internalCleaner, internalResetter, unsupported } = require('./common');
|
|
8
|
+
|
|
9
|
+
function runScript(project, script, workspace) {
|
|
10
|
+
const manager = project.packageManager;
|
|
11
|
+
if (workspace && manager === 'pnpm') {
|
|
12
|
+
return createCommand(manager, ['--filter', workspace.name, 'run', script], { cwd: project.root });
|
|
13
|
+
}
|
|
14
|
+
if (workspace && manager === 'yarn') {
|
|
15
|
+
return createCommand(manager, ['workspace', workspace.name, 'run', script], { cwd: project.root });
|
|
16
|
+
}
|
|
17
|
+
if (workspace) {
|
|
18
|
+
return createCommand(manager, ['run', script, '--workspace', workspace.name], { cwd: project.root });
|
|
19
|
+
}
|
|
20
|
+
return createCommand(manager, ['run', script], { cwd: project.root });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scriptsOf(project, workspace) {
|
|
24
|
+
return (workspace?.packageJson || project.packageJson)?.scripts || {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findScript(project, names, workspace) {
|
|
28
|
+
const scripts = scriptsOf(project, workspace);
|
|
29
|
+
return names.find((name) => scripts[name]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requireScript(project, names, action, workspace) {
|
|
33
|
+
const script = findScript(project, names, workspace);
|
|
34
|
+
if (!script) {
|
|
35
|
+
const where = workspace ? ` in workspace ${workspace.name}` : '';
|
|
36
|
+
throw new Error(
|
|
37
|
+
`No package.json script is available for dev ${action}${where}. Expected: ${names.join(' or ')}.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return script;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function packageManagerCommand(project, args) {
|
|
44
|
+
return [createCommand(project.packageManager, args, { cwd: project.root })];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function yarnBerry(project) {
|
|
48
|
+
return fs.existsSync(path.join(project.root, '.yarnrc.yml'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scriptCommands(project, script, filter) {
|
|
52
|
+
const workspace = filter ? resolveWorkspace(project, filter) : undefined;
|
|
53
|
+
if (!scriptsOf(project, workspace)[script]) {
|
|
54
|
+
const where = workspace ? ` in workspace ${workspace.name}` : '';
|
|
55
|
+
throw new Error(`Unknown task: ${script}${where}. Run dev task to list available tasks.`);
|
|
56
|
+
}
|
|
57
|
+
return [runScript(project, script, workspace)];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FILTERABLE_ACTIONS = new Set(['run', 'test', 'build', 'check', 'lint', 'format']);
|
|
61
|
+
|
|
62
|
+
function build(action, project, context = {}) {
|
|
63
|
+
if (context.filter && !FILTERABLE_ACTIONS.has(action)) {
|
|
64
|
+
throw new Error(`--filter is not supported by dev ${action}.`);
|
|
65
|
+
}
|
|
66
|
+
const workspace = context.filter ? resolveWorkspace(project, context.filter) : undefined;
|
|
67
|
+
const manager = project.packageManager;
|
|
68
|
+
|
|
69
|
+
if (action === 'install') return packageManagerCommand(project, ['install']);
|
|
70
|
+
if (action === 'run') return [runScript(project, requireScript(project, ['dev', 'start'], action, workspace), workspace)];
|
|
71
|
+
if (action === 'test') return [runScript(project, requireScript(project, ['test'], action, workspace), workspace)];
|
|
72
|
+
if (action === 'build') return [runScript(project, requireScript(project, ['build'], action, workspace), workspace)];
|
|
73
|
+
if (action === 'format') return [runScript(project, requireScript(project, ['format'], action, workspace), workspace)];
|
|
74
|
+
if (action === 'lint') {
|
|
75
|
+
const names = context.fix ? ['lint:fix'] : ['lint'];
|
|
76
|
+
return [runScript(project, requireScript(project, names, action, workspace), workspace)];
|
|
77
|
+
}
|
|
78
|
+
if (action === 'reset') {
|
|
79
|
+
return [...internalResetter(project), createCommand(manager, ['install'], { cwd: project.root })];
|
|
80
|
+
}
|
|
81
|
+
if (action === 'clean') {
|
|
82
|
+
return project.packageJson.scripts?.clean ? [runScript(project, 'clean')] : internalCleaner(project);
|
|
83
|
+
}
|
|
84
|
+
if (action === 'check') {
|
|
85
|
+
const scripts = ['lint', 'typecheck', 'test', 'build'].filter(
|
|
86
|
+
(script) => scriptsOf(project, workspace)[script]
|
|
87
|
+
);
|
|
88
|
+
if (scripts.length === 0) {
|
|
89
|
+
throw new Error('No quality scripts were found. Add one of: lint, typecheck, test, build.');
|
|
90
|
+
}
|
|
91
|
+
return scripts.map((script) => runScript(project, script, workspace));
|
|
92
|
+
}
|
|
93
|
+
if (action === 'outdated') return packageManagerCommand(project, ['outdated']);
|
|
94
|
+
if (action === 'upgrade') {
|
|
95
|
+
return packageManagerCommand(project, [manager === 'yarn' ? 'upgrade' : 'update']);
|
|
96
|
+
}
|
|
97
|
+
if (action === 'audit') {
|
|
98
|
+
return packageManagerCommand(
|
|
99
|
+
project,
|
|
100
|
+
manager === 'yarn' && yarnBerry(project) ? ['npm', 'audit'] : ['audit']
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return unsupported(action, project);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
build,
|
|
108
|
+
findScript,
|
|
109
|
+
id: 'node',
|
|
110
|
+
scriptCommands,
|
|
111
|
+
matches: (project) => Boolean(project.packageJson),
|
|
112
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { createCommand } = require('../../runner/command');
|
|
6
|
+
const { internalCleaner, internalResetter, unsupported } = require('./common');
|
|
7
|
+
|
|
8
|
+
const TYPES = ['python', 'django', 'fastapi', 'flask'];
|
|
9
|
+
|
|
10
|
+
function venvPython(project) {
|
|
11
|
+
const relative = process.platform === 'win32'
|
|
12
|
+
? path.join('.venv', 'Scripts', 'python.exe')
|
|
13
|
+
: path.join('.venv', 'bin', 'python');
|
|
14
|
+
return path.join(project.root, relative);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pyprojectContent(project) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(path.join(project.root, 'pyproject.toml'), 'utf8').toLowerCase();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function build(action, project, context) {
|
|
26
|
+
const config = context.config;
|
|
27
|
+
const venvExecutable = venvPython(project);
|
|
28
|
+
const venvExists = fs.existsSync(venvExecutable);
|
|
29
|
+
const python = venvExists ? venvExecutable : config.python;
|
|
30
|
+
|
|
31
|
+
if (action === 'install') {
|
|
32
|
+
const commands = [];
|
|
33
|
+
if (!venvExists) {
|
|
34
|
+
commands.push(createCommand(config.python, ['-m', 'venv', '.venv'], { cwd: project.root }));
|
|
35
|
+
}
|
|
36
|
+
commands.push(createCommand(venvExecutable, ['-m', 'pip', 'install', '-e', '.[dev]'], { cwd: project.root }));
|
|
37
|
+
return commands;
|
|
38
|
+
}
|
|
39
|
+
if (action === 'run' && project.type === 'django') {
|
|
40
|
+
return [createCommand(python, ['manage.py', 'runserver'], { cwd: project.root })];
|
|
41
|
+
}
|
|
42
|
+
if (action === 'run' && project.type === 'fastapi') {
|
|
43
|
+
return [createCommand(python, ['-m', 'fastapi', 'dev', 'app/main.py'], { cwd: project.root })];
|
|
44
|
+
}
|
|
45
|
+
if (action === 'run' && project.type === 'flask') {
|
|
46
|
+
return [createCommand(python, ['-m', 'flask', '--app', 'app', 'run', '--debug'], { cwd: project.root })];
|
|
47
|
+
}
|
|
48
|
+
if (action === 'test') return [createCommand(python, ['-m', 'pytest'], { cwd: project.root })];
|
|
49
|
+
if (action === 'lint') {
|
|
50
|
+
const args = ['-m', 'ruff', 'check', ...(context.fix ? ['--fix'] : []), '.'];
|
|
51
|
+
return [createCommand(python, args, { cwd: project.root })];
|
|
52
|
+
}
|
|
53
|
+
if (action === 'format') return [createCommand(python, ['-m', 'ruff', 'format', '.'], { cwd: project.root })];
|
|
54
|
+
if (action === 'outdated') {
|
|
55
|
+
return [createCommand(python, ['-m', 'pip', 'list', '--outdated'], { cwd: project.root })];
|
|
56
|
+
}
|
|
57
|
+
if (action === 'upgrade') {
|
|
58
|
+
return [createCommand(python, ['-m', 'pip', 'install', '--upgrade', '-e', '.[dev]'], { cwd: project.root })];
|
|
59
|
+
}
|
|
60
|
+
if (action === 'audit') return [createCommand(python, ['-m', 'pip_audit'], { cwd: project.root })];
|
|
61
|
+
if (action === 'build') return [createCommand(python, ['-m', 'build'], { cwd: project.root })];
|
|
62
|
+
if (action === 'reset') {
|
|
63
|
+
return [
|
|
64
|
+
...internalResetter(project),
|
|
65
|
+
createCommand(config.python, ['-m', 'venv', '.venv'], { cwd: project.root }),
|
|
66
|
+
createCommand(venvPython(project), ['-m', 'pip', 'install', '-e', '.[dev]'], { cwd: project.root }),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
if (action === 'clean') return internalCleaner(project);
|
|
70
|
+
if (action === 'check') {
|
|
71
|
+
const commands = [];
|
|
72
|
+
const pyproject = pyprojectContent(project);
|
|
73
|
+
if (/\bruff\b/.test(pyproject)) {
|
|
74
|
+
commands.push(createCommand(python, ['-m', 'ruff', 'check', '.'], { cwd: project.root }));
|
|
75
|
+
}
|
|
76
|
+
commands.push(
|
|
77
|
+
createCommand(python, ['-m', 'pytest'], { cwd: project.root }),
|
|
78
|
+
createCommand(python, ['-m', 'build'], { cwd: project.root })
|
|
79
|
+
);
|
|
80
|
+
return commands;
|
|
81
|
+
}
|
|
82
|
+
return unsupported(action, project);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
build,
|
|
87
|
+
id: 'python',
|
|
88
|
+
matches: (project) => TYPES.includes(project.type),
|
|
89
|
+
venvPython,
|
|
90
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { buildRecipeLifecycleCommands } = require('../../recipes');
|
|
4
|
+
|
|
5
|
+
function build(action, project, context) {
|
|
6
|
+
return buildRecipeLifecycleCommands(project.recipe, action, project, context);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
build,
|
|
11
|
+
id: 'recipe',
|
|
12
|
+
matches: (project) => Boolean(project.recipe?.lifecycle && !project.recipe.lifecycle.adapter),
|
|
13
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { builtInRegistry } = require('../recipes');
|
|
6
|
+
const { loadProfile } = require('./profile');
|
|
7
|
+
const { findWorkspaces } = require('./workspaces');
|
|
8
|
+
|
|
9
|
+
function exists(root, name, fsImpl = fs) {
|
|
10
|
+
return fsImpl.existsSync(path.join(root, name));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readJson(file, fsImpl = fs) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`Invalid JSON in ${file}: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectPackageManager(root, packageJson = {}, fsImpl = fs) {
|
|
22
|
+
if (exists(root, 'pnpm-lock.yaml', fsImpl)) return 'pnpm';
|
|
23
|
+
if (exists(root, 'yarn.lock', fsImpl)) return 'yarn';
|
|
24
|
+
if (exists(root, 'package-lock.json', fsImpl)) return 'npm';
|
|
25
|
+
const declared = packageJson.packageManager?.split('@')[0];
|
|
26
|
+
return ['npm', 'pnpm', 'yarn'].includes(declared) ? declared : 'npm';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectNodeType(packageJson) {
|
|
30
|
+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
31
|
+
if (dependencies.next) return 'next';
|
|
32
|
+
if (dependencies.nuxt) return 'nuxt';
|
|
33
|
+
if (dependencies['@nestjs/core']) return 'nest';
|
|
34
|
+
if (dependencies.expo) return 'react_native';
|
|
35
|
+
if (dependencies['react-native']) return 'react_native_cli';
|
|
36
|
+
if (dependencies.vue) return 'vue';
|
|
37
|
+
if (dependencies.react) return 'react';
|
|
38
|
+
if (dependencies.express) return 'express';
|
|
39
|
+
return 'node';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readText(root, file, fsImpl = fs) {
|
|
43
|
+
try {
|
|
44
|
+
return fsImpl.readFileSync(path.join(root, file), 'utf8').toLowerCase();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function recipeRuleMatches(root, rule, packageJson, fsImpl = fs) {
|
|
51
|
+
if (rule.files && !rule.files.every((file) => exists(root, file, fsImpl))) return false;
|
|
52
|
+
if (rule.dependencies) {
|
|
53
|
+
if (!packageJson) return false;
|
|
54
|
+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
55
|
+
if (!rule.dependencies.every((name) => dependencies[name])) return false;
|
|
56
|
+
}
|
|
57
|
+
if (rule.contains && !Object.entries(rule.contains).every(([file, text]) => {
|
|
58
|
+
const content = readText(root, file, fsImpl);
|
|
59
|
+
return content !== undefined && content.includes(text.toLowerCase());
|
|
60
|
+
})) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectRecipeAtRoot(root, registry, packageJson, fsImpl = fs) {
|
|
65
|
+
const candidates = [...registry.values()]
|
|
66
|
+
.filter((recipe) => recipe.detect)
|
|
67
|
+
.sort((left, right) => (right.detect.priority || 0) - (left.detect.priority || 0));
|
|
68
|
+
return candidates.find((recipe) =>
|
|
69
|
+
recipe.detect.rules.some((rule) => recipeRuleMatches(root, rule, packageJson, fsImpl))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readPyproject(root, fsImpl = fs) {
|
|
74
|
+
try {
|
|
75
|
+
return fsImpl.readFileSync(path.join(root, 'pyproject.toml'), 'utf8').toLowerCase();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findXcodeContainer(root, fsImpl = fs) {
|
|
82
|
+
try {
|
|
83
|
+
const entries = fsImpl.readdirSync(root);
|
|
84
|
+
const workspace = entries.find((entry) => entry.endsWith('.xcworkspace'));
|
|
85
|
+
if (workspace) return { kind: 'workspace', file: workspace };
|
|
86
|
+
const project = entries.find((entry) => entry.endsWith('.xcodeproj'));
|
|
87
|
+
if (project) return { kind: 'project', file: project };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectBase(root, fsImpl = fs, registry = builtInRegistry) {
|
|
95
|
+
const packageJson = exists(root, 'package.json', fsImpl)
|
|
96
|
+
? readJson(path.join(root, 'package.json'), fsImpl)
|
|
97
|
+
: undefined;
|
|
98
|
+
const recipe = detectRecipeAtRoot(root, registry, packageJson, fsImpl);
|
|
99
|
+
if (recipe) {
|
|
100
|
+
return {
|
|
101
|
+
type: recipe.name,
|
|
102
|
+
name: packageJson?.name || path.basename(root),
|
|
103
|
+
root,
|
|
104
|
+
packageJson,
|
|
105
|
+
workspaces: packageJson ? findWorkspaces(root, packageJson, fsImpl) : [],
|
|
106
|
+
packageManager: packageJson ? detectPackageManager(root, packageJson, fsImpl) : undefined,
|
|
107
|
+
recipe,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (packageJson) {
|
|
112
|
+
const type = detectNodeType(packageJson);
|
|
113
|
+
return {
|
|
114
|
+
type,
|
|
115
|
+
name: packageJson.name || path.basename(root),
|
|
116
|
+
root,
|
|
117
|
+
packageJson,
|
|
118
|
+
workspaces: findWorkspaces(root, packageJson, fsImpl),
|
|
119
|
+
packageManager: detectPackageManager(root, packageJson, fsImpl),
|
|
120
|
+
recipe: registry.get(type),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (exists(root, 'pubspec.yaml', fsImpl)) {
|
|
125
|
+
return { type: 'flutter', name: path.basename(root), root, recipe: registry.get('flutter') };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (exists(root, 'gradlew', fsImpl) || exists(root, 'gradlew.bat', fsImpl)) {
|
|
129
|
+
return { type: 'android', name: path.basename(root), root };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const xcode = findXcodeContainer(root, fsImpl);
|
|
133
|
+
if (xcode) return { type: 'ios', name: path.basename(root), root, xcode };
|
|
134
|
+
|
|
135
|
+
if (exists(root, 'manage.py', fsImpl)) {
|
|
136
|
+
return { type: 'django', name: path.basename(root), root, recipe: registry.get('django') };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (exists(root, 'pyproject.toml', fsImpl)) {
|
|
140
|
+
const pyproject = readPyproject(root, fsImpl);
|
|
141
|
+
let type = 'python';
|
|
142
|
+
if (/\bfastapi(?:\[|[<>=\s"])/.test(pyproject)) type = 'fastapi';
|
|
143
|
+
else if (/\bflask(?:[<>=\s"])/.test(pyproject)) type = 'flask';
|
|
144
|
+
else if (/\bdjango(?:[<>=\s"])/.test(pyproject)) type = 'django';
|
|
145
|
+
return { type, name: path.basename(root), root, recipe: registry.get(type) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function detectAtRoot(root, fsImpl = fs, registry = builtInRegistry) {
|
|
152
|
+
const project = detectBase(root, fsImpl, registry);
|
|
153
|
+
const profile = loadProfile(root, fsImpl);
|
|
154
|
+
if (!profile) return project;
|
|
155
|
+
if (project) return { ...project, profile };
|
|
156
|
+
return { type: 'custom', name: profile.name || path.basename(root), root, profile };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectProject(start = process.cwd(), options = {}) {
|
|
160
|
+
const fsImpl = options.fsImpl || fs;
|
|
161
|
+
const registry = options.registry || builtInRegistry;
|
|
162
|
+
let current = path.resolve(start);
|
|
163
|
+
try {
|
|
164
|
+
if (!fsImpl.statSync(current).isDirectory()) current = path.dirname(current);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`Project path does not exist: ${current}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
while (true) {
|
|
170
|
+
const project = detectAtRoot(current, fsImpl, registry);
|
|
171
|
+
if (project) return project;
|
|
172
|
+
const parent = path.dirname(current);
|
|
173
|
+
if (parent === current) break;
|
|
174
|
+
current = parent;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`No supported project was detected from: ${path.resolve(start)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
detectAtRoot,
|
|
181
|
+
detectRecipeAtRoot,
|
|
182
|
+
detectNodeType,
|
|
183
|
+
detectPackageManager,
|
|
184
|
+
detectProject,
|
|
185
|
+
findXcodeContainer,
|
|
186
|
+
recipeRuleMatches,
|
|
187
|
+
};
|