@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,175 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { TRUST_FILE } = require('../config');
7
+ const { LIFECYCLE_COMMANDS } = require('../constants');
8
+ const { buildRecipeActions } = require('../recipes');
9
+ const { validateAction } = require('../recipes/schema');
10
+
11
+ const PROFILE_FILE = '.devcmd.json';
12
+ const TASK_NAME = /^[A-Za-z][A-Za-z0-9:_.-]*$/;
13
+
14
+ function validateRunActions(actions, label) {
15
+ if (!Array.isArray(actions) || actions.length === 0) {
16
+ throw new Error(`${label} must be a non-empty array of run actions.`);
17
+ }
18
+ actions.forEach((action, index) => {
19
+ validateAction(action, index, label);
20
+ if (action.type !== 'run') throw new Error(`${label}[${index}].type must be "run".`);
21
+ });
22
+ }
23
+
24
+ function validateProfile(data, file = PROFILE_FILE) {
25
+ const fail = (message) => {
26
+ throw new Error(`Invalid ${file}: ${message}`);
27
+ };
28
+ if (!data || typeof data !== 'object' || Array.isArray(data)) fail('root must be an object.');
29
+ const unknown = Object.keys(data).filter((key) => !['name', 'lifecycle', 'tasks'].includes(key));
30
+ if (unknown.length > 0) fail(`unsupported fields: ${unknown.join(', ')}.`);
31
+ if (data.name !== undefined && (typeof data.name !== 'string' || !data.name.trim())) {
32
+ fail('name must be a non-empty string.');
33
+ }
34
+ try {
35
+ for (const [action, actions] of Object.entries(data.lifecycle || {})) {
36
+ if (!LIFECYCLE_COMMANDS.includes(action)) fail(`unsupported lifecycle action: ${action}.`);
37
+ validateRunActions(actions, `lifecycle.${action}`);
38
+ }
39
+ for (const [name, task] of Object.entries(data.tasks || {})) {
40
+ if (!TASK_NAME.test(name)) fail(`invalid task name: ${name}.`);
41
+ const normalized = Array.isArray(task) ? { commands: task } : task;
42
+ if (!normalized || typeof normalized !== 'object' || Object.keys(normalized).some(
43
+ (key) => !['description', 'commands'].includes(key)
44
+ )) {
45
+ fail(`task ${name} must be an array or an object with description and commands.`);
46
+ }
47
+ if (normalized.description !== undefined && typeof normalized.description !== 'string') {
48
+ fail(`task ${name} description must be a string.`);
49
+ }
50
+ validateRunActions(normalized.commands, `tasks.${name}.commands`);
51
+ }
52
+ } catch (error) {
53
+ if (error.message.startsWith('Invalid ')) throw error;
54
+ fail(error.message);
55
+ }
56
+ return data;
57
+ }
58
+
59
+ function loadProfile(root, fsImpl = fs) {
60
+ const file = path.join(root, PROFILE_FILE);
61
+ let content;
62
+ try {
63
+ content = fsImpl.readFileSync(file, 'utf8');
64
+ } catch (error) {
65
+ if (error.code === 'ENOENT') return undefined;
66
+ throw new Error(`Unable to read ${file}: ${error.message}`);
67
+ }
68
+ let data;
69
+ try {
70
+ data = JSON.parse(content);
71
+ } catch (error) {
72
+ throw new Error(`Invalid JSON in ${file}: ${error.message}`);
73
+ }
74
+ validateProfile(data, file);
75
+ return {
76
+ file,
77
+ hash: crypto.createHash('sha256').update(content).digest('hex'),
78
+ name: data.name,
79
+ lifecycle: data.lifecycle || {},
80
+ tasks: Object.fromEntries(Object.entries(data.tasks || {}).map(([name, task]) => [
81
+ name,
82
+ Array.isArray(task) ? { commands: task } : task,
83
+ ])),
84
+ };
85
+ }
86
+
87
+ function readTrusted(trustFile, fsImpl = fs) {
88
+ try {
89
+ const data = JSON.parse(fsImpl.readFileSync(trustFile, 'utf8'));
90
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+
96
+ function isTrusted(profile, options = {}) {
97
+ const trusted = readTrusted(options.trustFile || TRUST_FILE, options.fsImpl);
98
+ return trusted[profile.file] === profile.hash;
99
+ }
100
+
101
+ function trustProfile(profile, options = {}) {
102
+ const trustFile = options.trustFile || TRUST_FILE;
103
+ const fsImpl = options.fsImpl || fs;
104
+ const trusted = readTrusted(trustFile, fsImpl);
105
+ trusted[profile.file] = profile.hash;
106
+ fsImpl.mkdirSync(path.dirname(trustFile), { recursive: true });
107
+ fsImpl.writeFileSync(trustFile, `${JSON.stringify(trusted, null, 2)}\n`);
108
+ }
109
+
110
+ function assertTrusted(profile, options = {}) {
111
+ if (!isTrusted(profile, options)) {
112
+ throw new Error(
113
+ `${profile.file} is not trusted or has changed since it was approved. ` +
114
+ 'Review it, then run: dev trust'
115
+ );
116
+ }
117
+ }
118
+
119
+ function buildProfileCommands(profile, actions, project, options = {}) {
120
+ if (!options.previewUntrusted) assertTrusted(profile, options);
121
+ return buildRecipeActions(
122
+ { name: 'profile', inputs: {}, baseDir: project.root },
123
+ actions,
124
+ project.root,
125
+ {
126
+ packageManager: project.packageManager,
127
+ python: options.config?.python,
128
+ values: {},
129
+ skipRequiredInputs: true,
130
+ },
131
+ { root: project.root }
132
+ );
133
+ }
134
+
135
+ function buildProfileLifecycle(project, action, options = {}) {
136
+ const actions = project.profile?.lifecycle?.[action];
137
+ if (!actions) return undefined;
138
+ return buildProfileCommands(project.profile, actions, project, options);
139
+ }
140
+
141
+ function buildProfileTask(project, name, options = {}) {
142
+ const task = project.profile?.tasks?.[name];
143
+ if (!task) return undefined;
144
+ return buildProfileCommands(project.profile, task.commands, project, options);
145
+ }
146
+
147
+ function describeProfile(project, options = {}) {
148
+ const preview = { ...options, previewUntrusted: true };
149
+ const entries = [];
150
+ for (const [action, actions] of Object.entries(project.profile.lifecycle)) {
151
+ entries.push({
152
+ label: `lifecycle ${action}`,
153
+ commands: buildProfileCommands(project.profile, actions, project, preview),
154
+ });
155
+ }
156
+ for (const [name, task] of Object.entries(project.profile.tasks)) {
157
+ entries.push({
158
+ label: `task ${name}`,
159
+ commands: buildProfileCommands(project.profile, task.commands, project, preview),
160
+ });
161
+ }
162
+ return entries;
163
+ }
164
+
165
+ module.exports = {
166
+ PROFILE_FILE,
167
+ describeProfile,
168
+ assertTrusted,
169
+ buildProfileLifecycle,
170
+ buildProfileTask,
171
+ isTrusted,
172
+ loadProfile,
173
+ trustProfile,
174
+ validateProfile,
175
+ };
@@ -0,0 +1,50 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { PROJECTS_FILE } = require('../config');
6
+
7
+ function readProjects(options = {}) {
8
+ const file = options.file || PROJECTS_FILE;
9
+ const fsImpl = options.fsImpl || fs;
10
+ try {
11
+ const projects = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
12
+ return Array.isArray(projects) ? projects : [];
13
+ } catch (error) {
14
+ return [];
15
+ }
16
+ }
17
+
18
+ function saveProjects(projects, options = {}) {
19
+ const file = options.file || PROJECTS_FILE;
20
+ const fsImpl = options.fsImpl || fs;
21
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
22
+ fsImpl.writeFileSync(file, `${JSON.stringify(projects.slice(0, 50), null, 2)}\n`);
23
+ }
24
+
25
+ function recordProject(project, options = {}) {
26
+ const fsImpl = options.fsImpl || fs;
27
+ const absolutePath = path.resolve(project.path);
28
+ if (!fsImpl.existsSync(absolutePath)) return;
29
+
30
+ const projects = readProjects(options).filter((entry) => entry.path !== absolutePath);
31
+ projects.unshift({
32
+ name: project.name || path.basename(absolutePath),
33
+ type: project.type || 'unknown',
34
+ path: absolutePath,
35
+ lastUsedAt: new Date().toISOString(),
36
+ });
37
+ saveProjects(projects, options);
38
+ }
39
+
40
+ function getExistingProjects(options = {}) {
41
+ const fsImpl = options.fsImpl || fs;
42
+ return readProjects(options).filter((project) => fsImpl.existsSync(project.path));
43
+ }
44
+
45
+ module.exports = {
46
+ getExistingProjects,
47
+ readProjects,
48
+ recordProject,
49
+ saveProjects,
50
+ };
@@ -0,0 +1,96 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function readJsonSafe(file, fsImpl) {
7
+ try {
8
+ return JSON.parse(fsImpl.readFileSync(file, 'utf8'));
9
+ } catch {
10
+ return undefined;
11
+ }
12
+ }
13
+
14
+ function readPnpmPatterns(root, fsImpl) {
15
+ let content;
16
+ try {
17
+ content = fsImpl.readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8');
18
+ } catch {
19
+ return [];
20
+ }
21
+ const patterns = [];
22
+ let inPackages = false;
23
+ for (const line of content.split(/\r?\n/)) {
24
+ if (/^packages\s*:/.test(line)) {
25
+ inPackages = true;
26
+ continue;
27
+ }
28
+ if (inPackages) {
29
+ const item = line.match(/^\s+-\s+['"]?([^'"#]+?)['"]?\s*(?:#.*)?$/);
30
+ if (item) patterns.push(item[1]);
31
+ else if (/^\S/.test(line)) inPackages = false;
32
+ }
33
+ }
34
+ return patterns;
35
+ }
36
+
37
+ function declaredPatterns(root, packageJson, fsImpl) {
38
+ const declared = packageJson?.workspaces;
39
+ const fromPackage = Array.isArray(declared) ? declared : declared?.packages;
40
+ return [
41
+ ...(Array.isArray(fromPackage) ? fromPackage : []),
42
+ ...readPnpmPatterns(root, fsImpl),
43
+ ].filter((pattern) => typeof pattern === 'string' && pattern && !pattern.startsWith('!'));
44
+ }
45
+
46
+ function expandPattern(root, pattern, fsImpl) {
47
+ const normalized = pattern.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
48
+ if (normalized.split('/').includes('..') || path.isAbsolute(normalized)) return [];
49
+ if (!normalized.includes('*')) return [normalized];
50
+ const base = normalized.replace(/\/\*{1,2}$/, '');
51
+ if (base.includes('*')) return [];
52
+ try {
53
+ return fsImpl.readdirSync(path.join(root, base), { withFileTypes: true })
54
+ .filter((entry) => entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.'))
55
+ .map((entry) => `${base}/${entry.name}`);
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ function findWorkspaces(root, packageJson, fsImpl = fs) {
62
+ const seen = new Set();
63
+ const workspaces = [];
64
+ for (const pattern of declaredPatterns(root, packageJson, fsImpl)) {
65
+ for (const dir of expandPattern(root, pattern, fsImpl)) {
66
+ if (seen.has(dir)) continue;
67
+ const manifest = readJsonSafe(path.join(root, dir, 'package.json'), fsImpl);
68
+ if (!manifest) continue;
69
+ seen.add(dir);
70
+ workspaces.push({ name: manifest.name || path.basename(dir), dir, packageJson: manifest });
71
+ }
72
+ }
73
+ return workspaces;
74
+ }
75
+
76
+ function resolveWorkspace(project, filter) {
77
+ const workspaces = project.workspaces || [];
78
+ if (workspaces.length === 0) {
79
+ throw new Error('--filter requires a Node.js workspace project (no workspaces were detected).');
80
+ }
81
+ const normalized = filter.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
82
+ const match = workspaces.find((workspace) => workspace.name === filter)
83
+ || workspaces.find((workspace) => workspace.dir === normalized)
84
+ || workspaces.find((workspace) => path.basename(workspace.dir) === filter);
85
+ if (!match) {
86
+ throw new Error(
87
+ `Workspace not found: ${filter}. Available: ${workspaces.map((workspace) => workspace.name).join(', ')}.`
88
+ );
89
+ }
90
+ return match;
91
+ }
92
+
93
+ module.exports = {
94
+ findWorkspaces,
95
+ resolveWorkspace,
96
+ };
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 bachbnt
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const [action, source, value] = process.argv.slice(2);
8
+
9
+ try {
10
+ if (action === 'mkdir') {
11
+ fs.mkdirSync(source, { recursive: true });
12
+ } else if (action === 'write') {
13
+ fs.mkdirSync(path.dirname(source), { recursive: true });
14
+ fs.writeFileSync(source, Buffer.from(value || '', 'base64').toString('utf8'), { flag: 'wx' });
15
+ } else if (action === 'copy') {
16
+ fs.mkdirSync(path.dirname(value), { recursive: true });
17
+ fs.cpSync(source, value, { recursive: true, errorOnExist: true, force: false });
18
+ } else {
19
+ throw new Error(`Unsupported recipe action: ${action}`);
20
+ }
21
+ } catch (error) {
22
+ console.error(`Unable to execute recipe action: ${error.message}`);
23
+ process.exitCode = 1;
24
+ }