@fredlackey/devutils 0.0.19 → 0.1.0

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 (122) hide show
  1. package/README.md +223 -32
  2. package/package.json +7 -5
  3. package/src/api/loader.js +229 -0
  4. package/src/api/registry.json +62 -0
  5. package/src/cli.js +305 -0
  6. package/src/commands/ai/index.js +16 -0
  7. package/src/commands/ai/launch.js +112 -0
  8. package/src/commands/ai/list.js +54 -0
  9. package/src/commands/ai/resume.js +70 -0
  10. package/src/commands/ai/sessions.js +121 -0
  11. package/src/commands/ai/set.js +131 -0
  12. package/src/commands/ai/show.js +74 -0
  13. package/src/commands/ai/tools.js +46 -0
  14. package/src/commands/alias/add.js +93 -0
  15. package/src/commands/alias/helpers.js +107 -0
  16. package/src/commands/alias/index.js +14 -0
  17. package/src/commands/alias/list.js +55 -0
  18. package/src/commands/alias/remove.js +62 -0
  19. package/src/commands/alias/sync.js +109 -0
  20. package/src/commands/api/disable.js +73 -0
  21. package/src/commands/api/enable.js +148 -0
  22. package/src/commands/api/index.js +15 -0
  23. package/src/commands/api/list.js +66 -0
  24. package/src/commands/api/update.js +87 -0
  25. package/src/commands/auth/index.js +15 -0
  26. package/src/commands/auth/list.js +49 -0
  27. package/src/commands/auth/login.js +384 -0
  28. package/src/commands/auth/logout.js +111 -0
  29. package/src/commands/auth/refresh.js +184 -0
  30. package/src/commands/auth/services.js +169 -0
  31. package/src/commands/auth/status.js +104 -0
  32. package/src/commands/config/export.js +224 -0
  33. package/src/commands/config/get.js +52 -0
  34. package/src/commands/config/import.js +308 -0
  35. package/src/commands/config/index.js +17 -0
  36. package/src/commands/config/init.js +143 -0
  37. package/src/commands/config/reset.js +57 -0
  38. package/src/commands/config/set.js +93 -0
  39. package/src/commands/config/show.js +35 -0
  40. package/src/commands/help.js +338 -0
  41. package/src/commands/identity/add.js +133 -0
  42. package/src/commands/identity/index.js +17 -0
  43. package/src/commands/identity/link.js +76 -0
  44. package/src/commands/identity/list.js +48 -0
  45. package/src/commands/identity/remove.js +72 -0
  46. package/src/commands/identity/show.js +65 -0
  47. package/src/commands/identity/sync.js +172 -0
  48. package/src/commands/identity/unlink.js +57 -0
  49. package/src/commands/ignore/add.js +165 -0
  50. package/src/commands/ignore/index.js +14 -0
  51. package/src/commands/ignore/list.js +89 -0
  52. package/src/commands/ignore/markers.js +43 -0
  53. package/src/commands/ignore/remove.js +164 -0
  54. package/src/commands/ignore/show.js +169 -0
  55. package/src/commands/machine/detect.js +122 -0
  56. package/src/commands/machine/index.js +14 -0
  57. package/src/commands/machine/list.js +74 -0
  58. package/src/commands/machine/set.js +106 -0
  59. package/src/commands/machine/show.js +35 -0
  60. package/src/commands/schema.js +152 -0
  61. package/src/commands/search/collections.js +134 -0
  62. package/src/commands/search/get.js +71 -0
  63. package/src/commands/search/index-cmd.js +54 -0
  64. package/src/commands/search/index.js +21 -0
  65. package/src/commands/search/keyword.js +60 -0
  66. package/src/commands/search/qmd.js +70 -0
  67. package/src/commands/search/query.js +64 -0
  68. package/src/commands/search/semantic.js +62 -0
  69. package/src/commands/search/status.js +46 -0
  70. package/src/commands/status.js +276 -0
  71. package/src/commands/tools/check.js +79 -0
  72. package/src/commands/tools/index.js +14 -0
  73. package/src/commands/tools/install.js +110 -0
  74. package/src/commands/tools/list.js +91 -0
  75. package/src/commands/tools/search.js +60 -0
  76. package/src/commands/update.js +113 -0
  77. package/src/commands/util/add.js +151 -0
  78. package/src/commands/util/index.js +15 -0
  79. package/src/commands/util/list.js +97 -0
  80. package/src/commands/util/remove.js +76 -0
  81. package/src/commands/util/run.js +79 -0
  82. package/src/commands/util/show.js +67 -0
  83. package/src/commands/version.js +33 -0
  84. package/src/installers/_template.js +104 -0
  85. package/src/installers/git.js +150 -0
  86. package/src/installers/homebrew.js +190 -0
  87. package/src/installers/node.js +223 -0
  88. package/src/installers/registry.json +29 -0
  89. package/src/lib/config.js +125 -0
  90. package/src/lib/detect.js +74 -0
  91. package/src/lib/errors.js +114 -0
  92. package/src/lib/github.js +315 -0
  93. package/src/lib/installer.js +225 -0
  94. package/src/lib/output.js +239 -0
  95. package/src/lib/platform.js +112 -0
  96. package/src/lib/platforms/amazon-linux.js +41 -0
  97. package/src/lib/platforms/gitbash.js +46 -0
  98. package/src/lib/platforms/macos.js +45 -0
  99. package/src/lib/platforms/raspbian.js +41 -0
  100. package/src/lib/platforms/ubuntu.js +39 -0
  101. package/src/lib/platforms/windows.js +45 -0
  102. package/src/lib/prompt.js +161 -0
  103. package/src/lib/schema.js +211 -0
  104. package/src/lib/shell.js +75 -0
  105. package/src/patterns/gitignore/claude-code.txt +25 -0
  106. package/src/patterns/gitignore/docker.txt +15 -0
  107. package/src/patterns/gitignore/go.txt +24 -0
  108. package/src/patterns/gitignore/java.txt +38 -0
  109. package/src/patterns/gitignore/jetbrains.txt +26 -0
  110. package/src/patterns/gitignore/linux.txt +18 -0
  111. package/src/patterns/gitignore/macos.txt +27 -0
  112. package/src/patterns/gitignore/node.txt +51 -0
  113. package/src/patterns/gitignore/python.txt +55 -0
  114. package/src/patterns/gitignore/rust.txt +14 -0
  115. package/src/patterns/gitignore/terraform.txt +30 -0
  116. package/src/patterns/gitignore/vscode.txt +15 -0
  117. package/src/patterns/gitignore/windows.txt +25 -0
  118. package/src/utils/clone/index.js +165 -0
  119. package/src/utils/git-push/index.js +230 -0
  120. package/src/utils/git-status/index.js +116 -0
  121. package/src/utils/git-status/unix.sh +75 -0
  122. package/src/utils/registry.json +41 -0
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { MARKER_START, MARKER_END } = require('./markers');
6
+
7
+ const meta = {
8
+ description: 'Remove managed gitignore patterns for a technology',
9
+ arguments: [
10
+ { name: 'technology', description: 'Technology name to remove (e.g., node, macos)', required: true }
11
+ ],
12
+ flags: [
13
+ { name: 'path', description: 'Target directory (defaults to current directory)', type: 'string', default: '.' }
14
+ ]
15
+ };
16
+
17
+ /**
18
+ * Removes a DevUtils-managed section from the .gitignore in the target
19
+ * directory. If the section does not exist, this is a no-op (idempotent).
20
+ * If removing the section leaves the file empty, the file is deleted.
21
+ *
22
+ * @param {object} args - Parsed command arguments ({ positional, flags }).
23
+ * @param {object} context - The DevUtils context object.
24
+ */
25
+ async function run(args, context) {
26
+ const technology = args.positional[0];
27
+
28
+ // Validate: technology name is required
29
+ if (!technology) {
30
+ context.errors.throwError(400, 'Missing required argument: <technology>. Example: dev ignore remove node', 'ignore');
31
+ return;
32
+ }
33
+
34
+ // Step A: Resolve the .gitignore path
35
+ const flagPath = args.flags.path || '.';
36
+ const gitignorePath = path.resolve(flagPath, '.gitignore');
37
+
38
+ // Step B: Read existing .gitignore
39
+ if (!fs.existsSync(gitignorePath)) {
40
+ if (context.flags.format === 'json') {
41
+ context.output.out({
42
+ technology,
43
+ action: 'none',
44
+ message: `No .gitignore found at ${path.resolve(flagPath)}.`
45
+ });
46
+ } else {
47
+ context.output.info(`No .gitignore found at ${path.resolve(flagPath)}.`);
48
+ }
49
+ return;
50
+ }
51
+
52
+ const existingContent = fs.readFileSync(gitignorePath, 'utf8');
53
+
54
+ // Step C: Find the section markers
55
+ const lines = existingContent.split(/\r?\n/);
56
+ const startMarker = MARKER_START(technology);
57
+ const endMarker = MARKER_END(technology);
58
+ const startIndex = lines.findIndex(line => line.trim() === startMarker);
59
+ const endIndex = lines.findIndex(line => line.trim() === endMarker);
60
+
61
+ if (startIndex === -1 && endIndex === -1) {
62
+ // Section not present: no-op
63
+ if (context.flags.format === 'json') {
64
+ context.output.out({
65
+ technology,
66
+ action: 'none',
67
+ message: `No managed section for "${technology}" found in .gitignore.`
68
+ });
69
+ } else {
70
+ context.output.info(`No managed section for "${technology}" found in .gitignore.`);
71
+ }
72
+ return;
73
+ }
74
+
75
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
76
+ // Only one marker found or end before start: corrupted state
77
+ context.output.info(`Warning: Found partial or corrupted markers for "${technology}" in .gitignore. Please fix manually.`);
78
+ return;
79
+ }
80
+
81
+ // Step D: Remove the section (start through end marker, inclusive)
82
+ const before = lines.slice(0, startIndex);
83
+ let after = lines.slice(endIndex + 1);
84
+
85
+ // Clean up one trailing blank line after the end marker
86
+ if (after.length > 0 && after[0].trim() === '') {
87
+ after = after.slice(1);
88
+ }
89
+
90
+ let newLines = [...before, ...after];
91
+
92
+ // Build the updated content
93
+ let updatedContent = newLines.join('\n');
94
+
95
+ // Step E: If the file is now empty (only whitespace), delete it
96
+ if (updatedContent.trim() === '') {
97
+ if (context.flags.dryRun) {
98
+ if (context.flags.format === 'json') {
99
+ context.output.out({
100
+ technology,
101
+ action: 'removed',
102
+ path: gitignorePath,
103
+ fileDeleted: true,
104
+ dryRun: true
105
+ });
106
+ } else {
107
+ context.output.info(`[dry-run] Would remove ${technology} patterns from .gitignore and delete the empty file`);
108
+ }
109
+ return;
110
+ }
111
+
112
+ fs.unlinkSync(gitignorePath);
113
+
114
+ if (context.flags.format === 'json') {
115
+ context.output.out({
116
+ technology,
117
+ action: 'removed',
118
+ path: gitignorePath,
119
+ fileDeleted: true
120
+ });
121
+ } else {
122
+ context.output.info(`Removed ${technology} patterns from .gitignore (file deleted -- was empty)`);
123
+ }
124
+ return;
125
+ }
126
+
127
+ // Make sure the file ends with a trailing newline
128
+ if (!updatedContent.endsWith('\n')) {
129
+ updatedContent += '\n';
130
+ }
131
+
132
+ // Handle --dry-run
133
+ if (context.flags.dryRun) {
134
+ if (context.flags.format === 'json') {
135
+ context.output.out({
136
+ technology,
137
+ action: 'removed',
138
+ path: gitignorePath,
139
+ fileDeleted: false,
140
+ dryRun: true
141
+ });
142
+ } else {
143
+ context.output.info(`[dry-run] Would remove ${technology} patterns from .gitignore`);
144
+ }
145
+ return;
146
+ }
147
+
148
+ // Step E: Write the result
149
+ fs.writeFileSync(gitignorePath, updatedContent);
150
+
151
+ // Step F: Output the result
152
+ if (context.flags.format === 'json') {
153
+ context.output.out({
154
+ technology,
155
+ action: 'removed',
156
+ path: gitignorePath,
157
+ fileDeleted: false
158
+ });
159
+ } else {
160
+ context.output.info(`Removed ${technology} patterns from .gitignore`);
161
+ }
162
+ }
163
+
164
+ module.exports = { meta, run };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { MARKER_START_PREFIX, MARKER_END_PREFIX } = require('./markers');
6
+
7
+ const meta = {
8
+ description: 'Show managed gitignore sections in the current directory',
9
+ arguments: [],
10
+ flags: [
11
+ { name: 'path', description: 'Target directory (defaults to current directory)', type: 'string', default: '.' }
12
+ ]
13
+ };
14
+
15
+ /**
16
+ * Scans the .gitignore in the target directory and reports which
17
+ * DevUtils-managed sections are present, including the technology name,
18
+ * pattern line count, and line range for each section. Also counts
19
+ * unmanaged lines (lines outside any DevUtils section that are not blank
20
+ * or comments).
21
+ *
22
+ * @param {object} args - Parsed command arguments ({ positional, flags }).
23
+ * @param {object} context - The DevUtils context object.
24
+ */
25
+ async function run(args, context) {
26
+ // Step A: Resolve the .gitignore path
27
+ const flagPath = args.flags.path || '.';
28
+ const gitignorePath = path.resolve(flagPath, '.gitignore');
29
+
30
+ // Step B: Read the file
31
+ if (!fs.existsSync(gitignorePath)) {
32
+ if (context.flags.format === 'json') {
33
+ context.output.out({
34
+ path: gitignorePath,
35
+ sections: [],
36
+ count: 0,
37
+ unmanagedLines: 0,
38
+ message: `No .gitignore found at ${path.resolve(flagPath)}.`
39
+ });
40
+ } else {
41
+ context.output.info(`No .gitignore found at ${path.resolve(flagPath)}.`);
42
+ }
43
+ return;
44
+ }
45
+
46
+ const content = fs.readFileSync(gitignorePath, 'utf8');
47
+ const lines = content.split(/\r?\n/);
48
+
49
+ // Step C: Parse managed sections
50
+ const sections = [];
51
+ const managedLineIndices = new Set();
52
+ let i = 0;
53
+
54
+ while (i < lines.length) {
55
+ const trimmedLine = lines[i].trim();
56
+
57
+ if (trimmedLine.startsWith(MARKER_START_PREFIX)) {
58
+ // Found a start marker -- extract the technology name
59
+ const technology = trimmedLine.slice(MARKER_START_PREFIX.length);
60
+ const startLine = i + 1; // 1-indexed
61
+ managedLineIndices.add(i);
62
+
63
+ // Find the corresponding end marker
64
+ const expectedEnd = `# <<< devutils:${technology}`;
65
+ let endIdx = -1;
66
+
67
+ for (let j = i + 1; j < lines.length; j++) {
68
+ if (lines[j].trim() === expectedEnd) {
69
+ endIdx = j;
70
+ break;
71
+ }
72
+ }
73
+
74
+ if (endIdx !== -1) {
75
+ // Mark all lines in this section as managed
76
+ for (let k = i; k <= endIdx; k++) {
77
+ managedLineIndices.add(k);
78
+ }
79
+
80
+ const patternLineCount = endIdx - i - 1; // lines between markers
81
+ const endLine = endIdx + 1; // 1-indexed
82
+
83
+ sections.push({
84
+ technology,
85
+ lines: patternLineCount,
86
+ startLine,
87
+ endLine
88
+ });
89
+
90
+ i = endIdx + 1;
91
+ } else {
92
+ // Orphaned start marker -- no matching end
93
+ sections.push({
94
+ technology,
95
+ lines: null,
96
+ startLine,
97
+ endLine: null,
98
+ error: 'Missing end marker'
99
+ });
100
+ i++;
101
+ }
102
+ } else if (trimmedLine.startsWith(MARKER_END_PREFIX)) {
103
+ // Orphaned end marker -- no matching start
104
+ const technology = trimmedLine.slice(MARKER_END_PREFIX.length);
105
+ managedLineIndices.add(i);
106
+ sections.push({
107
+ technology,
108
+ lines: null,
109
+ startLine: null,
110
+ endLine: i + 1, // 1-indexed
111
+ error: 'Missing start marker'
112
+ });
113
+ i++;
114
+ } else {
115
+ i++;
116
+ }
117
+ }
118
+
119
+ // Step D: Count unmanaged lines (not blank, not comments, not in a section)
120
+ let unmanagedLines = 0;
121
+ for (let idx = 0; idx < lines.length; idx++) {
122
+ if (managedLineIndices.has(idx)) continue;
123
+ const trimmed = lines[idx].trim();
124
+ if (trimmed === '') continue;
125
+ if (trimmed.startsWith('#')) continue;
126
+ unmanagedLines++;
127
+ }
128
+
129
+ // Step E: Build the result
130
+ const result = {
131
+ path: gitignorePath,
132
+ sections,
133
+ count: sections.length,
134
+ unmanagedLines
135
+ };
136
+
137
+ // Step F: Format the output
138
+ if (context.flags.format === 'json') {
139
+ context.output.out(result);
140
+ } else {
141
+ if (sections.length === 0) {
142
+ context.output.info('No DevUtils-managed sections found in .gitignore.');
143
+ } else {
144
+ // Calculate padding for aligned columns
145
+ const maxNameLength = Math.max(...sections.map(s => s.technology.length));
146
+ const maxLinesLength = Math.max(...sections.map(s => s.lines !== null ? String(s.lines).length : 1));
147
+
148
+ context.output.info('Managed sections in .gitignore:');
149
+ for (const section of sections) {
150
+ if (section.error) {
151
+ const paddedName = section.technology.padEnd(maxNameLength + 2);
152
+ context.output.info(` ${paddedName}[${section.error}]`);
153
+ } else {
154
+ const paddedName = section.technology.padEnd(maxNameLength + 2);
155
+ const paddedLines = String(section.lines).padStart(maxLinesLength);
156
+ context.output.info(` ${paddedName}${paddedLines} patterns (lines ${section.startLine}-${section.endLine})`);
157
+ }
158
+ }
159
+ context.output.info('');
160
+ context.output.info(`${sections.length} managed section${sections.length === 1 ? '' : 's'}.`);
161
+ }
162
+
163
+ if (unmanagedLines > 0) {
164
+ context.output.info(`Plus ${unmanagedLines} unmanaged line${unmanagedLines === 1 ? '' : 's'}.`);
165
+ }
166
+ }
167
+ }
168
+
169
+ module.exports = { meta, run };
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * Formats a byte count into a human-readable string (e.g., "16.0 GB").
9
+ * Rounds to one decimal place for clarity.
10
+ *
11
+ * @param {number} bytes - The number of bytes to format.
12
+ * @returns {string} A human-readable size string like "16.0 GB".
13
+ */
14
+ function formatBytes(bytes) {
15
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
16
+ let i = 0;
17
+ let value = bytes;
18
+ while (value >= 1024 && i < units.length - 1) {
19
+ value /= 1024;
20
+ i++;
21
+ }
22
+ return `${value.toFixed(1)} ${units[i]}`;
23
+ }
24
+
25
+ const meta = {
26
+ description: 'Detect the current machine\'s OS, architecture, package managers, and capabilities.',
27
+ arguments: [],
28
+ flags: []
29
+ };
30
+
31
+ /**
32
+ * Detects the current machine's hardware and OS details, then writes
33
+ * the profile to ~/.devutils/machines/current.json.
34
+ *
35
+ * Uses context.platform.detect() for OS type, then layers on hostname,
36
+ * CPU count, memory, available package managers, and OS version info
37
+ * from Node.js built-in modules and shell commands.
38
+ *
39
+ * @param {object} args - Parsed command arguments (none expected).
40
+ * @param {object} context - The CLI context object with platform, shell, output, errors.
41
+ */
42
+ async function run(args, context) {
43
+ // Step 1: Get base platform detection
44
+ const platform = context.platform.detect();
45
+
46
+ // Step 2: Gather hardware and system info from Node.js os module
47
+ const hostname = os.hostname();
48
+ const cpuCount = os.cpus().length;
49
+ const totalMemory = os.totalmem();
50
+ const arch = os.arch();
51
+ const osRelease = os.release();
52
+
53
+ // Step 3: Detect which package managers are installed on this machine
54
+ const packageManagers = [];
55
+ const candidates = ['brew', 'apt', 'snap', 'dnf', 'yum', 'choco', 'winget', 'npm', 'yarn', 'pnpm'];
56
+
57
+ for (const pm of candidates) {
58
+ if (context.shell.commandExists(pm)) {
59
+ packageManagers.push(pm);
60
+ }
61
+ }
62
+
63
+ // Step 4: Get OS-specific version and name info
64
+ let osVersion = osRelease;
65
+ let osName = platform.type;
66
+
67
+ try {
68
+ if (platform.type === 'macos') {
69
+ const result = await context.shell.exec('sw_vers -productVersion');
70
+ if (result.exitCode === 0 && result.stdout) {
71
+ osVersion = result.stdout.trim();
72
+ }
73
+ osName = 'macOS';
74
+ } else if (['ubuntu', 'raspbian', 'amazon-linux'].includes(platform.type)) {
75
+ const releaseFile = fs.readFileSync('/etc/os-release', 'utf8');
76
+ const nameMatch = releaseFile.match(/^PRETTY_NAME="?(.+?)"?$/m);
77
+ if (nameMatch) {
78
+ osName = nameMatch[1];
79
+ }
80
+ const versionMatch = releaseFile.match(/^VERSION_ID="?(.+?)"?$/m);
81
+ if (versionMatch) {
82
+ osVersion = versionMatch[1];
83
+ }
84
+ }
85
+ } catch (err) {
86
+ // Keep defaults if detection fails
87
+ }
88
+
89
+ // Step 5: Build the machine profile object
90
+ const machineProfile = {
91
+ hostname: hostname,
92
+ os: {
93
+ type: platform.type,
94
+ name: osName,
95
+ version: osVersion,
96
+ kernel: osRelease
97
+ },
98
+ arch: arch,
99
+ cpu: {
100
+ count: cpuCount,
101
+ model: os.cpus()[0] ? os.cpus()[0].model : 'unknown'
102
+ },
103
+ memory: {
104
+ total: totalMemory,
105
+ totalHuman: formatBytes(totalMemory)
106
+ },
107
+ packageManagers: packageManagers,
108
+ detectedAt: new Date().toISOString()
109
+ };
110
+
111
+ // Step 6: Write the profile to disk (idempotent — overwrites if exists)
112
+ const MACHINES_DIR = path.join(os.homedir(), '.devutils', 'machines');
113
+ const CURRENT_FILE = path.join(MACHINES_DIR, 'current.json');
114
+
115
+ fs.mkdirSync(MACHINES_DIR, { recursive: true });
116
+ fs.writeFileSync(CURRENT_FILE, JSON.stringify(machineProfile, null, 2) + '\n');
117
+
118
+ // Step 7: Output the result
119
+ context.output.out(machineProfile);
120
+ }
121
+
122
+ module.exports = { meta, run };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Machine service registration.
3
+ * Machine profiles and detection.
4
+ */
5
+ module.exports = {
6
+ name: 'machine',
7
+ description: 'Machine profiles and detection',
8
+ commands: {
9
+ detect: () => require('./detect'),
10
+ show: () => require('./show'),
11
+ set: () => require('./set'),
12
+ list: () => require('./list'),
13
+ }
14
+ };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const meta = {
8
+ description: 'List all known machine profiles.',
9
+ arguments: [],
10
+ flags: []
11
+ };
12
+
13
+ /**
14
+ * Lists all machine profiles stored in ~/.devutils/machines/.
15
+ * Reads every .json file in the directory and builds a summary for each,
16
+ * including hostname, OS, architecture, and detection timestamp.
17
+ * The current machine (current.json) is marked in the output.
18
+ *
19
+ * Right now this will only show the local machine. When backup/sync is
20
+ * built, profiles from other computers will also appear here.
21
+ *
22
+ * @param {object} args - Parsed command arguments (none expected).
23
+ * @param {object} context - The CLI context object with output and errors.
24
+ */
25
+ async function run(args, context) {
26
+ const MACHINES_DIR = path.join(os.homedir(), '.devutils', 'machines');
27
+
28
+ // Check if the machines directory exists
29
+ if (!fs.existsSync(MACHINES_DIR)) {
30
+ context.errors.throwError(404, 'No machine profiles found. Run "dev machine detect" first.', 'machine');
31
+ return;
32
+ }
33
+
34
+ // Find all JSON files in the machines directory
35
+ const files = fs.readdirSync(MACHINES_DIR).filter(f => f.endsWith('.json'));
36
+
37
+ if (files.length === 0) {
38
+ context.errors.throwError(404, 'No machine profiles found. Run "dev machine detect" first.', 'machine');
39
+ return;
40
+ }
41
+
42
+ // Load each profile and build a summary
43
+ const machines = [];
44
+
45
+ for (const file of files) {
46
+ try {
47
+ const raw = fs.readFileSync(path.join(MACHINES_DIR, file), 'utf8');
48
+ const profile = JSON.parse(raw);
49
+
50
+ machines.push({
51
+ file: file,
52
+ current: file === 'current.json',
53
+ hostname: profile.hostname || 'unknown',
54
+ os: profile.os ? `${profile.os.name} ${profile.os.version}` : 'unknown',
55
+ arch: profile.arch || 'unknown',
56
+ detectedAt: profile.detectedAt || 'unknown'
57
+ });
58
+ } catch (err) {
59
+ // Don't crash on corrupted files — show them as error entries
60
+ machines.push({
61
+ file: file,
62
+ current: file === 'current.json',
63
+ hostname: 'error',
64
+ os: `Failed to read: ${err.message}`,
65
+ arch: 'unknown',
66
+ detectedAt: 'unknown'
67
+ });
68
+ }
69
+ }
70
+
71
+ context.output.out(machines);
72
+ }
73
+
74
+ module.exports = { meta, run };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const meta = {
8
+ description: 'Set a value in the current machine profile.',
9
+ arguments: [
10
+ { name: 'key', required: true, description: 'Dot-notation path to the value (e.g., nickname, os.version)' },
11
+ { name: 'value', required: false, description: 'The value to set. Omit if using --json.' }
12
+ ],
13
+ flags: [
14
+ { name: 'json', type: 'string', description: 'Set a structured value using a JSON string' }
15
+ ]
16
+ };
17
+
18
+ /**
19
+ * Updates a single value in the current machine profile by dot-notation key.
20
+ * Reads ~/.devutils/machines/current.json, sets the value at the given path,
21
+ * and writes the file back. Preserves all other existing fields.
22
+ *
23
+ * Supports type coercion for simple values: "true"/"false" become booleans,
24
+ * "null" becomes null, numeric strings become numbers. Use --json for
25
+ * structured values like arrays or objects.
26
+ *
27
+ * @param {object} args - Parsed command arguments with positional[0]=key, positional[1]=value.
28
+ * @param {object} context - The CLI context object with output and errors.
29
+ */
30
+ async function run(args, context) {
31
+ const key = args.positional[0];
32
+ const rawValue = args.positional[1];
33
+ const jsonValue = args.flags.json;
34
+
35
+ // Validate required arguments
36
+ if (!key) {
37
+ context.errors.throwError(400, 'Missing required argument: <key>. Example: dev machine set nickname "my-laptop"', 'machine');
38
+ return;
39
+ }
40
+
41
+ if (!rawValue && rawValue !== '' && !jsonValue) {
42
+ context.errors.throwError(400, 'Missing value. Provide a value or use --json for structured data.', 'machine');
43
+ return;
44
+ }
45
+
46
+ if (rawValue && jsonValue) {
47
+ context.errors.throwError(400, 'Provide either a positional value or --json, not both.', 'machine');
48
+ return;
49
+ }
50
+
51
+ // Parse the value with type coercion
52
+ let value;
53
+
54
+ if (jsonValue) {
55
+ try {
56
+ value = JSON.parse(jsonValue);
57
+ } catch (err) {
58
+ context.errors.throwError(400, `Invalid JSON: ${err.message}`, 'machine');
59
+ return;
60
+ }
61
+ } else {
62
+ if (rawValue === 'true') {
63
+ value = true;
64
+ } else if (rawValue === 'false') {
65
+ value = false;
66
+ } else if (rawValue === 'null') {
67
+ value = null;
68
+ } else if (!isNaN(rawValue) && rawValue.trim() !== '') {
69
+ value = Number(rawValue);
70
+ } else {
71
+ value = rawValue;
72
+ }
73
+ }
74
+
75
+ // Read the existing machine profile
76
+ const CURRENT_FILE = path.join(os.homedir(), '.devutils', 'machines', 'current.json');
77
+
78
+ if (!fs.existsSync(CURRENT_FILE)) {
79
+ context.errors.throwError(404, 'No machine profile found. Run "dev machine detect" first.', 'machine');
80
+ return;
81
+ }
82
+
83
+ const raw = fs.readFileSync(CURRENT_FILE, 'utf8');
84
+ const profile = JSON.parse(raw);
85
+
86
+ // Walk the dot-notation path and set the value
87
+ const parts = key.split('.');
88
+ let target = profile;
89
+
90
+ for (let i = 0; i < parts.length - 1; i++) {
91
+ const part = parts[i];
92
+ if (!(part in target) || typeof target[part] !== 'object' || target[part] === null) {
93
+ target[part] = {};
94
+ }
95
+ target = target[part];
96
+ }
97
+
98
+ const lastPart = parts[parts.length - 1];
99
+ target[lastPart] = value;
100
+
101
+ // Write the updated profile back to disk
102
+ fs.writeFileSync(CURRENT_FILE, JSON.stringify(profile, null, 2) + '\n');
103
+ context.output.out({ key: key, value: value });
104
+ }
105
+
106
+ module.exports = { meta, run };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const meta = {
8
+ description: 'Display the current machine profile.',
9
+ arguments: [],
10
+ flags: []
11
+ };
12
+
13
+ /**
14
+ * Reads the current machine profile from ~/.devutils/machines/current.json
15
+ * and displays it. If the file does not exist, tells the user to run
16
+ * "dev machine detect" first.
17
+ *
18
+ * @param {object} args - Parsed command arguments (none expected).
19
+ * @param {object} context - The CLI context object with output and errors.
20
+ */
21
+ async function run(args, context) {
22
+ const CURRENT_FILE = path.join(os.homedir(), '.devutils', 'machines', 'current.json');
23
+
24
+ if (!fs.existsSync(CURRENT_FILE)) {
25
+ context.errors.throwError(404, 'No machine profile found. Run "dev machine detect" first.', 'machine');
26
+ return;
27
+ }
28
+
29
+ const raw = fs.readFileSync(CURRENT_FILE, 'utf8');
30
+ const profile = JSON.parse(raw);
31
+
32
+ context.output.out(profile);
33
+ }
34
+
35
+ module.exports = { meta, run };