@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.
Files changed (66) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +849 -2
  3. package/dev +33 -0
  4. package/package.json +47 -4
  5. package/src/cli.js +217 -0
  6. package/src/commands/completion.js +97 -0
  7. package/src/commands/devices.js +26 -0
  8. package/src/commands/docker.js +35 -0
  9. package/src/commands/env.js +52 -0
  10. package/src/commands/frameworks.js +36 -0
  11. package/src/commands/history.js +59 -0
  12. package/src/commands/inspect.js +48 -0
  13. package/src/commands/lifecycle.js +70 -0
  14. package/src/config/index.js +35 -0
  15. package/src/config/user.js +101 -0
  16. package/src/constants.js +23 -0
  17. package/src/devices/android.js +89 -0
  18. package/src/devices/index.js +14 -0
  19. package/src/devices/ios.js +47 -0
  20. package/src/generators/clean-project.js +52 -0
  21. package/src/generators/python-project.js +175 -0
  22. package/src/generators/reset-project.js +16 -0
  23. package/src/handlers/clone.js +73 -0
  24. package/src/handlers/devices.js +78 -0
  25. package/src/handlers/history.js +75 -0
  26. package/src/handlers/json.js +68 -0
  27. package/src/handlers/lifecycle.js +119 -0
  28. package/src/handlers/mcp.js +10 -0
  29. package/src/handlers/scaffold.js +166 -0
  30. package/src/handlers/system.js +135 -0
  31. package/src/handlers/tools.js +66 -0
  32. package/src/handlers/workflow.js +121 -0
  33. package/src/mcp/processes.js +164 -0
  34. package/src/mcp/server.js +32 -0
  35. package/src/mcp/tools.js +389 -0
  36. package/src/openers/built-ins.json +80 -0
  37. package/src/openers/engine.js +51 -0
  38. package/src/openers/index.js +26 -0
  39. package/src/openers/loader.js +89 -0
  40. package/src/openers/schema.js +81 -0
  41. package/src/projects/adapters/android.js +24 -0
  42. package/src/projects/adapters/common.js +24 -0
  43. package/src/projects/adapters/flutter.js +29 -0
  44. package/src/projects/adapters/index.js +30 -0
  45. package/src/projects/adapters/ios.js +35 -0
  46. package/src/projects/adapters/node.js +112 -0
  47. package/src/projects/adapters/python.js +90 -0
  48. package/src/projects/adapters/recipe.js +13 -0
  49. package/src/projects/detect.js +187 -0
  50. package/src/projects/profile.js +175 -0
  51. package/src/projects/recent.js +50 -0
  52. package/src/projects/workspaces.js +96 -0
  53. package/src/recipes/action-runner.js +24 -0
  54. package/src/recipes/built-ins.json +417 -0
  55. package/src/recipes/engine.js +159 -0
  56. package/src/recipes/index.js +34 -0
  57. package/src/recipes/loader.js +82 -0
  58. package/src/recipes/schema.js +225 -0
  59. package/src/runner/captured.js +125 -0
  60. package/src/runner/check.js +93 -0
  61. package/src/runner/command.js +43 -0
  62. package/src/runner/parallel.js +110 -0
  63. package/src/runner/process.js +80 -0
  64. package/src/runtime/execute.js +99 -0
  65. package/src/utils/args.js +219 -0
  66. package/src/utils/interpolate.js +10 -0
package/dev ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 bachbnt
3
+
4
+ const MIN_NODE_VERSION = '20.12.0';
5
+
6
+ function compareVersions(current, required) {
7
+ const currentParts = current.split('.').map(Number);
8
+ const requiredParts = required.split('.').map(Number);
9
+ for (let index = 0; index < 3; index += 1) {
10
+ const currentPart = currentParts[index] || 0;
11
+ const requiredPart = requiredParts[index] || 0;
12
+ if (currentPart > requiredPart) return 1;
13
+ if (currentPart < requiredPart) return -1;
14
+ }
15
+ return 0;
16
+ }
17
+
18
+ if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
19
+ console.error(`DevCmd requires Node.js ${MIN_NODE_VERSION} or newer. Current Node.js: ${process.version}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const { main } = require('./src/cli');
24
+
25
+ main(process.argv.slice(2)).then(
26
+ (exitCode) => {
27
+ process.exitCode = exitCode;
28
+ },
29
+ (error) => {
30
+ console.error(`DevCmd failed: ${error.message}`);
31
+ process.exitCode = 1;
32
+ }
33
+ );
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@bachbntdev/devcmd",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.7.1",
4
+ "description": "DevCmd project scaffolding, lifecycle, and development environment CLI with an MCP server for AI agents",
5
+ "bin": {
6
+ "dev": "./dev",
7
+ "devcmd": "./dev"
8
+ },
9
+ "files": [
10
+ "dev",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "install-local": "./scripts/install-local.sh",
15
+ "install-remote": "./scripts/install-remote.sh",
16
+ "release": "./scripts/release.sh",
17
+ "test": "node --test",
18
+ "prepublishOnly": "npm test"
19
+ },
20
+ "keywords": [
21
+ "cli",
22
+ "scaffolding",
23
+ "development",
24
+ "monorepo",
25
+ "mcp",
26
+ "devtools"
27
+ ],
28
+ "author": "bachbnt",
29
+ "license": "ISC",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/bachbnt/dev-cmd.git"
33
+ },
34
+ "homepage": "https://github.com/bachbnt/dev-cmd#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/bachbnt/dev-cmd/issues"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.12.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@clack/prompts": "^1.5.1",
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "picocolors": "^1.1.1"
48
+ }
49
+ }
package/src/cli.js ADDED
@@ -0,0 +1,217 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { BRAND, PACKAGE_MANAGERS, VERSION, frameworkDefinitions } = require('./config');
4
+ const { loadConfig } = require('./config/user');
5
+ const { getCompletion } = require('./commands/completion');
6
+ const { handleClone } = require('./handlers/clone');
7
+ const { handleMcp } = require('./handlers/mcp');
8
+ const { handleDeviceCommand, handleDeviceList } = require('./handlers/devices');
9
+ const { handleAgain, handleHistory, rebuildHistoryCommands } = require('./handlers/history');
10
+ const { runJson } = require('./handlers/json');
11
+ const { handleInspect, handleLifecycle } = require('./handlers/lifecycle');
12
+ const { handleDocker, handleEnv } = require('./handlers/tools');
13
+ const { handleParallel, handleTask, handleTrust } = require('./handlers/workflow');
14
+ const { LIFECYCLE_COMMANDS } = require('./constants');
15
+ const { applyConfigDefaults, handleInit, handleScaffold } = require('./handlers/scaffold');
16
+ const {
17
+ handleConfig,
18
+ handleDoctor,
19
+ handleInfo,
20
+ handleOpeners,
21
+ handleProjects,
22
+ handleRecipes,
23
+ handleUpdate,
24
+ } = require('./handlers/system');
25
+ const { describeFailure, executeCommands } = require('./runtime/execute');
26
+ const { parseArgs, validateCommandFlags } = require('./utils/args');
27
+ const { getRecipeDefinitions, loadRecipeRegistry } = require('./recipes');
28
+ const { loadOpenerRegistry } = require('./openers');
29
+
30
+ function printHelp(pc, definitions = frameworkDefinitions, openerRegistry) {
31
+ console.log(
32
+ `\n${pc.bgCyan(pc.black(` ${BRAND} v${VERSION} `))} ${pc.bold('Development toolkit by bachbnt')}\n`
33
+ );
34
+ console.log(pc.bold('Usage:'));
35
+ console.log(' dev <command> [target] [options]\n');
36
+ console.log(pc.bold('Frameworks:'));
37
+ console.log(` ${pc.green('init'.padEnd(20))} ${pc.dim('Create a new project interactively')}`);
38
+ for (const [name, definition] of Object.entries(definitions)) {
39
+ console.log(` ${pc.green(name.padEnd(20))} ${pc.dim(definition.description)}`);
40
+ }
41
+ console.log(`\n${pc.bold('Project Lifecycle:')}`);
42
+ console.log(` ${pc.magenta('doctor'.padEnd(16))} ${pc.dim('Check development tools')}`);
43
+ console.log(` ${pc.magenta('inspect'.padEnd(16))} ${pc.dim('Show detected project and lifecycle commands')}`);
44
+ for (const action of LIFECYCLE_COMMANDS) {
45
+ const label = action.charAt(0).toUpperCase() + action.slice(1);
46
+ console.log(` ${pc.magenta(action.padEnd(16))} ${pc.dim(`${label} the detected project`)}`);
47
+ }
48
+ console.log(` ${pc.magenta('task'.padEnd(16))} ${pc.dim('List or run project tasks (package.json scripts, .devcmd.json)')}`);
49
+ console.log(` ${pc.magenta('parallel'.padEnd(16))} ${pc.dim('Run a lifecycle action in several projects at once')}`);
50
+ console.log(` ${pc.magenta('trust'.padEnd(16))} ${pc.dim('Review and trust the project .devcmd.json profile')}`);
51
+ console.log(` ${pc.magenta('env'.padEnd(16))} ${pc.dim('Create .env from .env.example, or check for missing variables')}`);
52
+ console.log(` ${pc.magenta('docker'.padEnd(16))} ${pc.dim('Run Docker Compose: up, down, build, ps, logs, restart')}`);
53
+ console.log(` ${pc.magenta('projects'.padEnd(16))} ${pc.dim('Show recently used projects')}`);
54
+ console.log(` ${pc.magenta('recipes'.padEnd(16))} ${pc.dim('List or validate framework recipes')}`);
55
+ console.log(` ${pc.magenta('openers'.padEnd(16))} ${pc.dim('List or validate project openers')}`);
56
+ console.log(` ${pc.magenta('config'.padEnd(16))} ${pc.dim('Show or update DevCmd defaults')}`);
57
+ console.log(` ${pc.magenta('completion'.padEnd(16))} ${pc.dim('Generate bash or zsh completion')}`);
58
+ console.log(`\n${pc.bold('Tools:')}`);
59
+ console.log(` ${pc.magenta('clone'.padEnd(16))} ${pc.dim('Clone a repository and install dependencies')}`);
60
+ console.log(` ${pc.magenta('update'.padEnd(16))} ${pc.dim('Update DevCmd to the latest version')}`);
61
+ console.log(` ${pc.magenta('mcp'.padEnd(16))} ${pc.dim('Start the DevCmd MCP server (stdio transport)')}`);
62
+ console.log(`\n${pc.bold('Openers:')}`);
63
+ for (const opener of openerRegistry?.values() || []) {
64
+ const aliases = opener.aliases?.length ? ` (${opener.aliases.join(', ')})` : '';
65
+ const source = opener.source === 'user' ? ' [user]' : '';
66
+ console.log(
67
+ ` ${pc.cyan(opener.name.padEnd(20))} ${pc.dim(`${opener.description}${aliases}${source}`)}`
68
+ );
69
+ }
70
+ console.log(`\n${pc.bold('Devices:')}`);
71
+ console.log(` ${pc.blue('devices'.padEnd(16))} ${pc.dim('List Android and iOS devices')}`);
72
+ console.log(` ${pc.blue('android'.padEnd(16))} ${pc.dim('Boot Android emulator')}`);
73
+ console.log(` ${pc.blue('ios'.padEnd(16))} ${pc.dim('Boot or manage iOS simulators')}`);
74
+ console.log(`\n${pc.bold('History:')}`);
75
+ console.log(` ${pc.cyan('history'.padEnd(16))} ${pc.dim('Show recent successful commands [--clear] [--project <path>]')}`);
76
+ console.log(` ${pc.cyan('again'.padEnd(16))} ${pc.dim('Run a recent command again [n]')}`);
77
+ console.log(`\n${pc.bold('Options:')}`);
78
+ const printOption = (flags, description) => {
79
+ console.log(` ${flags.padEnd(31)} ${pc.dim(description)}`);
80
+ };
81
+ printOption('--dry-run', 'Print without executing');
82
+ printOption('--typescript, --javascript', 'Select project language');
83
+ printOption(
84
+ PACKAGE_MANAGERS.map((manager) => `--${manager}`).join(', '),
85
+ 'Select package manager'
86
+ );
87
+ printOption('--git, --no-git', 'Toggle Git initialization');
88
+ printOption('--eslint, --no-eslint', 'Toggle ESLint where supported');
89
+ printOption('--no-install', 'Scaffold without installing dependencies');
90
+ printOption('--python <executable>', 'Select Python executable');
91
+ printOption('--set <name=value>', 'Set a custom recipe input');
92
+ printOption('--editor <executable>', 'Override editor for dev open');
93
+ printOption('--with <opener>', 'Select an opener for dev open');
94
+ printOption('--list', 'List available openers');
95
+ printOption('--filter <workspace>', 'Target one monorepo workspace (run, test, build, ...)');
96
+ printOption('--fix', 'Apply automatic fixes (lint only)');
97
+ printOption('--json', 'Print machine-readable JSON (inspect, doctor, ...)');
98
+ printOption('--search <text>', 'Filter history by text (history only)');
99
+ printOption('--yes, -y', 'Skip confirmation (trust only)');
100
+ printOption('--cold-boot', 'Cold boot Android emulator');
101
+ printOption('--shutdown-all', 'Shutdown all iOS simulators');
102
+ printOption('--clear', 'Clear history (history only)');
103
+ printOption('--project <path>', 'Filter history by project path (history only)');
104
+ console.log('');
105
+ }
106
+
107
+ const COMMAND_HANDLERS = new Map([
108
+ ['config', handleConfig],
109
+ ['doctor', handleDoctor],
110
+ ['projects', handleProjects],
111
+ ['recipes', handleRecipes],
112
+ ['openers', handleOpeners],
113
+ ['inspect', handleInspect],
114
+ ['info', handleInfo],
115
+ ['history', handleHistory],
116
+ ['again', handleAgain],
117
+ ['init', handleInit],
118
+ ['update', handleUpdate],
119
+ ['clone', handleClone],
120
+ ['mcp', handleMcp],
121
+ ['task', handleTask],
122
+ ['parallel', handleParallel],
123
+ ['trust', handleTrust],
124
+ ['env', handleEnv],
125
+ ['docker', handleDocker],
126
+ ['devices', handleDeviceList],
127
+ ['android', handleDeviceCommand],
128
+ ['ios', handleDeviceCommand],
129
+ ]);
130
+ for (const cmd of LIFECYCLE_COMMANDS) COMMAND_HANDLERS.set(cmd, handleLifecycle);
131
+
132
+ async function dispatch(context) {
133
+ const { command } = context;
134
+ const definitions = context.frameworkDefinitions || frameworkDefinitions;
135
+ const handler = COMMAND_HANDLERS.get(command);
136
+ if (handler) return handler(context);
137
+ if (definitions[command]) return handleScaffold(context);
138
+ throw new Error(`Unsupported command: ${command}. Run dev help to see available commands.`);
139
+ }
140
+
141
+ async function main(argv) {
142
+ const p = await import('@clack/prompts');
143
+ const pc = (await import('picocolors')).default;
144
+ let parsed;
145
+ let recipeRegistry;
146
+ let definitions;
147
+ let openerRegistry;
148
+
149
+ try {
150
+ parsed = parseArgs(argv);
151
+ } catch (error) {
152
+ p.log.error(error.message);
153
+ return 1;
154
+ }
155
+
156
+ const { command, options, positionals } = parsed;
157
+ try {
158
+ const validatingRecipe = command === 'recipes' && positionals[0] === 'validate';
159
+ recipeRegistry = loadRecipeRegistry({ includeUser: !validatingRecipe });
160
+ definitions = getRecipeDefinitions(recipeRegistry);
161
+ const validatingOpener = command === 'openers' && positionals[0] === 'validate';
162
+ openerRegistry = loadOpenerRegistry({ includeUser: !validatingOpener });
163
+ } catch (error) {
164
+ p.log.error(error.message);
165
+ return 1;
166
+ }
167
+ if (!command || ['help', '-h', '--help'].includes(command)) {
168
+ printHelp(pc, definitions, openerRegistry);
169
+ return command ? 0 : 1;
170
+ }
171
+
172
+ if (command === 'completion') {
173
+ try {
174
+ process.stdout.write(getCompletion(
175
+ positionals[0] || process.env.SHELL?.split('/').pop() || 'zsh',
176
+ definitions,
177
+ openerRegistry
178
+ ));
179
+ return 0;
180
+ } catch (error) {
181
+ p.log.error(error.message);
182
+ return 1;
183
+ }
184
+ }
185
+
186
+ try {
187
+ validateCommandFlags(command, options, definitions);
188
+ const config = loadConfig();
189
+ if (options.json) return runJson({ command, options, positionals, config, recipeRegistry, openerRegistry });
190
+ if (command !== 'mcp') p.intro(`${pc.bgCyan(pc.black(` ${BRAND} `))} ${pc.bold(command)}`);
191
+ return await dispatch({
192
+ p,
193
+ pc,
194
+ command,
195
+ options,
196
+ positionals,
197
+ config,
198
+ frameworkDefinitions: definitions,
199
+ recipeRegistry,
200
+ openerRegistry,
201
+ });
202
+ } catch (error) {
203
+ p.log.error(error.message);
204
+ return 1;
205
+ }
206
+ }
207
+
208
+ module.exports = {
209
+ applyConfigDefaults,
210
+ describeFailure,
211
+ dispatch,
212
+ executeCommands,
213
+ handleLifecycle,
214
+ main,
215
+ printHelp,
216
+ rebuildHistoryCommands,
217
+ };
@@ -0,0 +1,97 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { frameworkDefinitions } = require('../config');
4
+
5
+ const { LIFECYCLE_COMMANDS } = require('../constants');
6
+
7
+ const SYSTEM_COMMANDS = [
8
+ 'doctor',
9
+ 'inspect',
10
+ ...LIFECYCLE_COMMANDS,
11
+ 'task',
12
+ 'parallel',
13
+ 'trust',
14
+ 'env',
15
+ 'docker',
16
+ 'devices',
17
+ 'android',
18
+ 'ios',
19
+ 'history',
20
+ 'again',
21
+ 'projects',
22
+ 'recipes',
23
+ 'openers',
24
+ 'config',
25
+ 'completion',
26
+ 'help',
27
+ ];
28
+ const COMMANDS = [...Object.keys(frameworkDefinitions), ...SYSTEM_COMMANDS];
29
+
30
+ const OPTIONS = [
31
+ '--dry-run', '--typescript', '--javascript', '--npm', '--pnpm', '--yarn',
32
+ '--git', '--no-git', '--eslint', '--no-eslint', '--no-install', '--python', '--editor',
33
+ '--cold-boot', '--shutdown-all', '--set', '--with', '--list',
34
+ '--filter', '--fix', '--json', '--search', '--yes',
35
+ ];
36
+
37
+ function commandNames(definitions = frameworkDefinitions) {
38
+ return [...Object.keys(definitions), ...SYSTEM_COMMANDS];
39
+ }
40
+
41
+ function openerNames(registry) {
42
+ if (!registry) return [];
43
+ return [...registry.values()].flatMap((opener) => [opener.name, ...(opener.aliases || [])]);
44
+ }
45
+
46
+ function bashCompletion(definitions, openerRegistry) {
47
+ const commands = commandNames(definitions);
48
+ const candidates = [...OPTIONS, ...openerNames(openerRegistry)];
49
+ return `_devcmd_completion() {
50
+ local current
51
+ current="\${COMP_WORDS[COMP_CWORD]}"
52
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
53
+ COMPREPLY=( $(compgen -W "${commands.join(' ')}" -- "\${current}") )
54
+ else
55
+ COMPREPLY=( $(compgen -W "${candidates.join(' ')}" -- "\${current}") )
56
+ fi
57
+ }
58
+ complete -F _devcmd_completion dev devcmd
59
+ `;
60
+ }
61
+
62
+ function zshCompletion(definitions, openerRegistry) {
63
+ const commands = commandNames(definitions);
64
+ const candidates = [...OPTIONS, ...openerNames(openerRegistry)];
65
+ return `#compdef dev devcmd
66
+
67
+ _devcmd() {
68
+ local -a commands options
69
+ commands=(${commands.map((command) => `'${command}'`).join(' ')})
70
+ options=(${candidates.map((option) => `'${option}'`).join(' ')})
71
+
72
+ if (( CURRENT == 2 )); then
73
+ _describe 'command' commands
74
+ else
75
+ _describe 'option' options
76
+ _files
77
+ fi
78
+ }
79
+
80
+ compdef _devcmd dev devcmd
81
+ `;
82
+ }
83
+
84
+ function getCompletion(shell, definitions = frameworkDefinitions, openerRegistry) {
85
+ if (shell === 'bash') return bashCompletion(definitions, openerRegistry);
86
+ if (shell === 'zsh') return zshCompletion(definitions, openerRegistry);
87
+ throw new Error('Completion supports: bash, zsh.');
88
+ }
89
+
90
+ module.exports = {
91
+ COMMANDS,
92
+ OPTIONS,
93
+ SYSTEM_COMMANDS,
94
+ commandNames,
95
+ getCompletion,
96
+ openerNames,
97
+ };
@@ -0,0 +1,26 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { createCommand } = require('../runner/command');
4
+
5
+ function buildAndroidCommands(target, options = {}) {
6
+ const args = ['-avd', target];
7
+ if (options.coldBoot) args.push('-no-snapshot-load');
8
+ return [createCommand('emulator', args)];
9
+ }
10
+
11
+ function buildIOSBootCommands(target) {
12
+ return [
13
+ createCommand('open', ['-a', 'Simulator']),
14
+ createCommand('xcrun', ['simctl', 'boot', target]),
15
+ ];
16
+ }
17
+
18
+ function buildIOSShutdownCommands() {
19
+ return [createCommand('xcrun', ['simctl', 'shutdown', 'all'])];
20
+ }
21
+
22
+ module.exports = {
23
+ buildAndroidCommands,
24
+ buildIOSBootCommands,
25
+ buildIOSShutdownCommands,
26
+ };
@@ -0,0 +1,35 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { createCommand } = require('../runner/command');
6
+
7
+ const COMPOSE_FILES = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml'];
8
+ const DOCKER_ACTIONS = {
9
+ up: ['up', '-d'],
10
+ down: ['down'],
11
+ build: ['build'],
12
+ ps: ['ps'],
13
+ logs: ['logs', '--follow', '--tail', '100'],
14
+ restart: ['restart'],
15
+ };
16
+
17
+ function findComposeFile(root, fsImpl = fs) {
18
+ return COMPOSE_FILES.find((name) => fsImpl.existsSync(path.join(root, name)));
19
+ }
20
+
21
+ function buildDockerCommands(action, root, fsImpl = fs) {
22
+ if (!Object.hasOwn(DOCKER_ACTIONS, action)) {
23
+ throw new Error(`Usage: dev docker <${Object.keys(DOCKER_ACTIONS).join('|')}> [path]`);
24
+ }
25
+ const file = findComposeFile(root, fsImpl);
26
+ if (!file) throw new Error(`No Compose file was found in ${root}. Expected: ${COMPOSE_FILES.join(', ')}.`);
27
+ return [createCommand('docker', ['compose', '--file', file, ...DOCKER_ACTIONS[action]], { cwd: root })];
28
+ }
29
+
30
+ module.exports = {
31
+ COMPOSE_FILES,
32
+ DOCKER_ACTIONS,
33
+ buildDockerCommands,
34
+ findComposeFile,
35
+ };
@@ -0,0 +1,52 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEMPLATE_FILES = ['.env.example', '.env.sample', '.env.template'];
7
+ const ENV_FILE = '.env';
8
+
9
+ function parseEnvKeys(content) {
10
+ const keys = [];
11
+ for (const line of content.split(/\r?\n/)) {
12
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
13
+ if (match) keys.push(match[1]);
14
+ }
15
+ return keys;
16
+ }
17
+
18
+ function findTemplate(root, fsImpl = fs) {
19
+ return TEMPLATE_FILES.find((name) => fsImpl.existsSync(path.join(root, name)));
20
+ }
21
+
22
+ function initEnv(root, fsImpl = fs) {
23
+ const template = findTemplate(root, fsImpl);
24
+ if (!template) throw new Error(`No ${TEMPLATE_FILES.join(', ')} file was found in ${root}.`);
25
+ const target = path.join(root, ENV_FILE);
26
+ if (fsImpl.existsSync(target)) return { created: false, template, file: target };
27
+ fsImpl.copyFileSync(path.join(root, template), target, fs.constants.COPYFILE_EXCL);
28
+ return { created: true, template, file: target };
29
+ }
30
+
31
+ function checkEnv(root, fsImpl = fs) {
32
+ const template = findTemplate(root, fsImpl);
33
+ if (!template) throw new Error(`No ${TEMPLATE_FILES.join(', ')} file was found in ${root}.`);
34
+ const target = path.join(root, ENV_FILE);
35
+ if (!fsImpl.existsSync(target)) {
36
+ throw new Error(`${ENV_FILE} does not exist. Run: dev env`);
37
+ }
38
+ const expected = parseEnvKeys(fsImpl.readFileSync(path.join(root, template), 'utf8'));
39
+ const actual = new Set(parseEnvKeys(fsImpl.readFileSync(target, 'utf8')));
40
+ const missing = expected.filter((key) => !actual.has(key));
41
+ const known = new Set(expected);
42
+ const extra = [...actual].filter((key) => !known.has(key));
43
+ return { template, missing, extra, ok: missing.length === 0 };
44
+ }
45
+
46
+ module.exports = {
47
+ TEMPLATE_FILES,
48
+ checkEnv,
49
+ findTemplate,
50
+ initEnv,
51
+ parseEnvKeys,
52
+ };
@@ -0,0 +1,36 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const {
4
+ buildRecipeCommands,
5
+ builtInRegistry,
6
+ getRecipeRequirements,
7
+ } = require('../recipes');
8
+
9
+ function getRecipe(command, registry = builtInRegistry) {
10
+ const recipe = registry.get(command);
11
+ if (!recipe) throw new Error(`Unsupported framework: ${command}`);
12
+ return recipe;
13
+ }
14
+
15
+ function buildFrameworkCommands(command, target, options = {}, registry = builtInRegistry) {
16
+ return buildRecipeCommands(getRecipe(command, registry), target, options);
17
+ }
18
+
19
+ function buildPythonCommands(framework, target, options = {}, registry = builtInRegistry) {
20
+ if (!['django', 'fastapi', 'flask'].includes(framework)) {
21
+ throw new Error(`Unsupported Python framework: ${framework}`);
22
+ }
23
+ return buildFrameworkCommands(framework, target, options, registry);
24
+ }
25
+
26
+ function getFrameworkRequirements(command, options = {}, registry = builtInRegistry, target = 'project') {
27
+ return getRecipeRequirements(getRecipe(command, registry), target, options);
28
+ }
29
+
30
+ module.exports = {
31
+ buildFrameworkCommands,
32
+ buildPythonCommands,
33
+ builtInRegistry,
34
+ getFrameworkRequirements,
35
+ getRecipe,
36
+ };
@@ -0,0 +1,59 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { HISTORY_FILE } = require('../config');
6
+ const { formatSequence, isCommandSequence } = require('../runner/command');
7
+
8
+ function readHistory(options = {}) {
9
+ const file = options.file || HISTORY_FILE;
10
+ const fsImpl = options.fsImpl || fs;
11
+ try {
12
+ const history = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
13
+ return Array.isArray(history) ? history : [];
14
+ } catch (error) {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function saveHistory(entry, options = {}) {
20
+ const file = options.file || HISTORY_FILE;
21
+ const fsImpl = options.fsImpl || fs;
22
+ const history = readHistory({ file, fsImpl });
23
+ history.unshift(entry);
24
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
25
+ fsImpl.writeFileSync(file, `${JSON.stringify(history.slice(0, 50), null, 2)}\n`);
26
+ }
27
+
28
+ function createHistoryEntry(metadata, commands) {
29
+ return {
30
+ ...metadata,
31
+ commands,
32
+ commandLine: formatSequence(commands),
33
+ executedAt: new Date().toISOString(),
34
+ };
35
+ }
36
+
37
+ function filterHistory(history, filters = {}) {
38
+ const project = filters.project ? path.resolve(filters.project) : undefined;
39
+ const search = filters.search?.toLowerCase();
40
+ return history.filter((entry) => {
41
+ if (project && entry.projectPath !== project) return false;
42
+ if (!search) return true;
43
+ return [entry.command, entry.projectName, entry.commandLine, entry.projectType]
44
+ .some((value) => typeof value === 'string' && value.toLowerCase().includes(search));
45
+ });
46
+ }
47
+
48
+ function getHistoryDisplay(entry) {
49
+ if (isCommandSequence(entry.commands)) return formatSequence(entry.commands);
50
+ return entry.commandLine || 'Unknown command';
51
+ }
52
+
53
+ module.exports = {
54
+ createHistoryEntry,
55
+ filterHistory,
56
+ getHistoryDisplay,
57
+ readHistory,
58
+ saveHistory,
59
+ };
@@ -0,0 +1,48 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { buildLifecycleCommands, buildOpenCommands } = require('./lifecycle');
4
+ const { formatSequence } = require('../runner/command');
5
+ const { LIFECYCLE_COMMANDS } = require('../constants');
6
+ const { isTrusted } = require('../projects/profile');
7
+ const { listTasks } = require('./lifecycle');
8
+
9
+ function inspectAction(action, project, config, openerRegistry, options = {}) {
10
+ try {
11
+ const commands = action === 'open'
12
+ ? buildOpenCommands(project, undefined, config, openerRegistry)
13
+ : buildLifecycleCommands(action, project, { config, trustFile: options.trustFile });
14
+ return {
15
+ action,
16
+ available: true,
17
+ command: formatSequence(commands),
18
+ commands,
19
+ };
20
+ } catch (error) {
21
+ return { action, available: false, reason: error.message };
22
+ }
23
+ }
24
+
25
+ function inspectProject(project, config, openerRegistry, options = {}) {
26
+ return {
27
+ name: project.name,
28
+ type: project.type,
29
+ root: project.root,
30
+ packageManager: project.packageManager,
31
+ xcode: project.xcode,
32
+ recipeSource: project.recipe?.source,
33
+ profile: project.profile
34
+ ? { file: project.profile.file, trusted: isTrusted(project.profile, options) }
35
+ : undefined,
36
+ workspaces: (project.workspaces || []).map(({ name, dir }) => ({ name, dir })),
37
+ tasks: listTasks(project),
38
+ actions: LIFECYCLE_COMMANDS.map(
39
+ (action) => inspectAction(action, project, config, openerRegistry, options)
40
+ ),
41
+ };
42
+ }
43
+
44
+ module.exports = {
45
+ INSPECT_ACTIONS: LIFECYCLE_COMMANDS,
46
+ inspectAction,
47
+ inspectProject,
48
+ };