@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,70 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { getProjectAdapter } = require('../projects/adapters');
|
|
4
|
+
const { scriptCommands } = require('../projects/adapters/node');
|
|
5
|
+
const { buildProfileLifecycle, buildProfileTask } = require('../projects/profile');
|
|
6
|
+
const { createCommand } = require('../runner/command');
|
|
7
|
+
const { buildRecipeLifecycleCommands } = require('../recipes');
|
|
8
|
+
const { buildOpenerCommands, builtInOpenerRegistry, resolveOpener } = require('../openers');
|
|
9
|
+
const { unsupported } = require('../projects/adapters/common');
|
|
10
|
+
|
|
11
|
+
function buildLifecycleCommands(action, project, options = {}) {
|
|
12
|
+
const custom = buildProfileLifecycle(project, action, options);
|
|
13
|
+
if (custom) return custom;
|
|
14
|
+
if (project.type === 'custom') return unsupported(action, project);
|
|
15
|
+
const adapter = getProjectAdapter(project);
|
|
16
|
+
if (options.filter && adapter.id !== 'node') {
|
|
17
|
+
throw new Error('--filter requires a Node.js workspace project.');
|
|
18
|
+
}
|
|
19
|
+
if (options.fix && action !== 'lint') {
|
|
20
|
+
throw new Error('--fix is only supported by the lint command.');
|
|
21
|
+
}
|
|
22
|
+
return adapter.build(action, project, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listTasks(project) {
|
|
26
|
+
const tasks = new Map();
|
|
27
|
+
for (const [name, definition] of Object.entries(project.profile?.tasks || {})) {
|
|
28
|
+
tasks.set(name, { name, source: 'profile', description: definition.description });
|
|
29
|
+
}
|
|
30
|
+
for (const name of Object.keys(project.packageJson?.scripts || {})) {
|
|
31
|
+
if (!tasks.has(name)) tasks.set(name, { name, source: 'package.json' });
|
|
32
|
+
}
|
|
33
|
+
return [...tasks.values()];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildTaskCommands(project, name, options = {}) {
|
|
37
|
+
const custom = buildProfileTask(project, name, options);
|
|
38
|
+
if (custom) return custom;
|
|
39
|
+
if (project.packageJson && getProjectAdapter(project).id === 'node') {
|
|
40
|
+
return scriptCommands(project, name, options.filter);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Unknown task: ${name}. Run dev task to list available tasks.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildOpenCommands(
|
|
46
|
+
project,
|
|
47
|
+
openerName,
|
|
48
|
+
config,
|
|
49
|
+
openerRegistry = builtInOpenerRegistry,
|
|
50
|
+
options = {}
|
|
51
|
+
) {
|
|
52
|
+
if (!openerName && options.editor) return [createCommand(options.editor, [project.root])];
|
|
53
|
+
if (!openerName && !config.opener && config.editor) {
|
|
54
|
+
return [createCommand(config.editor, [project.root])];
|
|
55
|
+
}
|
|
56
|
+
if (!openerName && project.recipe?.lifecycle?.open && !project.recipe.lifecycle.adapter) {
|
|
57
|
+
return buildRecipeLifecycleCommands(project.recipe, 'open', project, { config });
|
|
58
|
+
}
|
|
59
|
+
const requested = openerName || config.opener || 'vscode';
|
|
60
|
+
const opener = resolveOpener(openerRegistry, requested);
|
|
61
|
+
if (!opener) throw new Error(`Unknown opener: ${requested}. Run dev open --list.`);
|
|
62
|
+
return buildOpenerCommands(opener, project, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
buildLifecycleCommands,
|
|
67
|
+
buildOpenCommands,
|
|
68
|
+
buildTaskCommands,
|
|
69
|
+
listTasks,
|
|
70
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { version } = require('../../package.json');
|
|
6
|
+
const { builtInRegistry, getRecipeDefinitions } = require('../recipes');
|
|
7
|
+
|
|
8
|
+
const BRAND = 'DevCmd';
|
|
9
|
+
const CONFIG_FILE = path.join(os.homedir(), '.devcmd', 'config.json');
|
|
10
|
+
const HISTORY_FILE = path.join(os.homedir(), '.devcmd', 'history.json');
|
|
11
|
+
const OPENERS_DIR = path.join(os.homedir(), '.devcmd', 'openers');
|
|
12
|
+
const PROJECTS_FILE = path.join(os.homedir(), '.devcmd', 'projects.json');
|
|
13
|
+
const RECIPES_DIR = path.join(os.homedir(), '.devcmd', 'recipes');
|
|
14
|
+
const TRUST_FILE = path.join(os.homedir(), '.devcmd', 'trusted.json');
|
|
15
|
+
const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn'];
|
|
16
|
+
const DEFAULT_DEVICES = {
|
|
17
|
+
android: 'Pixel_9_Pro_Fold',
|
|
18
|
+
ios: 'iPhone 17 Pro',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const frameworkDefinitions = getRecipeDefinitions(builtInRegistry);
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
BRAND,
|
|
25
|
+
CONFIG_FILE,
|
|
26
|
+
DEFAULT_DEVICES,
|
|
27
|
+
HISTORY_FILE,
|
|
28
|
+
OPENERS_DIR,
|
|
29
|
+
PACKAGE_MANAGERS,
|
|
30
|
+
PROJECTS_FILE,
|
|
31
|
+
RECIPES_DIR,
|
|
32
|
+
TRUST_FILE,
|
|
33
|
+
VERSION: version,
|
|
34
|
+
frameworkDefinitions,
|
|
35
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { CONFIG_FILE, PACKAGE_MANAGERS } = require('./index');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
packageManager: 'npm',
|
|
9
|
+
initializeGit: true,
|
|
10
|
+
python: process.platform === 'win32' ? 'python' : 'python3',
|
|
11
|
+
opener: 'vscode',
|
|
12
|
+
notify: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const CONFIG_KEYS = [...Object.keys(DEFAULT_CONFIG), 'editor'];
|
|
16
|
+
|
|
17
|
+
function readJson(file, fsImpl = fs) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT') return {};
|
|
22
|
+
throw new Error(`Unable to read config ${file}: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateConfig(config) {
|
|
27
|
+
if (!PACKAGE_MANAGERS.includes(config.packageManager)) {
|
|
28
|
+
throw new Error(`packageManager must be one of: ${PACKAGE_MANAGERS.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof config.initializeGit !== 'boolean') {
|
|
31
|
+
throw new Error('initializeGit must be true or false.');
|
|
32
|
+
}
|
|
33
|
+
if (typeof config.python !== 'string' || !config.python.trim()) {
|
|
34
|
+
throw new Error('python must be a non-empty executable name or path.');
|
|
35
|
+
}
|
|
36
|
+
if (config.opener !== undefined && (typeof config.opener !== 'string' || !config.opener.trim())) {
|
|
37
|
+
throw new Error('opener must be a non-empty opener name.');
|
|
38
|
+
}
|
|
39
|
+
if (config.editor !== undefined && (typeof config.editor !== 'string' || !config.editor.trim())) {
|
|
40
|
+
throw new Error('editor must be a non-empty application or executable name.');
|
|
41
|
+
}
|
|
42
|
+
if (config.notify !== undefined && (!Number.isInteger(config.notify) || config.notify < 0)) {
|
|
43
|
+
throw new Error('notify must be a non-negative integer (seconds).');
|
|
44
|
+
}
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadConfig(options = {}) {
|
|
49
|
+
const file = options.file || CONFIG_FILE;
|
|
50
|
+
const fsImpl = options.fsImpl || fs;
|
|
51
|
+
const saved = readJson(file, fsImpl);
|
|
52
|
+
const config = { ...DEFAULT_CONFIG, ...saved };
|
|
53
|
+
if (saved.editor && saved.opener === undefined) delete config.opener;
|
|
54
|
+
return validateConfig(config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseConfigValue(key, value) {
|
|
58
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
59
|
+
throw new Error(`Unknown config key: ${key}. Available keys: ${CONFIG_KEYS.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (key === 'initializeGit') {
|
|
62
|
+
if (value === 'true') return true;
|
|
63
|
+
if (value === 'false') return false;
|
|
64
|
+
throw new Error('initializeGit must be true or false.');
|
|
65
|
+
}
|
|
66
|
+
if (key === 'notify') {
|
|
67
|
+
const seconds = Number(value);
|
|
68
|
+
if (!Number.isInteger(seconds) || seconds < 0) {
|
|
69
|
+
throw new Error('notify must be a non-negative integer (seconds). Use 0 to disable.');
|
|
70
|
+
}
|
|
71
|
+
return seconds;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function saveConfig(config, options = {}) {
|
|
77
|
+
const file = options.file || CONFIG_FILE;
|
|
78
|
+
const fsImpl = options.fsImpl || fs;
|
|
79
|
+
const validated = validateConfig({ ...config });
|
|
80
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
fsImpl.writeFileSync(file, `${JSON.stringify(validated, null, 2)}\n`);
|
|
82
|
+
return validated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setConfigValue(key, value, options = {}) {
|
|
86
|
+
const config = loadConfig(options);
|
|
87
|
+
if (key === 'editor') delete config.opener;
|
|
88
|
+
if (key === 'opener') delete config.editor;
|
|
89
|
+
config[key] = parseConfigValue(key, value);
|
|
90
|
+
return saveConfig(config, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
CONFIG_KEYS,
|
|
95
|
+
DEFAULT_CONFIG,
|
|
96
|
+
loadConfig,
|
|
97
|
+
parseConfigValue,
|
|
98
|
+
saveConfig,
|
|
99
|
+
setConfigValue,
|
|
100
|
+
validateConfig,
|
|
101
|
+
};
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const LIFECYCLE_COMMANDS = [
|
|
4
|
+
'install', 'run', 'test', 'build', 'check', 'lint', 'format',
|
|
5
|
+
'outdated', 'upgrade', 'audit', 'clean', 'reset', 'open',
|
|
6
|
+
];
|
|
7
|
+
const SYSTEM_COMMANDS = [
|
|
8
|
+
'config', 'doctor', 'projects', 'recipes', 'openers', 'inspect', 'info', 'history', 'again',
|
|
9
|
+
'init', 'update', 'clone', 'mcp', 'completion', 'task', 'parallel', 'env', 'docker', 'trust',
|
|
10
|
+
];
|
|
11
|
+
const DEVICE_COMMANDS = ['devices', 'android', 'ios'];
|
|
12
|
+
const FILTER_COMMANDS = ['run', 'test', 'build', 'check', 'lint', 'format', 'task'];
|
|
13
|
+
const JSON_COMMANDS = [
|
|
14
|
+
'inspect', 'doctor', 'projects', 'history', 'recipes', 'openers', 'devices', 'config',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
DEVICE_COMMANDS,
|
|
19
|
+
FILTER_COMMANDS,
|
|
20
|
+
JSON_COMMANDS,
|
|
21
|
+
LIFECYCLE_COMMANDS,
|
|
22
|
+
SYSTEM_COMMANDS,
|
|
23
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
function executeFile(executable, args, executeFileSync = childProcess.execFileSync) {
|
|
9
|
+
return executeFileSync(executable, args, {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseAdbDevices(output) {
|
|
16
|
+
return output
|
|
17
|
+
.split('\n')
|
|
18
|
+
.slice(1)
|
|
19
|
+
.map((line) => line.trim().split(/\s+/))
|
|
20
|
+
.filter((parts) => parts.length === 2 && parts[1] === 'device')
|
|
21
|
+
.map((parts) => parts[0])
|
|
22
|
+
.filter((serial) => serial.startsWith('emulator-'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseAvdNames(output) {
|
|
26
|
+
return output.split('\n').map((name) => name.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseAndroidRuntime(config) {
|
|
30
|
+
const image = config.match(/^image\.sysdir\.1\s*=\s*(.+)$/m)?.[1]?.trim();
|
|
31
|
+
return image ? image.replace(/^system-images\//, '').replace(/\/$/, '') : 'Android';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getRunningAndroidDevices(options = {}) {
|
|
35
|
+
const executeFileSync = options.executeFileSync || childProcess.execFileSync;
|
|
36
|
+
const running = new Map();
|
|
37
|
+
try {
|
|
38
|
+
const serials = parseAdbDevices(executeFile('adb', ['devices'], executeFileSync));
|
|
39
|
+
for (const serial of serials) {
|
|
40
|
+
try {
|
|
41
|
+
const name = executeFile('adb', ['-s', serial, 'emu', 'avd', 'name'], executeFileSync)
|
|
42
|
+
.split('\n')[0]
|
|
43
|
+
.trim();
|
|
44
|
+
if (name) running.set(name, serial);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// The emulator may disappear while the list is being collected.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return running;
|
|
51
|
+
}
|
|
52
|
+
return running;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getAndroidDevices(options = {}) {
|
|
56
|
+
const executeFileSync = options.executeFileSync || childProcess.execFileSync;
|
|
57
|
+
const readFileSync = options.readFileSync || fs.readFileSync;
|
|
58
|
+
const homeDirectory = options.homeDirectory || os.homedir();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const running = getRunningAndroidDevices({ executeFileSync });
|
|
62
|
+
const names = parseAvdNames(executeFile('emulator', ['-list-avds'], executeFileSync));
|
|
63
|
+
return names.map((name) => {
|
|
64
|
+
let runtime = 'Android';
|
|
65
|
+
try {
|
|
66
|
+
const configPath = path.join(homeDirectory, '.android', 'avd', `${name}.avd`, 'config.ini');
|
|
67
|
+
runtime = parseAndroidRuntime(readFileSync(configPath, 'utf8'));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Keep the generic runtime when the AVD config is unavailable.
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
id: name,
|
|
73
|
+
name,
|
|
74
|
+
runtime,
|
|
75
|
+
status: running.has(name) ? 'Booted' : 'Shutdown',
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
getAndroidDevices,
|
|
85
|
+
getRunningAndroidDevices,
|
|
86
|
+
parseAdbDevices,
|
|
87
|
+
parseAndroidRuntime,
|
|
88
|
+
parseAvdNames,
|
|
89
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { getAndroidDevices } = require('./android');
|
|
4
|
+
const { getIOSDevices } = require('./ios');
|
|
5
|
+
|
|
6
|
+
function formatDevice(device) {
|
|
7
|
+
return `${device.name} | ${device.runtime} | ${device.status}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
formatDevice,
|
|
12
|
+
getAndroidDevices,
|
|
13
|
+
getIOSDevices,
|
|
14
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
|
|
5
|
+
function parseRuntime(runtime) {
|
|
6
|
+
return runtime
|
|
7
|
+
.replace(/^com\.apple\.CoreSimulator\.SimRuntime\./, '')
|
|
8
|
+
.replace(/-/g, ' ')
|
|
9
|
+
.replace(/(?<=\d) (?=\d)/g, '.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseIOSDevices(output) {
|
|
13
|
+
const data = typeof output === 'string' ? JSON.parse(output) : output;
|
|
14
|
+
const result = [];
|
|
15
|
+
for (const [runtime, devices] of Object.entries(data.devices || {})) {
|
|
16
|
+
for (const device of devices) {
|
|
17
|
+
if (device.isAvailable && device.name) {
|
|
18
|
+
result.push({
|
|
19
|
+
id: device.udid,
|
|
20
|
+
name: device.name,
|
|
21
|
+
runtime: parseRuntime(runtime),
|
|
22
|
+
status: device.state || 'Shutdown',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getIOSDevices(options = {}) {
|
|
31
|
+
const executeFileSync = options.executeFileSync || childProcess.execFileSync;
|
|
32
|
+
try {
|
|
33
|
+
const output = executeFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '-j'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
36
|
+
});
|
|
37
|
+
return parseIOSDevices(output);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
getIOSDevices,
|
|
45
|
+
parseIOSDevices,
|
|
46
|
+
parseRuntime,
|
|
47
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 bachbnt
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(process.argv[2] || '.');
|
|
8
|
+
const type = process.argv[3] || 'unknown';
|
|
9
|
+
|
|
10
|
+
const CLEAN_TARGETS = {
|
|
11
|
+
node: ['dist', 'build', 'coverage', '.cache'],
|
|
12
|
+
react: ['dist', 'build', 'coverage', '.cache'],
|
|
13
|
+
vue: ['dist', 'coverage', '.cache'],
|
|
14
|
+
next: ['.next', 'out', 'coverage'],
|
|
15
|
+
nuxt: ['.nuxt', '.output', 'dist', 'coverage'],
|
|
16
|
+
nest: ['dist', 'coverage'],
|
|
17
|
+
express: ['dist', 'coverage'],
|
|
18
|
+
react_native: ['.expo', 'dist', 'coverage'],
|
|
19
|
+
react_native_cli: ['android/app/build', 'ios/build', 'coverage'],
|
|
20
|
+
python: ['build', 'dist', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', '.coverage'],
|
|
21
|
+
django: ['build', 'dist', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', '.coverage'],
|
|
22
|
+
fastapi: ['build', 'dist', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', '.coverage'],
|
|
23
|
+
flask: ['build', 'dist', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', '.coverage'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const PYTHON_CACHE_DIRECTORIES = new Set(['__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache']);
|
|
27
|
+
const PYTHON_SKIP_DIRECTORIES = new Set(['.git', '.venv', '.tox', 'node_modules']);
|
|
28
|
+
|
|
29
|
+
function removePythonCaches(directory) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(directory, entry.name);
|
|
38
|
+
if (entry.isDirectory() && PYTHON_CACHE_DIRECTORIES.has(entry.name)) {
|
|
39
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
40
|
+
} else if (entry.isDirectory() && !PYTHON_SKIP_DIRECTORIES.has(entry.name)) {
|
|
41
|
+
removePythonCaches(fullPath);
|
|
42
|
+
} else if (entry.isFile() && /\.py[co]$/.test(entry.name)) {
|
|
43
|
+
fs.rmSync(fullPath, { force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const target of CLEAN_TARGETS[type] || CLEAN_TARGETS.node) {
|
|
49
|
+
fs.rmSync(path.join(root, target), { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (['python', 'django', 'fastapi', 'flask'].includes(type)) removePythonCaches(root);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 bachbnt
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const framework = process.argv[2];
|
|
8
|
+
const target = process.argv[3];
|
|
9
|
+
const supported = ['django', 'fastapi', 'flask'];
|
|
10
|
+
|
|
11
|
+
function write(relativePath, content) {
|
|
12
|
+
const file = path.join(target, relativePath);
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.writeFileSync(file, content);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pyproject(name, dependencies, options = {}) {
|
|
18
|
+
const devDependencies = ['pytest>=8,<10', 'build>=1.2,<2.0', ...(options.devDependencies || [])];
|
|
19
|
+
const pytestOptions = options.pytestOptions || '';
|
|
20
|
+
return `[build-system]
|
|
21
|
+
requires = ["setuptools>=77"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[project]
|
|
25
|
+
name = "${name}"
|
|
26
|
+
version = "0.1.0"
|
|
27
|
+
description = "${framework} project created by DevCmd"
|
|
28
|
+
readme = "README.md"
|
|
29
|
+
requires-python = ">=3.10"
|
|
30
|
+
dependencies = [
|
|
31
|
+
${dependencies.map((dependency) => ` "${dependency}",`).join('\n')}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
${devDependencies.map((dependency) => ` "${dependency}",`).join('\n')}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
${pytestOptions}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createBase() {
|
|
46
|
+
if (fs.existsSync(target)) {
|
|
47
|
+
throw new Error(`Target already exists: ${target}`);
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(target, { recursive: true });
|
|
50
|
+
write('.gitignore', `.venv/
|
|
51
|
+
__pycache__/
|
|
52
|
+
*.py[cod]
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
.coverage
|
|
55
|
+
.env
|
|
56
|
+
dist/
|
|
57
|
+
build/
|
|
58
|
+
*.egg-info/
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createDjango() {
|
|
63
|
+
write(
|
|
64
|
+
'pyproject.toml',
|
|
65
|
+
pyproject(target, ['Django>=5.2,<7.0'], {
|
|
66
|
+
devDependencies: ['pytest-django>=4.11,<5.0'],
|
|
67
|
+
pytestOptions: 'DJANGO_SETTINGS_MODULE = "config.settings"',
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
write('README.md', `# ${target}
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
source .venv/bin/activate
|
|
76
|
+
python manage.py migrate
|
|
77
|
+
python manage.py runserver
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
Run tests with \`python manage.py test\` or \`pytest\`.
|
|
81
|
+
`);
|
|
82
|
+
write('tests/test_project.py', `from django.conf import settings
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_django_project_is_configured() -> None:
|
|
86
|
+
assert settings.ROOT_URLCONF == "config.urls"
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createFastAPI() {
|
|
91
|
+
write('pyproject.toml', pyproject(target, ['fastapi[standard]>=0.115,<1.0', 'httpx>=0.27,<1.0']));
|
|
92
|
+
write('README.md', `# ${target}
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
\`\`\`bash
|
|
97
|
+
source .venv/bin/activate
|
|
98
|
+
fastapi dev app/main.py
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
Run tests with \`pytest\`.
|
|
102
|
+
`);
|
|
103
|
+
write('app/__init__.py', '');
|
|
104
|
+
write('app/main.py', `from fastapi import FastAPI
|
|
105
|
+
|
|
106
|
+
app = FastAPI(title="${target}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.get("/")
|
|
110
|
+
async def root() -> dict[str, str]:
|
|
111
|
+
return {"message": "Hello from ${target}"}
|
|
112
|
+
`);
|
|
113
|
+
write('tests/test_main.py', `from fastapi.testclient import TestClient
|
|
114
|
+
|
|
115
|
+
from app.main import app
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
client = TestClient(app)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_root() -> None:
|
|
122
|
+
response = client.get("/")
|
|
123
|
+
assert response.status_code == 200
|
|
124
|
+
assert response.json() == {"message": "Hello from ${target}"}
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createFlask() {
|
|
129
|
+
write('pyproject.toml', pyproject(target, ['Flask>=3.1,<4.0']));
|
|
130
|
+
write('README.md', `# ${target}
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
source .venv/bin/activate
|
|
136
|
+
flask --app app run --debug
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
Run tests with \`pytest\`.
|
|
140
|
+
`);
|
|
141
|
+
write('app/__init__.py', `from flask import Flask
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def create_app() -> Flask:
|
|
145
|
+
app = Flask(__name__)
|
|
146
|
+
|
|
147
|
+
@app.get("/")
|
|
148
|
+
def index() -> dict[str, str]:
|
|
149
|
+
return {"message": "Hello from ${target}"}
|
|
150
|
+
|
|
151
|
+
return app
|
|
152
|
+
`);
|
|
153
|
+
write('tests/test_app.py', `from app import create_app
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_index() -> None:
|
|
157
|
+
client = create_app().test_client()
|
|
158
|
+
response = client.get("/")
|
|
159
|
+
assert response.status_code == 200
|
|
160
|
+
assert response.get_json() == {"message": "Hello from ${target}"}
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
if (!supported.includes(framework) || !target) {
|
|
166
|
+
throw new Error(`Usage: python-project.js <${supported.join('|')}> <target>`);
|
|
167
|
+
}
|
|
168
|
+
createBase();
|
|
169
|
+
if (framework === 'django') createDjango();
|
|
170
|
+
if (framework === 'fastapi') createFastAPI();
|
|
171
|
+
if (framework === 'flask') createFlask();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`Unable to create ${framework || 'Python'} project: ${error.message}`);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 bachbnt
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(process.argv[2] || '.');
|
|
8
|
+
const type = process.argv[3] || 'unknown';
|
|
9
|
+
|
|
10
|
+
const PYTHON_TYPES = new Set(['python', 'django', 'fastapi', 'flask']);
|
|
11
|
+
|
|
12
|
+
const target = PYTHON_TYPES.has(type)
|
|
13
|
+
? path.join(root, '.venv')
|
|
14
|
+
: path.join(root, 'node_modules');
|
|
15
|
+
|
|
16
|
+
fs.rmSync(target, { recursive: true, force: true });
|