@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
@@ -0,0 +1,80 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ const SIGNAL_EXIT_CODES = {
6
+ SIGINT: 130,
7
+ SIGTERM: 143,
8
+ };
9
+
10
+ function runProcess(command, options = {}) {
11
+ const spawnImpl = options.spawnImpl || spawn;
12
+ const signalSource = options.signalSource || process;
13
+ const stdio = options.stdio || 'inherit';
14
+
15
+ return new Promise((resolve) => {
16
+ let settled = false;
17
+ let forwardedSignal;
18
+ let child;
19
+
20
+ const finish = (result) => {
21
+ if (settled) return;
22
+ settled = true;
23
+ signalSource.removeListener('SIGINT', onSigint);
24
+ signalSource.removeListener('SIGTERM', onSigterm);
25
+ resolve(result);
26
+ };
27
+
28
+ const forwardSignal = (signal) => {
29
+ forwardedSignal = signal;
30
+ if (child && !child.killed) child.kill(signal);
31
+ };
32
+ const onSigint = () => forwardSignal('SIGINT');
33
+ const onSigterm = () => forwardSignal('SIGTERM');
34
+
35
+ try {
36
+ const spawnOptions = {
37
+ shell: false,
38
+ stdio,
39
+ };
40
+ if (command.cwd) spawnOptions.cwd = command.cwd;
41
+ child = spawnImpl(command.executable, command.args, spawnOptions);
42
+ } catch (error) {
43
+ finish({ ok: false, exitCode: error.code === 'ENOENT' ? 127 : 1, error });
44
+ return;
45
+ }
46
+
47
+ signalSource.once('SIGINT', onSigint);
48
+ signalSource.once('SIGTERM', onSigterm);
49
+
50
+ child.once('error', (error) => {
51
+ finish({ ok: false, exitCode: error.code === 'ENOENT' ? 127 : 1, error });
52
+ });
53
+ child.once('exit', (code, signal) => {
54
+ const finalSignal = signal || forwardedSignal;
55
+ if (finalSignal) {
56
+ finish({
57
+ ok: false,
58
+ exitCode: SIGNAL_EXIT_CODES[finalSignal] || 1,
59
+ signal: finalSignal,
60
+ });
61
+ return;
62
+ }
63
+ finish({ ok: code === 0, exitCode: code ?? 1 });
64
+ });
65
+ });
66
+ }
67
+
68
+ async function runSequence(commands, options = {}) {
69
+ for (let index = 0; index < commands.length; index += 1) {
70
+ const result = await runProcess(commands[index], options);
71
+ if (!result.ok) return { ...result, failedCommand: commands[index], failedIndex: index };
72
+ }
73
+ return { ok: true, exitCode: 0 };
74
+ }
75
+
76
+ module.exports = {
77
+ SIGNAL_EXIT_CODES,
78
+ runProcess,
79
+ runSequence,
80
+ };
@@ -0,0 +1,99 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { spawn } = require('child_process');
4
+ const { createHistoryEntry, saveHistory } = require('../commands/history');
5
+ const { recordProject } = require('../projects/recent');
6
+ const { assertExecutables } = require('../runner/check');
7
+ const { formatSequence } = require('../runner/command');
8
+ const { runSequence } = require('../runner/process');
9
+
10
+ function buildNotificationCommand(message, platform = process.platform) {
11
+ if (platform === 'darwin') {
12
+ return {
13
+ executable: 'osascript',
14
+ args: [
15
+ '-e',
16
+ 'on run argv',
17
+ '-e',
18
+ 'display notification (item 1 of argv) with title "DevCmd"',
19
+ '-e',
20
+ 'end run',
21
+ message,
22
+ ],
23
+ };
24
+ }
25
+ if (platform === 'linux') {
26
+ return { executable: 'notify-send', args: ['DevCmd', message] };
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ function sendNotification(message) {
32
+ try {
33
+ const command = buildNotificationCommand(message);
34
+ if (command) spawn(command.executable, command.args, { stdio: 'ignore' });
35
+ } catch {}
36
+ }
37
+
38
+ function describeFailure(result) {
39
+ if (result.signal === 'SIGINT') return 'Command cancelled by user.';
40
+ if (result.signal) return `Command stopped by ${result.signal}.`;
41
+ if (result.error?.code === 'ENOENT') {
42
+ return `Executable "${result.failedCommand.executable}" was not found in PATH.`;
43
+ }
44
+ if (result.error) return `Unable to start command: ${result.error.message}`;
45
+ return `Command exited with code ${result.exitCode}.`;
46
+ }
47
+
48
+ async function executeCommands(p, pc, commands, metadata, options = {}) {
49
+ const commandLine = formatSequence(commands);
50
+ p.log.step(`${options.dryRun ? 'Dry run' : 'Executing'}: ${pc.yellow(commandLine)}`);
51
+
52
+ if (options.dryRun) {
53
+ p.outro('No command was executed.');
54
+ return 0;
55
+ }
56
+
57
+ try {
58
+ if (options.requirements) assertExecutables(options.requirements);
59
+ } catch (error) {
60
+ p.outro(`${pc.red('✖')} ${error.message}`);
61
+ return 127;
62
+ }
63
+
64
+ const startTime = Date.now();
65
+ const result = await runSequence(commands);
66
+ const elapsed = Date.now() - startTime;
67
+ const notifyThreshold = (options.config ?? metadata.config)?.notify;
68
+ if (notifyThreshold > 0 && elapsed >= notifyThreshold * 1000) {
69
+ const status = result.ok ? '✔' : '✖';
70
+ const project = metadata.projectName ? ` — ${metadata.projectName}` : '';
71
+ sendNotification(`dev ${metadata.command}${project} ${status}`);
72
+ }
73
+
74
+ if (!result.ok) {
75
+ p.outro(`${pc.red('✖')} ${describeFailure(result)}`);
76
+ return result.exitCode;
77
+ }
78
+
79
+ try {
80
+ saveHistory(createHistoryEntry(metadata, commands));
81
+ } catch (error) {
82
+ p.log.warn(`Command succeeded, but history could not be saved: ${error.message}`);
83
+ }
84
+ if (metadata.projectPath) {
85
+ recordProject({
86
+ name: metadata.projectName,
87
+ type: metadata.projectType || metadata.command,
88
+ path: metadata.projectPath,
89
+ });
90
+ }
91
+ p.outro(`${pc.green('✔')} Command completed successfully.`);
92
+ return 0;
93
+ }
94
+
95
+ module.exports = {
96
+ buildNotificationCommand,
97
+ describeFailure,
98
+ executeCommands,
99
+ };
@@ -0,0 +1,219 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { PACKAGE_MANAGERS, frameworkDefinitions } = require('../config');
4
+ const { FILTER_COMMANDS, JSON_COMMANDS } = require('../constants');
5
+
6
+ const FILTER_PATTERN = /^[@A-Za-z0-9._][@A-Za-z0-9._/-]*$/;
7
+
8
+ function parseArgs(argv) {
9
+ const args = [...argv];
10
+ const command = args.shift();
11
+ const options = {
12
+ dryRun: false,
13
+ coldBoot: false,
14
+ shutdownAll: false,
15
+ clear: false,
16
+ project: undefined,
17
+ target: undefined,
18
+ typescript: undefined,
19
+ packageManager: undefined,
20
+ git: undefined,
21
+ eslint: undefined,
22
+ noInstall: false,
23
+ python: undefined,
24
+ editor: undefined,
25
+ opener: undefined,
26
+ list: false,
27
+ json: false,
28
+ fix: false,
29
+ yes: false,
30
+ filter: undefined,
31
+ search: undefined,
32
+ values: {},
33
+ };
34
+ const positionals = [];
35
+
36
+ while (args.length > 0) {
37
+ const arg = args.shift();
38
+
39
+ if (!arg.startsWith('-')) {
40
+ positionals.push(arg);
41
+ if (!options.target) options.target = arg;
42
+ continue;
43
+ }
44
+
45
+ if (arg === '--dry-run') options.dryRun = true;
46
+ else if (arg === '--cold-boot') options.coldBoot = true;
47
+ else if (arg === '--shutdown-all') options.shutdownAll = true;
48
+ else if (arg === '--clear') options.clear = true;
49
+ else if (arg === '--project') {
50
+ options.project = args.shift();
51
+ if (!options.project || options.project.startsWith('-')) {
52
+ throw new Error('--project requires a path.');
53
+ }
54
+ }
55
+ else if (arg === '--list') options.list = true;
56
+ else if (arg === '--json') options.json = true;
57
+ else if (arg === '--fix') options.fix = true;
58
+ else if (arg === '--yes' || arg === '-y') options.yes = true;
59
+ else if (arg === '--filter') {
60
+ options.filter = args.shift();
61
+ if (!options.filter || !FILTER_PATTERN.test(options.filter) || options.filter.includes('..')) {
62
+ throw new Error('--filter requires a workspace package name or directory.');
63
+ }
64
+ } else if (arg === '--search') {
65
+ options.search = args.shift();
66
+ if (!options.search || options.search.startsWith('-')) {
67
+ throw new Error('--search requires text.');
68
+ }
69
+ }
70
+ else if (arg === '--typescript' || arg === '--ts') options.typescript = true;
71
+ else if (arg === '--javascript' || arg === '--js') options.typescript = false;
72
+ else if (arg === '--git') options.git = true;
73
+ else if (arg === '--no-git') options.git = false;
74
+ else if (arg === '--eslint') options.eslint = true;
75
+ else if (arg === '--no-eslint') options.eslint = false;
76
+ else if (arg === '--no-install') options.noInstall = true;
77
+ else if (arg === '--install') options.noInstall = false;
78
+ else if (arg === '--python') {
79
+ options.python = args.shift();
80
+ if (!options.python || options.python.startsWith('-')) {
81
+ throw new Error('--python requires an executable name or path.');
82
+ }
83
+ } else if (arg === '--editor') {
84
+ options.editor = args.shift();
85
+ if (!options.editor || options.editor.startsWith('-')) {
86
+ throw new Error('--editor requires an application or executable name.');
87
+ }
88
+ } else if (arg === '--with') {
89
+ options.opener = args.shift();
90
+ if (!options.opener || options.opener.startsWith('-')) {
91
+ throw new Error('--with requires an opener name.');
92
+ }
93
+ } else if (arg === '--set') {
94
+ const assignment = args.shift();
95
+ const separator = assignment?.indexOf('=') ?? -1;
96
+ if (separator < 1) throw new Error('--set requires name=value.');
97
+ const name = assignment.slice(0, separator);
98
+ const value = assignment.slice(separator + 1);
99
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name) || !value) {
100
+ throw new Error('--set requires a valid name and non-empty value.');
101
+ }
102
+ options.values[name] = value;
103
+ }
104
+ else if (PACKAGE_MANAGERS.includes(arg.replace(/^--/, ''))) {
105
+ options.packageManager = arg.replace(/^--/, '');
106
+ } else if (arg === '--package-manager' || arg === '--pm') {
107
+ options.packageManager = args.shift();
108
+ if (!PACKAGE_MANAGERS.includes(options.packageManager)) {
109
+ throw new Error(`Package manager must be one of: ${PACKAGE_MANAGERS.join(', ')}`);
110
+ }
111
+ } else {
112
+ throw new Error(`Unsupported option: ${arg}`);
113
+ }
114
+ }
115
+
116
+ return { command, options, positionals };
117
+ }
118
+
119
+ function validateCommandFlags(command, options, definitions = frameworkDefinitions) {
120
+ if (options.coldBoot && command !== 'android') {
121
+ throw new Error('--cold-boot is only supported by the android command.');
122
+ }
123
+ if (options.clear && command !== 'history') {
124
+ throw new Error('--clear is only supported by the history command.');
125
+ }
126
+ if (options.project && command !== 'history') {
127
+ throw new Error('--project is only supported by the history command.');
128
+ }
129
+ if (options.shutdownAll && command !== 'ios') {
130
+ throw new Error('--shutdown-all is only supported by the ios command.');
131
+ }
132
+ if (options.list && command !== 'open') {
133
+ throw new Error('--list is only supported by the open command.');
134
+ }
135
+ if (options.opener && command !== 'open') {
136
+ throw new Error('--with is only supported by the open command.');
137
+ }
138
+ if (options.editor && command !== 'open') {
139
+ throw new Error('--editor is only supported by the open command.');
140
+ }
141
+ if (options.json && !JSON_COMMANDS.includes(command)) {
142
+ throw new Error(`--json is only supported by: ${JSON_COMMANDS.join(', ')}.`);
143
+ }
144
+ if (options.fix && command !== 'lint') {
145
+ throw new Error('--fix is only supported by the lint command.');
146
+ }
147
+ if (options.yes && command !== 'trust') {
148
+ throw new Error('--yes is only supported by the trust command.');
149
+ }
150
+ if (options.filter && !FILTER_COMMANDS.includes(command)) {
151
+ throw new Error(`--filter is only supported by: ${FILTER_COMMANDS.join(', ')}.`);
152
+ }
153
+ if (options.search && command !== 'history') {
154
+ throw new Error('--search is only supported by the history command.');
155
+ }
156
+ if (options.editor && options.opener) {
157
+ throw new Error('--editor and --with cannot be used together.');
158
+ }
159
+
160
+ const hasPreset =
161
+ options.typescript !== undefined ||
162
+ options.packageManager !== undefined ||
163
+ options.git !== undefined ||
164
+ options.eslint !== undefined ||
165
+ options.noInstall ||
166
+ options.python !== undefined ||
167
+ Object.keys(options.values || {}).length > 0;
168
+ if (hasPreset && !definitions[command]) {
169
+ throw new Error('Project preset options can only be used with framework commands.');
170
+ }
171
+ }
172
+
173
+ function validateFrameworkOptions(command, options, definitions = frameworkDefinitions) {
174
+ const definition = definitions[command];
175
+ if (!definition) throw new Error(`Unsupported framework: ${command}`);
176
+
177
+ const capabilities = definition.capabilities;
178
+ if (options.typescript !== undefined && !capabilities.language) {
179
+ throw new Error(`${command} does not support selecting TypeScript or JavaScript.`);
180
+ }
181
+ if (options.packageManager && !capabilities.packageManager) {
182
+ throw new Error(`${command} does not support selecting a package manager.`);
183
+ }
184
+ if (options.git !== undefined && !capabilities.git) {
185
+ throw new Error(`${command} does not support configuring Git initialization.`);
186
+ }
187
+ if (options.eslint !== undefined && !capabilities.eslint) {
188
+ throw new Error(`${command} does not support configuring ESLint.`);
189
+ }
190
+ if (options.noInstall && !capabilities.noInstall) {
191
+ throw new Error(`${command} does not support --no-install.`);
192
+ }
193
+ if (options.python && !capabilities.python) {
194
+ throw new Error(`${command} does not support selecting a Python executable.`);
195
+ }
196
+ if (
197
+ Array.isArray(capabilities.packageManager) &&
198
+ options.packageManager &&
199
+ !capabilities.packageManager.includes(options.packageManager)
200
+ ) {
201
+ throw new Error(
202
+ `${command} supports package managers: ${capabilities.packageManager.join(', ')}`
203
+ );
204
+ }
205
+ }
206
+
207
+ function validateProjectName(target) {
208
+ if (!target) throw new Error('Project name is required.');
209
+ if (!/^[A-Za-z0-9._-]+$/.test(target)) {
210
+ throw new Error('Invalid project name. Use only letters, numbers, dots, underscores, and hyphens.');
211
+ }
212
+ }
213
+
214
+ module.exports = {
215
+ parseArgs,
216
+ validateCommandFlags,
217
+ validateFrameworkOptions,
218
+ validateProjectName,
219
+ };
@@ -0,0 +1,10 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ function interpolate(value, context, label) {
4
+ return value.replace(/\{([A-Za-z][A-Za-z0-9_]*)\}/g, (match, key) => {
5
+ if (context[key] === undefined) throw new Error(`Unknown ${label}: ${key}`);
6
+ return String(context[key]);
7
+ });
8
+ }
9
+
10
+ module.exports = { interpolate };