@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,73 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { buildLifecycleCommands } = require('../commands/lifecycle');
|
|
5
|
+
const { detectProject } = require('../projects/detect');
|
|
6
|
+
const { executeCommands } = require('../runtime/execute');
|
|
7
|
+
const { runSequence } = require('../runner/process');
|
|
8
|
+
|
|
9
|
+
function parseRepo(input) {
|
|
10
|
+
if (/^https?:\/\//.test(input) || /^git@/.test(input)) {
|
|
11
|
+
const name = input.split('/').pop().replace(/\.git$/, '');
|
|
12
|
+
return { url: input, name };
|
|
13
|
+
}
|
|
14
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(input)) {
|
|
15
|
+
const name = input.split('/')[1];
|
|
16
|
+
return { url: `https://github.com/${input}`, name };
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Invalid repository: ${input}. Use owner/repo or a full URL.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function handleClone(context) {
|
|
22
|
+
const { p, pc, positionals, options, config } = context;
|
|
23
|
+
if (positionals.length === 0 || positionals.length > 2) {
|
|
24
|
+
throw new Error('Usage: dev clone <repo> [directory]');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { url, name } = parseRepo(positionals[0]);
|
|
28
|
+
const targetDir = path.resolve(positionals[1] || name);
|
|
29
|
+
const cloneCommand = { executable: 'git', args: ['clone', url, targetDir] };
|
|
30
|
+
|
|
31
|
+
p.log.step(`Cloning: ${pc.yellow(url)}`);
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
p.log.step(`Dry run: git clone ${url} ${targetDir}`);
|
|
34
|
+
p.outro('No command was executed.');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cloneResult = await runSequence([cloneCommand]);
|
|
39
|
+
if (!cloneResult.ok) {
|
|
40
|
+
p.outro(`${pc.red('✖')} Clone failed.`);
|
|
41
|
+
return cloneResult.exitCode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let project;
|
|
45
|
+
try {
|
|
46
|
+
project = detectProject(targetDir, { registry: context.recipeRegistry });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
p.log.warn(`Cloned successfully, but project detection failed: ${error.message}`);
|
|
49
|
+
p.outro(`${pc.green('✔')} Cloned to ${targetDir}`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
p.log.step(`Detected: ${pc.cyan(project.type)}`);
|
|
54
|
+
|
|
55
|
+
let installCommands;
|
|
56
|
+
try {
|
|
57
|
+
installCommands = buildLifecycleCommands('install', project, { config });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
p.log.warn(`Cloned successfully, but install is not supported: ${error.message}`);
|
|
60
|
+
p.outro(`${pc.green('✔')} Cloned to ${targetDir}`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return executeCommands(p, pc, installCommands, {
|
|
65
|
+
command: 'install',
|
|
66
|
+
projectName: project.name,
|
|
67
|
+
projectPath: project.root,
|
|
68
|
+
projectType: project.type,
|
|
69
|
+
config,
|
|
70
|
+
}, options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { handleClone, parseRepo };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { DEFAULT_DEVICES } = require('../config');
|
|
4
|
+
const { buildAndroidCommands, buildIOSBootCommands, buildIOSShutdownCommands } = require('../commands/devices');
|
|
5
|
+
const { formatDevice, getAndroidDevices, getIOSDevices } = require('../devices');
|
|
6
|
+
const { executeCommands } = require('../runtime/execute');
|
|
7
|
+
|
|
8
|
+
function handleDeviceList(context) {
|
|
9
|
+
const { p, pc } = context;
|
|
10
|
+
const android = getAndroidDevices();
|
|
11
|
+
const ios = getIOSDevices();
|
|
12
|
+
p.note([
|
|
13
|
+
pc.bold('Android'),
|
|
14
|
+
...(android.length ? android.map((device) => ` ${formatDevice(device)}`) : [' No devices found']),
|
|
15
|
+
'',
|
|
16
|
+
pc.bold('iOS'),
|
|
17
|
+
...(ios.length ? ios.map((device) => ` ${formatDevice(device)}`) : [' No devices found']),
|
|
18
|
+
].join('\n'), 'Available devices');
|
|
19
|
+
p.outro(`${android.length + ios.length} device(s) found.`);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function handleDeviceCommand(context) {
|
|
24
|
+
const { p, pc, command, options } = context;
|
|
25
|
+
if (command === 'ios' && options.shutdownAll) {
|
|
26
|
+
return executeCommands(p, pc, buildIOSShutdownCommands(), { command: 'ios', action: 'shutdown-all' }, {
|
|
27
|
+
...options,
|
|
28
|
+
requirements: ['xcrun'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const availableDevices = command === 'android' ? getAndroidDevices() : getIOSDevices();
|
|
33
|
+
let target = options.target;
|
|
34
|
+
if (!target && availableDevices.length > 0) {
|
|
35
|
+
const preferred = availableDevices.find((device) => device.name === DEFAULT_DEVICES[command]);
|
|
36
|
+
const selected = await p.select({
|
|
37
|
+
message: 'Select a device:',
|
|
38
|
+
options: availableDevices.map((device) => ({
|
|
39
|
+
value: device.id,
|
|
40
|
+
label: formatDevice(device),
|
|
41
|
+
hint: preferred?.id === device.id ? 'default' : '',
|
|
42
|
+
})),
|
|
43
|
+
initialValue: preferred?.id || availableDevices[0].id,
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(selected)) {
|
|
46
|
+
p.cancel('Operation cancelled.');
|
|
47
|
+
return 130;
|
|
48
|
+
}
|
|
49
|
+
target = selected;
|
|
50
|
+
}
|
|
51
|
+
if (!target) {
|
|
52
|
+
const result = await p.text({
|
|
53
|
+
message: 'No device was detected. Enter a device name or identifier:',
|
|
54
|
+
placeholder: DEFAULT_DEVICES[command],
|
|
55
|
+
defaultValue: DEFAULT_DEVICES[command],
|
|
56
|
+
validate: (value) => (!value ? 'Device name is required.' : undefined),
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(result)) {
|
|
59
|
+
p.cancel('Operation cancelled.');
|
|
60
|
+
return 130;
|
|
61
|
+
}
|
|
62
|
+
target = result;
|
|
63
|
+
}
|
|
64
|
+
if (command === 'ios') {
|
|
65
|
+
const match = availableDevices.find((device) => device.id === target || device.name === target);
|
|
66
|
+
target = match?.id || target;
|
|
67
|
+
}
|
|
68
|
+
const commands = command === 'android' ? buildAndroidCommands(target, options) : buildIOSBootCommands(target);
|
|
69
|
+
return executeCommands(p, pc, commands, { command, target, options }, {
|
|
70
|
+
...options,
|
|
71
|
+
requirements: command === 'android' ? ['emulator'] : ['open', 'xcrun'],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
handleDeviceCommand,
|
|
77
|
+
handleDeviceList,
|
|
78
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { HISTORY_FILE, frameworkDefinitions } = require('../config');
|
|
5
|
+
const { buildAndroidCommands, buildIOSBootCommands, buildIOSShutdownCommands } = require('../commands/devices');
|
|
6
|
+
const { buildFrameworkCommands, builtInRegistry } = require('../commands/frameworks');
|
|
7
|
+
const { filterHistory, getHistoryDisplay, readHistory } = require('../commands/history');
|
|
8
|
+
const { isCommandSequence } = require('../runner/command');
|
|
9
|
+
const { executeCommands } = require('../runtime/execute');
|
|
10
|
+
|
|
11
|
+
function rebuildHistoryCommands(entry, registry = builtInRegistry, definitions = frameworkDefinitions) {
|
|
12
|
+
if (isCommandSequence(entry.commands)) return entry.commands;
|
|
13
|
+
if (definitions[entry.command] && entry.target) {
|
|
14
|
+
return buildFrameworkCommands(entry.command, entry.target, entry.options || {}, registry);
|
|
15
|
+
}
|
|
16
|
+
if (entry.command === 'android' && entry.target) return buildAndroidCommands(entry.target, entry.options || {});
|
|
17
|
+
if (entry.command === 'ios' && entry.action === 'shutdown-all') return buildIOSShutdownCommands();
|
|
18
|
+
if (entry.command === 'ios' && entry.target) return buildIOSBootCommands(entry.target);
|
|
19
|
+
throw new Error('This legacy history entry cannot be replayed safely. Run the command again directly.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleHistory(context) {
|
|
23
|
+
const { p, options } = context;
|
|
24
|
+
|
|
25
|
+
if (options.clear) {
|
|
26
|
+
try { fs.unlinkSync(HISTORY_FILE); } catch {}
|
|
27
|
+
p.outro('History cleared.');
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const history = filterHistory(readHistory(), options).slice(0, 10);
|
|
32
|
+
|
|
33
|
+
if (history.length === 0) {
|
|
34
|
+
p.outro(options.project || options.search
|
|
35
|
+
? 'No history entries match the filter.'
|
|
36
|
+
: 'No successful commands in history yet.');
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
p.note(
|
|
40
|
+
history.map((entry, index) => `${index + 1}. ${getHistoryDisplay(entry)}\n ${entry.executedAt}`).join('\n'),
|
|
41
|
+
'Recent commands'
|
|
42
|
+
);
|
|
43
|
+
p.outro(`History file: ${HISTORY_FILE}`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleAgain(context) {
|
|
48
|
+
const { p, pc, positionals, options } = context;
|
|
49
|
+
const index = positionals[0] !== undefined ? parseInt(positionals[0], 10) : 1;
|
|
50
|
+
if (isNaN(index) || index < 1) {
|
|
51
|
+
throw new Error('Usage: dev again [n] — n must be a positive integer.');
|
|
52
|
+
}
|
|
53
|
+
const history = readHistory();
|
|
54
|
+
const entry = history[index - 1];
|
|
55
|
+
if (!entry) {
|
|
56
|
+
throw new Error(`No command at position ${index}. History has ${history.length} entries.`);
|
|
57
|
+
}
|
|
58
|
+
return executeCommands(
|
|
59
|
+
p,
|
|
60
|
+
pc,
|
|
61
|
+
rebuildHistoryCommands(
|
|
62
|
+
entry,
|
|
63
|
+
context.recipeRegistry || builtInRegistry,
|
|
64
|
+
context.frameworkDefinitions || frameworkDefinitions
|
|
65
|
+
),
|
|
66
|
+
{ command: 'again', source: entry.command },
|
|
67
|
+
options
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
handleAgain,
|
|
73
|
+
handleHistory,
|
|
74
|
+
rebuildHistoryCommands,
|
|
75
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { filterHistory, readHistory } = require('../commands/history');
|
|
4
|
+
const { inspectProject } = require('../commands/inspect');
|
|
5
|
+
const { getAndroidDevices, getIOSDevices } = require('../devices');
|
|
6
|
+
const { detectProject } = require('../projects/detect');
|
|
7
|
+
const { getExistingProjects } = require('../projects/recent');
|
|
8
|
+
const { checkTools } = require('../runner/check');
|
|
9
|
+
|
|
10
|
+
function describeRegistry(registry) {
|
|
11
|
+
return [...registry.values()].map((entry) => ({
|
|
12
|
+
name: entry.name,
|
|
13
|
+
description: entry.description,
|
|
14
|
+
source: entry.source,
|
|
15
|
+
...(entry.aliases?.length ? { aliases: entry.aliases } : {}),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectJson(context) {
|
|
20
|
+
const { command, positionals, options, config } = context;
|
|
21
|
+
if (command === 'inspect') {
|
|
22
|
+
if (positionals.length > 1) throw new Error('Usage: dev inspect [path] --json');
|
|
23
|
+
const project = detectProject(positionals[0] || process.cwd(), { registry: context.recipeRegistry });
|
|
24
|
+
return { data: inspectProject(project, config, context.openerRegistry), exitCode: 0 };
|
|
25
|
+
}
|
|
26
|
+
if (command === 'doctor') {
|
|
27
|
+
const tools = checkTools(config).map(({ name, executable, required, found, path, version, hint }) => ({
|
|
28
|
+
name, executable, required: Boolean(required), found, path, version, hint,
|
|
29
|
+
}));
|
|
30
|
+
const ready = tools.every((tool) => !tool.required || tool.found);
|
|
31
|
+
return { data: { ready, tools }, exitCode: ready ? 0 : 1 };
|
|
32
|
+
}
|
|
33
|
+
if (command === 'projects') return { data: getExistingProjects(), exitCode: 0 };
|
|
34
|
+
if (command === 'history') {
|
|
35
|
+
const entries = filterHistory(readHistory(), options).map((entry) => ({
|
|
36
|
+
command: entry.command,
|
|
37
|
+
projectName: entry.projectName,
|
|
38
|
+
projectPath: entry.projectPath,
|
|
39
|
+
projectType: entry.projectType,
|
|
40
|
+
commandLine: entry.commandLine,
|
|
41
|
+
executedAt: entry.executedAt,
|
|
42
|
+
}));
|
|
43
|
+
return { data: entries, exitCode: 0 };
|
|
44
|
+
}
|
|
45
|
+
if (command === 'recipes') return { data: describeRegistry(context.recipeRegistry), exitCode: 0 };
|
|
46
|
+
if (command === 'openers') return { data: describeRegistry(context.openerRegistry), exitCode: 0 };
|
|
47
|
+
if (command === 'devices') {
|
|
48
|
+
return { data: { android: getAndroidDevices(), ios: getIOSDevices() }, exitCode: 0 };
|
|
49
|
+
}
|
|
50
|
+
if (command === 'config') return { data: config, exitCode: 0 };
|
|
51
|
+
throw new Error(`--json is not supported by ${command}.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runJson(context) {
|
|
55
|
+
try {
|
|
56
|
+
const { data, exitCode } = collectJson(context);
|
|
57
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
58
|
+
return exitCode;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
process.stderr.write(`${JSON.stringify({ error: error.message })}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
collectJson,
|
|
67
|
+
runJson,
|
|
68
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const { buildLifecycleCommands, buildOpenCommands } = require('../commands/lifecycle');
|
|
4
|
+
const { inspectProject } = require('../commands/inspect');
|
|
5
|
+
const { detectProject } = require('../projects/detect');
|
|
6
|
+
const { getExistingProjects } = require('../projects/recent');
|
|
7
|
+
const { executeCommands } = require('../runtime/execute');
|
|
8
|
+
const { builtInOpenerRegistry, resolveOpener } = require('../openers');
|
|
9
|
+
const { LIFECYCLE_COMMANDS } = require('../constants');
|
|
10
|
+
|
|
11
|
+
function resolveLifecycleTarget(
|
|
12
|
+
command,
|
|
13
|
+
positionals,
|
|
14
|
+
options = {},
|
|
15
|
+
openerRegistry = builtInOpenerRegistry
|
|
16
|
+
) {
|
|
17
|
+
if (command === 'open' && options.list) {
|
|
18
|
+
if (positionals.length > 0) throw new Error('Usage: dev open --list');
|
|
19
|
+
return { listOpeners: true };
|
|
20
|
+
}
|
|
21
|
+
if (command === 'open' && positionals[0] === 'recent') {
|
|
22
|
+
if (positionals.length > 1) throw new Error('Usage: dev open recent');
|
|
23
|
+
const recent = getExistingProjects()[0];
|
|
24
|
+
if (!recent) throw new Error('No recent project is available.');
|
|
25
|
+
return { projectPath: recent.path };
|
|
26
|
+
}
|
|
27
|
+
if (command === 'open') {
|
|
28
|
+
if (options.opener) {
|
|
29
|
+
if (positionals.length > 1) throw new Error('Usage: dev open [path] --with <opener>');
|
|
30
|
+
return { openerName: options.opener, projectPath: positionals[0] || process.cwd() };
|
|
31
|
+
}
|
|
32
|
+
const opener = resolveOpener(openerRegistry, positionals[0]);
|
|
33
|
+
if (opener) {
|
|
34
|
+
if (positionals.length > 2) throw new Error(`Usage: dev open ${positionals[0]} [path]`);
|
|
35
|
+
return { openerName: positionals[0], projectPath: positionals[1] || process.cwd() };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (positionals.length > 1) throw new Error(`Usage: dev ${command} [path]`);
|
|
39
|
+
return { projectPath: positionals[0] || process.cwd() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handleLifecycle(context) {
|
|
43
|
+
const { p, pc, command, positionals, options, config } = context;
|
|
44
|
+
const { projectPath, openerName, listOpeners } = resolveLifecycleTarget(
|
|
45
|
+
command,
|
|
46
|
+
positionals,
|
|
47
|
+
options,
|
|
48
|
+
context.openerRegistry
|
|
49
|
+
);
|
|
50
|
+
if (listOpeners) {
|
|
51
|
+
const lines = [...context.openerRegistry.values()].map((opener) => {
|
|
52
|
+
const aliases = opener.aliases?.length ? ` aliases: ${opener.aliases.join(', ')}` : '';
|
|
53
|
+
return `${opener.name.padEnd(20)} [${opener.source}] ${opener.description}${aliases}`;
|
|
54
|
+
});
|
|
55
|
+
p.note(lines.join('\n'), 'Available openers');
|
|
56
|
+
p.outro('Use: dev open <opener> [path]');
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (options.editor && openerName) {
|
|
60
|
+
throw new Error('--editor cannot be combined with a named opener.');
|
|
61
|
+
}
|
|
62
|
+
const project = detectProject(projectPath, { registry: context.recipeRegistry });
|
|
63
|
+
const commands = command === 'open'
|
|
64
|
+
? buildOpenCommands(project, openerName, config, context.openerRegistry, { editor: options.editor })
|
|
65
|
+
: buildLifecycleCommands(command, project, { config, filter: options.filter, fix: options.fix });
|
|
66
|
+
|
|
67
|
+
return executeCommands(p, pc, commands, {
|
|
68
|
+
command,
|
|
69
|
+
projectName: project.name,
|
|
70
|
+
projectPath: project.root,
|
|
71
|
+
projectType: project.type,
|
|
72
|
+
config,
|
|
73
|
+
}, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleInspect(context) {
|
|
77
|
+
const { p, positionals, config } = context;
|
|
78
|
+
if (positionals.length > 1) throw new Error('Usage: dev inspect [path]');
|
|
79
|
+
const inspection = inspectProject(
|
|
80
|
+
detectProject(positionals[0] || process.cwd(), { registry: context.recipeRegistry }),
|
|
81
|
+
config,
|
|
82
|
+
context.openerRegistry
|
|
83
|
+
);
|
|
84
|
+
const lines = [
|
|
85
|
+
`Name: ${inspection.name}`,
|
|
86
|
+
`Type: ${inspection.type}`,
|
|
87
|
+
`Root: ${inspection.root}`,
|
|
88
|
+
];
|
|
89
|
+
if (inspection.packageManager) lines.push(`Package manager: ${inspection.packageManager}`);
|
|
90
|
+
if (inspection.xcode) lines.push(`Xcode: ${inspection.xcode.kind} ${inspection.xcode.file}`);
|
|
91
|
+
if (inspection.recipeSource) lines.push(`Recipe: ${inspection.recipeSource}`);
|
|
92
|
+
if (inspection.profile) {
|
|
93
|
+
lines.push(`Profile: ${inspection.profile.file} (${inspection.profile.trusted ? 'trusted' : 'untrusted, run dev trust'})`);
|
|
94
|
+
}
|
|
95
|
+
if (inspection.workspaces.length) {
|
|
96
|
+
lines.push(`Workspaces: ${inspection.workspaces.map((workspace) => workspace.name).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
if (inspection.tasks.length) {
|
|
99
|
+
lines.push(`Tasks: ${inspection.tasks.map((task) => task.name).join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
for (const action of inspection.actions) {
|
|
103
|
+
lines.push(
|
|
104
|
+
action.available
|
|
105
|
+
? `${action.action.padEnd(8)} ${action.command}`
|
|
106
|
+
: `${action.action.padEnd(8)} unavailable: ${action.reason}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
p.note(lines.join('\n'), 'Detected project');
|
|
110
|
+
p.outro('Inspection complete.');
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
LIFECYCLE_COMMANDS,
|
|
116
|
+
handleInspect,
|
|
117
|
+
handleLifecycle,
|
|
118
|
+
resolveLifecycleTarget,
|
|
119
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Copyright (c) 2026 bachbnt
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { PACKAGE_MANAGERS, frameworkDefinitions } = require('../config');
|
|
5
|
+
const {
|
|
6
|
+
buildFrameworkCommands,
|
|
7
|
+
builtInRegistry,
|
|
8
|
+
getFrameworkRequirements,
|
|
9
|
+
getRecipe,
|
|
10
|
+
} = require('../commands/frameworks');
|
|
11
|
+
const { executeCommands } = require('../runtime/execute');
|
|
12
|
+
const { validateFrameworkOptions, validateProjectName } = require('../utils/args');
|
|
13
|
+
|
|
14
|
+
function applyConfigDefaults(command, options, config, definitions = frameworkDefinitions) {
|
|
15
|
+
const capabilities = definitions[command].capabilities;
|
|
16
|
+
if (capabilities.packageManager && !options.packageManager) {
|
|
17
|
+
const supported = Array.isArray(capabilities.packageManager)
|
|
18
|
+
? capabilities.packageManager
|
|
19
|
+
: PACKAGE_MANAGERS;
|
|
20
|
+
options.packageManager = supported.includes(config.packageManager)
|
|
21
|
+
? config.packageManager
|
|
22
|
+
: supported[0];
|
|
23
|
+
}
|
|
24
|
+
if (capabilities.git && options.git === undefined) options.git = config.initializeGit;
|
|
25
|
+
if (capabilities.python && !options.python) options.python = config.python;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function promptFrameworkOptions(
|
|
29
|
+
p,
|
|
30
|
+
command,
|
|
31
|
+
options,
|
|
32
|
+
config,
|
|
33
|
+
definitions = frameworkDefinitions,
|
|
34
|
+
recipe = builtInRegistry.get(command)
|
|
35
|
+
) {
|
|
36
|
+
const capabilities = definitions[command].capabilities;
|
|
37
|
+
const unwrap = (value) => (p.isCancel(value) ? undefined : value);
|
|
38
|
+
|
|
39
|
+
if (capabilities.language && options.typescript === undefined) {
|
|
40
|
+
options.typescript = unwrap(await p.confirm({ message: 'Use TypeScript?', initialValue: true }));
|
|
41
|
+
if (options.typescript === undefined) return false;
|
|
42
|
+
}
|
|
43
|
+
if (capabilities.packageManager && !options.packageManager) {
|
|
44
|
+
const supported = Array.isArray(capabilities.packageManager)
|
|
45
|
+
? capabilities.packageManager
|
|
46
|
+
: PACKAGE_MANAGERS;
|
|
47
|
+
options.packageManager = unwrap(await p.select({
|
|
48
|
+
message: 'Select a package manager:',
|
|
49
|
+
options: supported.map((value) => ({ value, label: value })),
|
|
50
|
+
initialValue: supported.includes(config.packageManager) ? config.packageManager : supported[0],
|
|
51
|
+
}));
|
|
52
|
+
if (!options.packageManager) return false;
|
|
53
|
+
}
|
|
54
|
+
if (capabilities.git && options.git === undefined) {
|
|
55
|
+
options.git = unwrap(await p.confirm({
|
|
56
|
+
message: 'Initialize a Git repository?',
|
|
57
|
+
initialValue: config.initializeGit,
|
|
58
|
+
}));
|
|
59
|
+
if (options.git === undefined) return false;
|
|
60
|
+
}
|
|
61
|
+
if (capabilities.eslint && options.eslint === undefined) {
|
|
62
|
+
options.eslint = unwrap(await p.confirm({ message: 'Include ESLint?', initialValue: true }));
|
|
63
|
+
if (options.eslint === undefined) return false;
|
|
64
|
+
}
|
|
65
|
+
options.values ||= {};
|
|
66
|
+
for (const [name, input] of Object.entries(recipe?.inputs || {})) {
|
|
67
|
+
if (options.values[name] !== undefined || input.default !== undefined || !input.required) continue;
|
|
68
|
+
const value = unwrap(await p.text({
|
|
69
|
+
message: input.description || `Enter ${name}:`,
|
|
70
|
+
validate(candidate) {
|
|
71
|
+
if (!candidate?.trim()) return `${name} is required.`;
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
if (!value) return false;
|
|
76
|
+
options.values[name] = value;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function handleScaffold(context) {
|
|
82
|
+
const { p, pc, command, positionals, options, config } = context;
|
|
83
|
+
const definitions = context.frameworkDefinitions || frameworkDefinitions;
|
|
84
|
+
const recipeRegistry = context.recipeRegistry || builtInRegistry;
|
|
85
|
+
const recipe = getRecipe(command, recipeRegistry);
|
|
86
|
+
if (positionals.length > 1) throw new Error(`Unexpected argument: ${positionals[1]}`);
|
|
87
|
+
|
|
88
|
+
const interactive = !options.target;
|
|
89
|
+
let target = options.target;
|
|
90
|
+
if (!target) {
|
|
91
|
+
const result = await p.text({
|
|
92
|
+
message: 'What is your project name?',
|
|
93
|
+
placeholder: 'my-awesome-app',
|
|
94
|
+
validate(value) {
|
|
95
|
+
try {
|
|
96
|
+
validateProjectName(value);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return error.message;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (p.isCancel(result)) {
|
|
103
|
+
p.cancel('Operation cancelled.');
|
|
104
|
+
return 130;
|
|
105
|
+
}
|
|
106
|
+
target = result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
validateProjectName(target);
|
|
110
|
+
if (interactive && !(await promptFrameworkOptions(
|
|
111
|
+
p,
|
|
112
|
+
command,
|
|
113
|
+
options,
|
|
114
|
+
config,
|
|
115
|
+
definitions,
|
|
116
|
+
recipe
|
|
117
|
+
))) {
|
|
118
|
+
p.cancel('Operation cancelled.');
|
|
119
|
+
return 130;
|
|
120
|
+
}
|
|
121
|
+
applyConfigDefaults(command, options, config, definitions);
|
|
122
|
+
validateFrameworkOptions(command, options, definitions);
|
|
123
|
+
|
|
124
|
+
const commands = buildFrameworkCommands(command, target, options, recipeRegistry);
|
|
125
|
+
return executeCommands(p, pc, commands, {
|
|
126
|
+
command,
|
|
127
|
+
target,
|
|
128
|
+
options,
|
|
129
|
+
projectName: target,
|
|
130
|
+
projectPath: path.resolve(target),
|
|
131
|
+
projectType: command,
|
|
132
|
+
recipeSource: recipe.source,
|
|
133
|
+
config,
|
|
134
|
+
}, {
|
|
135
|
+
...options,
|
|
136
|
+
requirements: getFrameworkRequirements(command, options, recipeRegistry, target),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function handleInit(context) {
|
|
141
|
+
const { p } = context;
|
|
142
|
+
const definitions = context.frameworkDefinitions || frameworkDefinitions;
|
|
143
|
+
|
|
144
|
+
const framework = await p.select({
|
|
145
|
+
message: 'Select a framework:',
|
|
146
|
+
options: Object.entries(definitions).map(([value, def]) => ({
|
|
147
|
+
value,
|
|
148
|
+
label: value,
|
|
149
|
+
hint: def.description,
|
|
150
|
+
})),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (p.isCancel(framework)) {
|
|
154
|
+
p.cancel('Operation cancelled.');
|
|
155
|
+
return 130;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return handleScaffold({ ...context, command: framework });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
applyConfigDefaults,
|
|
163
|
+
handleInit,
|
|
164
|
+
handleScaffold,
|
|
165
|
+
promptFrameworkOptions,
|
|
166
|
+
};
|