@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.
- package/README.md +223 -32
- package/package.json +7 -5
- package/src/api/loader.js +229 -0
- package/src/api/registry.json +62 -0
- package/src/cli.js +305 -0
- package/src/commands/ai/index.js +16 -0
- package/src/commands/ai/launch.js +112 -0
- package/src/commands/ai/list.js +54 -0
- package/src/commands/ai/resume.js +70 -0
- package/src/commands/ai/sessions.js +121 -0
- package/src/commands/ai/set.js +131 -0
- package/src/commands/ai/show.js +74 -0
- package/src/commands/ai/tools.js +46 -0
- package/src/commands/alias/add.js +93 -0
- package/src/commands/alias/helpers.js +107 -0
- package/src/commands/alias/index.js +14 -0
- package/src/commands/alias/list.js +55 -0
- package/src/commands/alias/remove.js +62 -0
- package/src/commands/alias/sync.js +109 -0
- package/src/commands/api/disable.js +73 -0
- package/src/commands/api/enable.js +148 -0
- package/src/commands/api/index.js +15 -0
- package/src/commands/api/list.js +66 -0
- package/src/commands/api/update.js +87 -0
- package/src/commands/auth/index.js +15 -0
- package/src/commands/auth/list.js +49 -0
- package/src/commands/auth/login.js +384 -0
- package/src/commands/auth/logout.js +111 -0
- package/src/commands/auth/refresh.js +184 -0
- package/src/commands/auth/services.js +169 -0
- package/src/commands/auth/status.js +104 -0
- package/src/commands/config/export.js +224 -0
- package/src/commands/config/get.js +52 -0
- package/src/commands/config/import.js +308 -0
- package/src/commands/config/index.js +17 -0
- package/src/commands/config/init.js +143 -0
- package/src/commands/config/reset.js +57 -0
- package/src/commands/config/set.js +93 -0
- package/src/commands/config/show.js +35 -0
- package/src/commands/help.js +338 -0
- package/src/commands/identity/add.js +133 -0
- package/src/commands/identity/index.js +17 -0
- package/src/commands/identity/link.js +76 -0
- package/src/commands/identity/list.js +48 -0
- package/src/commands/identity/remove.js +72 -0
- package/src/commands/identity/show.js +65 -0
- package/src/commands/identity/sync.js +172 -0
- package/src/commands/identity/unlink.js +57 -0
- package/src/commands/ignore/add.js +165 -0
- package/src/commands/ignore/index.js +14 -0
- package/src/commands/ignore/list.js +89 -0
- package/src/commands/ignore/markers.js +43 -0
- package/src/commands/ignore/remove.js +164 -0
- package/src/commands/ignore/show.js +169 -0
- package/src/commands/machine/detect.js +122 -0
- package/src/commands/machine/index.js +14 -0
- package/src/commands/machine/list.js +74 -0
- package/src/commands/machine/set.js +106 -0
- package/src/commands/machine/show.js +35 -0
- package/src/commands/schema.js +152 -0
- package/src/commands/search/collections.js +134 -0
- package/src/commands/search/get.js +71 -0
- package/src/commands/search/index-cmd.js +54 -0
- package/src/commands/search/index.js +21 -0
- package/src/commands/search/keyword.js +60 -0
- package/src/commands/search/qmd.js +70 -0
- package/src/commands/search/query.js +64 -0
- package/src/commands/search/semantic.js +62 -0
- package/src/commands/search/status.js +46 -0
- package/src/commands/status.js +276 -0
- package/src/commands/tools/check.js +79 -0
- package/src/commands/tools/index.js +14 -0
- package/src/commands/tools/install.js +110 -0
- package/src/commands/tools/list.js +91 -0
- package/src/commands/tools/search.js +60 -0
- package/src/commands/update.js +113 -0
- package/src/commands/util/add.js +151 -0
- package/src/commands/util/index.js +15 -0
- package/src/commands/util/list.js +97 -0
- package/src/commands/util/remove.js +76 -0
- package/src/commands/util/run.js +79 -0
- package/src/commands/util/show.js +67 -0
- package/src/commands/version.js +33 -0
- package/src/installers/_template.js +104 -0
- package/src/installers/git.js +150 -0
- package/src/installers/homebrew.js +190 -0
- package/src/installers/node.js +223 -0
- package/src/installers/registry.json +29 -0
- package/src/lib/config.js +125 -0
- package/src/lib/detect.js +74 -0
- package/src/lib/errors.js +114 -0
- package/src/lib/github.js +315 -0
- package/src/lib/installer.js +225 -0
- package/src/lib/output.js +239 -0
- package/src/lib/platform.js +112 -0
- package/src/lib/platforms/amazon-linux.js +41 -0
- package/src/lib/platforms/gitbash.js +46 -0
- package/src/lib/platforms/macos.js +45 -0
- package/src/lib/platforms/raspbian.js +41 -0
- package/src/lib/platforms/ubuntu.js +39 -0
- package/src/lib/platforms/windows.js +45 -0
- package/src/lib/prompt.js +161 -0
- package/src/lib/schema.js +211 -0
- package/src/lib/shell.js +75 -0
- package/src/patterns/gitignore/claude-code.txt +25 -0
- package/src/patterns/gitignore/docker.txt +15 -0
- package/src/patterns/gitignore/go.txt +24 -0
- package/src/patterns/gitignore/java.txt +38 -0
- package/src/patterns/gitignore/jetbrains.txt +26 -0
- package/src/patterns/gitignore/linux.txt +18 -0
- package/src/patterns/gitignore/macos.txt +27 -0
- package/src/patterns/gitignore/node.txt +51 -0
- package/src/patterns/gitignore/python.txt +55 -0
- package/src/patterns/gitignore/rust.txt +14 -0
- package/src/patterns/gitignore/terraform.txt +30 -0
- package/src/patterns/gitignore/vscode.txt +15 -0
- package/src/patterns/gitignore/windows.txt +25 -0
- package/src/utils/clone/index.js +165 -0
- package/src/utils/git-push/index.js +230 -0
- package/src/utils/git-status/index.js +116 -0
- package/src/utils/git-status/unix.sh +75 -0
- 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 };
|