@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
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { PACKAGE_MANAGERS } = require('../config');
|
|
6
|
+
const { loadConfig } = require('../config/user');
|
|
7
|
+
const { buildAndroidCommands, buildIOSBootCommands } = require('../commands/devices');
|
|
8
|
+
const { filterHistory, readHistory } = require('../commands/history');
|
|
9
|
+
const { inspectProject } = require('../commands/inspect');
|
|
10
|
+
const { buildLifecycleCommands, buildOpenCommands, buildTaskCommands } = require('../commands/lifecycle');
|
|
11
|
+
const { LIFECYCLE_COMMANDS } = require('../constants');
|
|
12
|
+
const { getAndroidDevices, getIOSDevices } = require('../devices');
|
|
13
|
+
const { loadOpenerRegistry } = require('../openers');
|
|
14
|
+
const { detectProject } = require('../projects/detect');
|
|
15
|
+
const { getRecipeDefinitions, loadRecipeRegistry } = require('../recipes');
|
|
16
|
+
const { checkTools } = require('../runner/check');
|
|
17
|
+
const { formatSequence } = require('../runner/command');
|
|
18
|
+
const { runSequenceCaptured } = require('../runner/captured');
|
|
19
|
+
const { validateProjectName } = require('../utils/args');
|
|
20
|
+
const { ProcessManager } = require('./processes');
|
|
21
|
+
|
|
22
|
+
const DEV_BINARY = path.join(__dirname, '..', '..', 'dev');
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
24
|
+
const RUN_TIMEOUT_MS = 30000;
|
|
25
|
+
const SCAFFOLD_TIMEOUT_MS = 600000;
|
|
26
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
const defaultProcesses = new ProcessManager();
|
|
29
|
+
|
|
30
|
+
const PATH_PROPERTY = { type: 'string', description: 'Project path (defaults to the server working directory).' };
|
|
31
|
+
const FILTER_PROPERTY = { type: 'string', description: 'Monorepo workspace name or directory (Node.js workspaces only).' };
|
|
32
|
+
const DRY_RUN_PROPERTY = { type: 'boolean', description: 'Return the resolved commands without executing them.' };
|
|
33
|
+
const TIMEOUT_PROPERTY = { type: 'integer', description: 'Maximum time to wait before terminating the process.' };
|
|
34
|
+
const OUTPUT_PROPERTY = { type: 'integer', description: 'Maximum captured output bytes to return.' };
|
|
35
|
+
|
|
36
|
+
const TOOLS = [
|
|
37
|
+
{
|
|
38
|
+
name: 'inspect_project',
|
|
39
|
+
description: 'Detect the project and return its type, package manager, workspaces, tasks, profile trust state, and the exact command DevCmd would run for every lifecycle action.',
|
|
40
|
+
inputSchema: { type: 'object', properties: { path: PATH_PROPERTY } },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'run_lifecycle',
|
|
44
|
+
description: `Run a lifecycle action and return structured JSON with exit code and captured output. Actions: ${LIFECYCLE_COMMANDS.join(', ')}. Long-running servers (run) are stopped after the timeout; use start_process for those.`,
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
required: ['action'],
|
|
48
|
+
properties: {
|
|
49
|
+
action: { type: 'string', enum: LIFECYCLE_COMMANDS, description: 'Lifecycle action to run.' },
|
|
50
|
+
path: PATH_PROPERTY,
|
|
51
|
+
filter: FILTER_PROPERTY,
|
|
52
|
+
fix: { type: 'boolean', description: 'Apply automatic fixes (lint only).' },
|
|
53
|
+
dryRun: DRY_RUN_PROPERTY,
|
|
54
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
55
|
+
maxOutputBytes: OUTPUT_PROPERTY,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'run_task',
|
|
61
|
+
description: 'Run a project task: a .devcmd.json task or a package.json script. Tasks are listed by inspect_project.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
required: ['name'],
|
|
65
|
+
properties: {
|
|
66
|
+
name: { type: 'string', description: 'Task or script name.' },
|
|
67
|
+
path: PATH_PROPERTY,
|
|
68
|
+
filter: FILTER_PROPERTY,
|
|
69
|
+
dryRun: DRY_RUN_PROPERTY,
|
|
70
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
71
|
+
maxOutputBytes: OUTPUT_PROPERTY,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'start_process',
|
|
77
|
+
description: 'Start a lifecycle action (for example run, to launch a dev server) in the background and return a process id. Use get_process_logs and stop_process to manage it.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
required: ['action'],
|
|
81
|
+
properties: {
|
|
82
|
+
action: { type: 'string', enum: LIFECYCLE_COMMANDS.filter((action) => action !== 'open') },
|
|
83
|
+
path: PATH_PROPERTY,
|
|
84
|
+
filter: FILTER_PROPERTY,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'get_process_logs',
|
|
90
|
+
description: 'Return the status and the most recent output lines of a background process.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
required: ['id'],
|
|
94
|
+
properties: {
|
|
95
|
+
id: { type: 'string' },
|
|
96
|
+
tail: { type: 'integer', description: 'Number of trailing lines to return (default 100).' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'stop_process',
|
|
102
|
+
description: 'Stop a background process started by this server.',
|
|
103
|
+
inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'list_processes',
|
|
107
|
+
description: 'List background processes started by this server.',
|
|
108
|
+
inputSchema: { type: 'object', properties: {} },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'scaffold_project',
|
|
112
|
+
description: 'Create a new project from a framework recipe. dryRun defaults to true: review the command, then call again with dryRun false to create the project.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
required: ['framework', 'name'],
|
|
116
|
+
properties: {
|
|
117
|
+
framework: { type: 'string', description: 'Recipe name from list_frameworks.' },
|
|
118
|
+
name: { type: 'string', description: 'Project directory name (letters, numbers, dots, underscores, hyphens).' },
|
|
119
|
+
directory: { type: 'string', description: 'Existing parent directory (defaults to the server working directory).' },
|
|
120
|
+
typescript: { type: 'boolean' },
|
|
121
|
+
packageManager: { type: 'string', enum: PACKAGE_MANAGERS },
|
|
122
|
+
git: { type: 'boolean' },
|
|
123
|
+
eslint: { type: 'boolean' },
|
|
124
|
+
install: { type: 'boolean', description: 'Set false to skip dependency installation.' },
|
|
125
|
+
python: { type: 'string', description: 'Python executable for Python frameworks.' },
|
|
126
|
+
values: { type: 'object', additionalProperties: { type: 'string' }, description: 'Custom recipe inputs.' },
|
|
127
|
+
dryRun: { type: 'boolean', description: 'Default true.' },
|
|
128
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'list_frameworks',
|
|
134
|
+
description: 'List framework recipes that can be scaffolded, with their supported options and inputs.',
|
|
135
|
+
inputSchema: { type: 'object', properties: {} },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'doctor',
|
|
139
|
+
description: 'Check which development tools are installed and how to install missing ones.',
|
|
140
|
+
inputSchema: { type: 'object', properties: {} },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'list_devices',
|
|
144
|
+
description: 'List Android emulators and iOS simulators with runtime and boot status.',
|
|
145
|
+
inputSchema: { type: 'object', properties: {} },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'boot_device',
|
|
149
|
+
description: 'Boot an Android emulator (runs in the background; returns a process id) or an iOS simulator.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
required: ['platform', 'name'],
|
|
153
|
+
properties: {
|
|
154
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
155
|
+
name: { type: 'string', description: 'Device name or identifier from list_devices.' },
|
|
156
|
+
coldBoot: { type: 'boolean', description: 'Android only: skip the saved snapshot.' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'get_history',
|
|
162
|
+
description: 'Return recent successful DevCmd commands, optionally filtered.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
limit: { type: 'integer', description: 'Max entries to return (default 10, max 50).' },
|
|
167
|
+
project: { type: 'string', description: 'Only entries for this project path.' },
|
|
168
|
+
search: { type: 'string', description: 'Only entries containing this text.' },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
function textResult(text, isError = false) {
|
|
175
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function jsonResult(value, isError = false) {
|
|
179
|
+
return textResult(JSON.stringify(value, null, 2), isError);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function errorResult(message) {
|
|
183
|
+
return textResult(`Error: ${message}`, true);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function positiveInteger(value, fallback) {
|
|
187
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadContext(args, environment) {
|
|
191
|
+
const root = args.path ? path.resolve(args.path) : process.cwd();
|
|
192
|
+
const registry = loadRecipeRegistry({ includeUser: true });
|
|
193
|
+
const config = environment.config || loadConfig();
|
|
194
|
+
const project = detectProject(root, { registry });
|
|
195
|
+
return { project, config, registry, trustFile: environment.trustFile };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runCaptured(commands, args, defaultTimeout, extra = {}) {
|
|
199
|
+
const preview = formatSequence(commands);
|
|
200
|
+
if (args.dryRun) return jsonResult({ dryRun: true, command: preview, ...extra });
|
|
201
|
+
const timeoutMs = positiveInteger(args.timeoutMs, defaultTimeout);
|
|
202
|
+
const maxOutputBytes = Number.isInteger(args.maxOutputBytes) && args.maxOutputBytes >= 0
|
|
203
|
+
? args.maxOutputBytes
|
|
204
|
+
: MAX_OUTPUT_BYTES;
|
|
205
|
+
const result = await runSequenceCaptured(commands, { timeoutMs, maxOutputBytes });
|
|
206
|
+
return jsonResult({
|
|
207
|
+
ok: result.ok,
|
|
208
|
+
exitCode: result.exitCode,
|
|
209
|
+
command: preview,
|
|
210
|
+
...(result.failedCommand ? { failedCommand: formatSequence([result.failedCommand]) } : {}),
|
|
211
|
+
timedOut: Boolean(result.timedOut),
|
|
212
|
+
outputTruncated: Boolean(result.outputTruncated),
|
|
213
|
+
output: result.output || '',
|
|
214
|
+
...extra,
|
|
215
|
+
}, !result.ok);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function lifecycleCommands(args, environment) {
|
|
219
|
+
const { action } = args;
|
|
220
|
+
if (!LIFECYCLE_COMMANDS.includes(action)) {
|
|
221
|
+
throw new Error(`Unknown action "${action}". Valid: ${LIFECYCLE_COMMANDS.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
const { project, config, trustFile } = loadContext(args, environment);
|
|
224
|
+
const commands = action === 'open'
|
|
225
|
+
? buildOpenCommands(project, undefined, config, loadOpenerRegistry({ includeUser: true }))
|
|
226
|
+
: buildLifecycleCommands(action, project, { config, filter: args.filter, fix: args.fix, trustFile });
|
|
227
|
+
return { commands, project };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function scaffoldArguments(args, definitions) {
|
|
231
|
+
const { framework, name } = args;
|
|
232
|
+
if (!definitions[framework]) {
|
|
233
|
+
throw new Error(`Unsupported framework: ${framework}. Use list_frameworks.`);
|
|
234
|
+
}
|
|
235
|
+
validateProjectName(name);
|
|
236
|
+
const argv = [framework, name];
|
|
237
|
+
if (args.typescript !== undefined) argv.push(args.typescript ? '--typescript' : '--javascript');
|
|
238
|
+
if (args.packageManager !== undefined) {
|
|
239
|
+
if (!PACKAGE_MANAGERS.includes(args.packageManager)) {
|
|
240
|
+
throw new Error(`packageManager must be one of: ${PACKAGE_MANAGERS.join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
argv.push(`--${args.packageManager}`);
|
|
243
|
+
}
|
|
244
|
+
if (args.git !== undefined) argv.push(args.git ? '--git' : '--no-git');
|
|
245
|
+
if (args.eslint !== undefined) argv.push(args.eslint ? '--eslint' : '--no-eslint');
|
|
246
|
+
if (args.install === false) argv.push('--no-install');
|
|
247
|
+
if (args.python !== undefined) {
|
|
248
|
+
if (typeof args.python !== 'string' || !args.python || args.python.startsWith('-')) {
|
|
249
|
+
throw new Error('python must be an executable name or path.');
|
|
250
|
+
}
|
|
251
|
+
argv.push('--python', args.python);
|
|
252
|
+
}
|
|
253
|
+
for (const [key, value] of Object.entries(args.values || {})) {
|
|
254
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key) || typeof value !== 'string' || !value) {
|
|
255
|
+
throw new Error(`Invalid recipe input: ${key}`);
|
|
256
|
+
}
|
|
257
|
+
argv.push('--set', `${key}=${value}`);
|
|
258
|
+
}
|
|
259
|
+
return argv;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function scaffoldProject(args) {
|
|
263
|
+
const definitions = getRecipeDefinitions(loadRecipeRegistry({ includeUser: true }));
|
|
264
|
+
const argv = scaffoldArguments(args, definitions);
|
|
265
|
+
const directory = args.directory ? path.resolve(args.directory) : process.cwd();
|
|
266
|
+
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
|
|
267
|
+
throw new Error(`Directory does not exist: ${directory}`);
|
|
268
|
+
}
|
|
269
|
+
const dryRun = args.dryRun !== false;
|
|
270
|
+
if (dryRun) argv.push('--dry-run');
|
|
271
|
+
const command = { executable: process.execPath, args: [DEV_BINARY, ...argv], cwd: directory };
|
|
272
|
+
const result = await runSequenceCaptured([command], {
|
|
273
|
+
timeoutMs: positiveInteger(args.timeoutMs, SCAFFOLD_TIMEOUT_MS),
|
|
274
|
+
maxOutputBytes: MAX_OUTPUT_BYTES,
|
|
275
|
+
});
|
|
276
|
+
return jsonResult({
|
|
277
|
+
ok: result.ok,
|
|
278
|
+
exitCode: result.exitCode,
|
|
279
|
+
dryRun,
|
|
280
|
+
command: `dev ${argv.join(' ')}`,
|
|
281
|
+
projectPath: path.join(directory, args.name),
|
|
282
|
+
timedOut: Boolean(result.timedOut),
|
|
283
|
+
output: result.output || '',
|
|
284
|
+
}, !result.ok);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function describeFramework(name, definition) {
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
description: definition.description,
|
|
291
|
+
source: definition.source || 'built-in',
|
|
292
|
+
capabilities: definition.capabilities,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function bootDevice(args, environment) {
|
|
297
|
+
const processes = environment.processes || defaultProcesses;
|
|
298
|
+
const { platform, name } = args;
|
|
299
|
+
if (!['android', 'ios'].includes(platform)) throw new Error('platform must be android or ios.');
|
|
300
|
+
if (typeof name !== 'string' || !name || name.startsWith('-')) {
|
|
301
|
+
throw new Error('name must be a device name or identifier.');
|
|
302
|
+
}
|
|
303
|
+
if (platform === 'android') {
|
|
304
|
+
const commands = buildAndroidCommands(name, { coldBoot: Boolean(args.coldBoot) });
|
|
305
|
+
return jsonResult({ ...processes.start({ label: `android:${name}`, commands }) });
|
|
306
|
+
}
|
|
307
|
+
return runCaptured(buildIOSBootCommands(name), {}, DEFAULT_TIMEOUT_MS, { platform, name });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const HANDLERS = {
|
|
311
|
+
inspect_project(args, environment) {
|
|
312
|
+
const { project, config } = loadContext(args, environment);
|
|
313
|
+
return jsonResult(inspectProject(
|
|
314
|
+
project,
|
|
315
|
+
config,
|
|
316
|
+
loadOpenerRegistry({ includeUser: true }),
|
|
317
|
+
{ trustFile: environment.trustFile }
|
|
318
|
+
));
|
|
319
|
+
},
|
|
320
|
+
run_lifecycle(args, environment) {
|
|
321
|
+
const { commands } = lifecycleCommands(args, environment);
|
|
322
|
+
return runCaptured(commands, args, args.action === 'run' ? RUN_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
|
|
323
|
+
},
|
|
324
|
+
run_task(args, environment) {
|
|
325
|
+
const { project, config, trustFile } = loadContext(args, environment);
|
|
326
|
+
if (typeof args.name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:_.-]*$/.test(args.name)) {
|
|
327
|
+
throw new Error('Invalid task name.');
|
|
328
|
+
}
|
|
329
|
+
const commands = buildTaskCommands(project, args.name, { config, filter: args.filter, trustFile });
|
|
330
|
+
return runCaptured(commands, args, DEFAULT_TIMEOUT_MS);
|
|
331
|
+
},
|
|
332
|
+
start_process(args, environment) {
|
|
333
|
+
if (args.action === 'open') throw new Error('open cannot run as a background process.');
|
|
334
|
+
const { commands, project } = lifecycleCommands(args, environment);
|
|
335
|
+
const processes = environment.processes || defaultProcesses;
|
|
336
|
+
return jsonResult(processes.start({ label: `${args.action}:${project.name}`, commands }));
|
|
337
|
+
},
|
|
338
|
+
get_process_logs(args, environment) {
|
|
339
|
+
return jsonResult((environment.processes || defaultProcesses).logs(args.id, { tail: args.tail }));
|
|
340
|
+
},
|
|
341
|
+
async stop_process(args, environment) {
|
|
342
|
+
return jsonResult(await (environment.processes || defaultProcesses).stop(args.id));
|
|
343
|
+
},
|
|
344
|
+
list_processes(args, environment) {
|
|
345
|
+
return jsonResult((environment.processes || defaultProcesses).list());
|
|
346
|
+
},
|
|
347
|
+
scaffold_project: (args) => scaffoldProject(args),
|
|
348
|
+
list_frameworks() {
|
|
349
|
+
const definitions = getRecipeDefinitions(loadRecipeRegistry({ includeUser: true }));
|
|
350
|
+
return jsonResult(Object.entries(definitions).map(([name, definition]) => describeFramework(name, definition)));
|
|
351
|
+
},
|
|
352
|
+
doctor(args, environment) {
|
|
353
|
+
const config = environment.config || loadConfig();
|
|
354
|
+
const tools = checkTools(config).map(({ name, executable, required, found, path: toolPath, version, hint }) => ({
|
|
355
|
+
name, executable, required: Boolean(required), found, path: toolPath, version, hint,
|
|
356
|
+
}));
|
|
357
|
+
return jsonResult({ ready: tools.every((tool) => !tool.required || tool.found), tools });
|
|
358
|
+
},
|
|
359
|
+
list_devices() {
|
|
360
|
+
return jsonResult({ android: getAndroidDevices(), ios: getIOSDevices() });
|
|
361
|
+
},
|
|
362
|
+
boot_device: bootDevice,
|
|
363
|
+
get_history(args) {
|
|
364
|
+
const limit = Math.min(positiveInteger(args.limit, 10), 50);
|
|
365
|
+
return jsonResult(filterHistory(readHistory(), args).slice(0, limit).map((entry) => ({
|
|
366
|
+
command: entry.command,
|
|
367
|
+
projectName: entry.projectName,
|
|
368
|
+
projectPath: entry.projectPath,
|
|
369
|
+
projectType: entry.projectType,
|
|
370
|
+
commandLine: entry.commandLine,
|
|
371
|
+
executedAt: entry.executedAt,
|
|
372
|
+
})));
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
async function callTool(name, args = {}, environment = {}) {
|
|
377
|
+
if (!Object.hasOwn(HANDLERS, name)) return errorResult(`Unknown tool: ${name}`);
|
|
378
|
+
try {
|
|
379
|
+
return await HANDLERS[name](args, environment);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return errorResult(error.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
TOOLS,
|
|
387
|
+
callTool,
|
|
388
|
+
defaultProcesses,
|
|
389
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "vscode",
|
|
4
|
+
"description": "Visual Studio Code",
|
|
5
|
+
"target": "project",
|
|
6
|
+
"platforms": {
|
|
7
|
+
"darwin": { "executable": "open", "args": ["-a", "Visual Studio Code", "{target}"] },
|
|
8
|
+
"linux": { "executable": "code", "args": ["{target}"] },
|
|
9
|
+
"win32": { "executable": "code", "args": ["{target}"] }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "antigravity",
|
|
14
|
+
"description": "Google Antigravity IDE",
|
|
15
|
+
"target": "project",
|
|
16
|
+
"platforms": {
|
|
17
|
+
"darwin": { "executable": "open", "args": ["-a", "Antigravity IDE", "{target}"] }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "cursor",
|
|
22
|
+
"description": "Cursor",
|
|
23
|
+
"target": "project",
|
|
24
|
+
"platforms": {
|
|
25
|
+
"darwin": { "executable": "open", "args": ["-a", "Cursor", "{target}"] },
|
|
26
|
+
"linux": { "executable": "cursor", "args": ["{target}"] },
|
|
27
|
+
"win32": { "executable": "cursor", "args": ["{target}"] }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "intellij",
|
|
32
|
+
"description": "IntelliJ IDEA",
|
|
33
|
+
"target": "project",
|
|
34
|
+
"platforms": {
|
|
35
|
+
"darwin": { "executable": "open", "args": ["-a", "IntelliJ IDEA", "{target}"] },
|
|
36
|
+
"linux": { "executable": "idea", "args": ["{target}"] },
|
|
37
|
+
"win32": { "executable": "idea64.exe", "args": ["{target}"] }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "pycharm",
|
|
42
|
+
"description": "PyCharm",
|
|
43
|
+
"target": "project",
|
|
44
|
+
"platforms": {
|
|
45
|
+
"darwin": { "executable": "open", "args": ["-a", "PyCharm", "{target}"] },
|
|
46
|
+
"linux": { "executable": "pycharm", "args": ["{target}"] },
|
|
47
|
+
"win32": { "executable": "pycharm64.exe", "args": ["{target}"] }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "webstorm",
|
|
52
|
+
"description": "WebStorm",
|
|
53
|
+
"target": "project",
|
|
54
|
+
"platforms": {
|
|
55
|
+
"darwin": { "executable": "open", "args": ["-a", "WebStorm", "{target}"] },
|
|
56
|
+
"linux": { "executable": "webstorm", "args": ["{target}"] },
|
|
57
|
+
"win32": { "executable": "webstorm64.exe", "args": ["{target}"] }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "android_studio",
|
|
62
|
+
"aliases": ["android"],
|
|
63
|
+
"description": "Android Studio native project",
|
|
64
|
+
"target": "android",
|
|
65
|
+
"platforms": {
|
|
66
|
+
"darwin": { "executable": "open", "args": ["-a", "Android Studio", "{target}"] },
|
|
67
|
+
"linux": { "executable": "studio", "args": ["{target}"] },
|
|
68
|
+
"win32": { "executable": "studio64.exe", "args": ["{target}"] }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "xcode",
|
|
73
|
+
"aliases": ["ios"],
|
|
74
|
+
"description": "Xcode workspace or project",
|
|
75
|
+
"target": "xcode",
|
|
76
|
+
"platforms": {
|
|
77
|
+
"darwin": { "executable": "open", "args": ["{target}"] }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { findXcodeContainer } = require('../projects/detect');
|
|
6
|
+
const { createCommand } = require('../runner/command');
|
|
7
|
+
const { interpolate: _interpolate } = require('../utils/interpolate');
|
|
8
|
+
|
|
9
|
+
function interpolate(value, context) {
|
|
10
|
+
return _interpolate(value, context, 'opener placeholder');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveTarget(opener, project, options = {}) {
|
|
14
|
+
const fsImpl = options.fsImpl || fs;
|
|
15
|
+
if (opener.target === 'android') {
|
|
16
|
+
const androidRoot = path.join(project.root, 'android');
|
|
17
|
+
try {
|
|
18
|
+
if (fsImpl.statSync(androidRoot).isDirectory()) return androidRoot;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error.code !== 'ENOENT') throw error;
|
|
21
|
+
}
|
|
22
|
+
return project.root;
|
|
23
|
+
}
|
|
24
|
+
if (opener.target === 'xcode') {
|
|
25
|
+
if ((options.platform || process.platform) !== 'darwin') {
|
|
26
|
+
throw new Error('Xcode is only available on macOS.');
|
|
27
|
+
}
|
|
28
|
+
const iosRoot = project.xcode ? project.root : path.join(project.root, 'ios');
|
|
29
|
+
const xcode = project.xcode || findXcodeContainer(iosRoot, fsImpl);
|
|
30
|
+
if (!xcode) throw new Error('No Xcode workspace or project was detected.');
|
|
31
|
+
return path.join(iosRoot, xcode.file);
|
|
32
|
+
}
|
|
33
|
+
return project.root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildOpenerCommands(opener, project, options = {}) {
|
|
37
|
+
const platform = options.platform || process.platform;
|
|
38
|
+
const definition = opener.platforms[platform];
|
|
39
|
+
if (!definition) {
|
|
40
|
+
throw new Error(`${opener.description} is not configured for platform: ${platform}.`);
|
|
41
|
+
}
|
|
42
|
+
const target = resolveTarget(opener, project, options);
|
|
43
|
+
const context = { target, root: project.root, platform };
|
|
44
|
+
return [createCommand(
|
|
45
|
+
interpolate(definition.executable, context),
|
|
46
|
+
definition.args.map((arg) => interpolate(arg, context)),
|
|
47
|
+
definition.cwd === undefined ? {} : { cwd: interpolate(definition.cwd, context) }
|
|
48
|
+
)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { buildOpenerCommands, interpolate, resolveTarget };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { buildOpenerCommands } = require('./engine');
|
|
4
|
+
const {
|
|
5
|
+
createOpenerRegistry,
|
|
6
|
+
loadBuiltInOpeners,
|
|
7
|
+
loadOpenerDirectory,
|
|
8
|
+
loadOpenerFile,
|
|
9
|
+
loadOpenerRegistry,
|
|
10
|
+
resolveOpener,
|
|
11
|
+
} = require('./loader');
|
|
12
|
+
const { validateOpener } = require('./schema');
|
|
13
|
+
|
|
14
|
+
const builtInOpenerRegistry = loadOpenerRegistry({ includeUser: false });
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
buildOpenerCommands,
|
|
18
|
+
builtInOpenerRegistry,
|
|
19
|
+
createOpenerRegistry,
|
|
20
|
+
loadBuiltInOpeners,
|
|
21
|
+
loadOpenerDirectory,
|
|
22
|
+
loadOpenerFile,
|
|
23
|
+
loadOpenerRegistry,
|
|
24
|
+
resolveOpener,
|
|
25
|
+
validateOpener,
|
|
26
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const builtIns = require('./built-ins.json');
|
|
7
|
+
const { validateOpener } = require('./schema');
|
|
8
|
+
|
|
9
|
+
function attachSource(opener, source, baseDir) {
|
|
10
|
+
return { target: 'project', ...opener, source, baseDir };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadBuiltInOpeners() {
|
|
14
|
+
return builtIns.map((opener) => attachSource(validateOpener(opener), 'built-in', __dirname));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadOpenerFile(file, source, fsImpl = fs) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(`Unable to read opener ${file}: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
const openers = Array.isArray(parsed) ? parsed : [parsed];
|
|
25
|
+
return openers.map((opener) => {
|
|
26
|
+
try {
|
|
27
|
+
return attachSource(validateOpener(opener), source, path.dirname(file));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Invalid opener ${file}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadOpenerDirectory(directory, source, options = {}) {
|
|
35
|
+
const fsImpl = options.fsImpl || fs;
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = fsImpl.readdirSync(directory, { withFileTypes: true });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') return [];
|
|
41
|
+
throw new Error(`Unable to read opener directory ${directory}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
return entries
|
|
44
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
45
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
46
|
+
.flatMap((entry) => loadOpenerFile(path.join(directory, entry.name), source, fsImpl));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createOpenerRegistry(openers) {
|
|
50
|
+
const registry = new Map();
|
|
51
|
+
for (const opener of openers) registry.set(opener.name, opener);
|
|
52
|
+
|
|
53
|
+
const names = new Map([...registry.keys()].map((name) => [name, name]));
|
|
54
|
+
for (const opener of registry.values()) {
|
|
55
|
+
for (const alias of opener.aliases || []) {
|
|
56
|
+
const owner = names.get(alias);
|
|
57
|
+
if (owner && owner !== opener.name) {
|
|
58
|
+
throw new Error(`Opener alias ${alias} conflicts with opener: ${owner}`);
|
|
59
|
+
}
|
|
60
|
+
names.set(alias, opener.name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return registry;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadOpenerRegistry(options = {}) {
|
|
67
|
+
const home = options.home || os.homedir();
|
|
68
|
+
const userDirectory = options.userDirectory || path.join(home, '.devcmd', 'openers');
|
|
69
|
+
const builtInOpeners = options.builtIns || loadBuiltInOpeners();
|
|
70
|
+
const userOpeners = options.includeUser === false
|
|
71
|
+
? []
|
|
72
|
+
: loadOpenerDirectory(userDirectory, 'user', options);
|
|
73
|
+
return createOpenerRegistry([...builtInOpeners, ...userOpeners]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveOpener(registry, name) {
|
|
77
|
+
if (!name) return undefined;
|
|
78
|
+
if (registry.has(name)) return registry.get(name);
|
|
79
|
+
return [...registry.values()].find((opener) => (opener.aliases || []).includes(name));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
createOpenerRegistry,
|
|
84
|
+
loadBuiltInOpeners,
|
|
85
|
+
loadOpenerDirectory,
|
|
86
|
+
loadOpenerFile,
|
|
87
|
+
loadOpenerRegistry,
|
|
88
|
+
resolveOpener,
|
|
89
|
+
};
|