@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,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const DEVUTILS_DIR = path.join(os.homedir(), '.devutils');
|
|
8
|
+
const CONFIG_FILE = path.join(DEVUTILS_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
description: 'Pull config from remote backup and apply locally, or import from a local file.',
|
|
12
|
+
arguments: [],
|
|
13
|
+
flags: [
|
|
14
|
+
{ name: 'file', type: 'string', description: 'Import from a local file instead of remote backup' },
|
|
15
|
+
{ name: 'profile', type: 'string', description: 'Import a specific profile from the backup' }
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The config file keys and their mapping in the export bundle.
|
|
21
|
+
* Each entry maps a local filename to the bundle property name.
|
|
22
|
+
*/
|
|
23
|
+
const FILES_TO_IMPORT = {
|
|
24
|
+
'config.json': 'config',
|
|
25
|
+
'aliases.json': 'aliases',
|
|
26
|
+
'ai.json': 'ai',
|
|
27
|
+
'plugins.json': 'plugins'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a config file from ~/.devutils/ if it exists.
|
|
32
|
+
* Returns the parsed content, or null if the file is missing or invalid JSON.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} filename - The filename to read (e.g., 'config.json').
|
|
35
|
+
* @returns {object|null} The parsed JSON content, or null.
|
|
36
|
+
*/
|
|
37
|
+
function readConfigFile(filename) {
|
|
38
|
+
const filePath = path.join(DEVUTILS_DIR, filename);
|
|
39
|
+
if (!fs.existsSync(filePath)) return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write imported config data to ~/.devutils/.
|
|
49
|
+
* Only writes files that have data (skips null entries).
|
|
50
|
+
*
|
|
51
|
+
* @param {object} bundle - The import bundle with config, aliases, ai, plugins keys.
|
|
52
|
+
* @returns {number} The number of files written.
|
|
53
|
+
*/
|
|
54
|
+
function writeImportedFiles(bundle) {
|
|
55
|
+
fs.mkdirSync(DEVUTILS_DIR, { recursive: true });
|
|
56
|
+
|
|
57
|
+
let imported = 0;
|
|
58
|
+
for (const [filename, key] of Object.entries(FILES_TO_IMPORT)) {
|
|
59
|
+
const data = bundle[key];
|
|
60
|
+
if (data) {
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
path.join(DEVUTILS_DIR, filename),
|
|
63
|
+
JSON.stringify(data, null, 2) + '\n'
|
|
64
|
+
);
|
|
65
|
+
imported++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return imported;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Write the sync timestamp after a successful import.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} profileName - The profile that was imported.
|
|
76
|
+
*/
|
|
77
|
+
function writeSyncTimestamp(profileName) {
|
|
78
|
+
const syncPath = path.join(DEVUTILS_DIR, 'sync.json');
|
|
79
|
+
const syncData = {
|
|
80
|
+
lastSync: new Date().toISOString(),
|
|
81
|
+
direction: 'import',
|
|
82
|
+
profile: profileName
|
|
83
|
+
};
|
|
84
|
+
fs.writeFileSync(syncPath, JSON.stringify(syncData, null, 2) + '\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print the next-steps reminder after a successful import.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} context - CLI context (output).
|
|
91
|
+
*/
|
|
92
|
+
function printNextSteps(context) {
|
|
93
|
+
context.output.info('');
|
|
94
|
+
context.output.info('Next steps:');
|
|
95
|
+
context.output.info(' 1. Run "dev alias sync" to rebuild alias wrapper scripts');
|
|
96
|
+
context.output.info(' 2. Run "dev status" to verify your configuration');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run the config import command.
|
|
101
|
+
* Supports two modes:
|
|
102
|
+
* - File mode (--file): Reads a JSON export file from disk and writes config locally.
|
|
103
|
+
* - Remote mode (default): Pulls config from a GitHub repo or gist based on backup settings.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} args - Parsed CLI arguments (positional, flags).
|
|
106
|
+
* @param {object} context - CLI context (output, prompt, errors).
|
|
107
|
+
*/
|
|
108
|
+
async function run(args, context) {
|
|
109
|
+
const github = require('../../lib/github');
|
|
110
|
+
|
|
111
|
+
// --- File import mode ---
|
|
112
|
+
if (args.flags.file) {
|
|
113
|
+
const filePath = path.resolve(args.flags.file);
|
|
114
|
+
if (!fs.existsSync(filePath)) {
|
|
115
|
+
context.output.error(`File not found: ${filePath}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let bundle;
|
|
120
|
+
try {
|
|
121
|
+
bundle = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
context.output.error('Invalid JSON file: ' + err.message);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate the bundle has expected fields
|
|
128
|
+
if (!bundle.config) {
|
|
129
|
+
context.output.error('Invalid export file: missing config data.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const imported = writeImportedFiles(bundle);
|
|
134
|
+
const profileName = bundle.profile || 'default';
|
|
135
|
+
|
|
136
|
+
context.output.info(`Config imported from ${filePath} (${imported} file(s)).`);
|
|
137
|
+
writeSyncTimestamp(profileName);
|
|
138
|
+
printNextSteps(context);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Remote import mode ---
|
|
143
|
+
// Read current config to get backup settings
|
|
144
|
+
const config = readConfigFile('config.json');
|
|
145
|
+
if (!config || !config.backup) {
|
|
146
|
+
context.output.info('No backup storage configured.');
|
|
147
|
+
context.output.info('Run "dev config init" to set up backup storage, or use --file to import from a local file.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const backend = config.backup.backend;
|
|
152
|
+
const location = config.backup.location;
|
|
153
|
+
|
|
154
|
+
if (backend === 'repo') {
|
|
155
|
+
// Check gh authentication
|
|
156
|
+
const isAuth = await github.isAuthenticated();
|
|
157
|
+
if (!isAuth) {
|
|
158
|
+
context.output.info('Not authenticated with GitHub. Run: gh auth login');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cacheDir = path.join(DEVUTILS_DIR, 'cache');
|
|
163
|
+
const repoDir = path.join(cacheDir, 'config-backup');
|
|
164
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
// Clone or pull the repo
|
|
167
|
+
if (fs.existsSync(path.join(repoDir, '.git'))) {
|
|
168
|
+
const pullResult = await github.pullRepo(repoDir);
|
|
169
|
+
if (!pullResult.success) {
|
|
170
|
+
context.output.error('Failed to pull latest config: ' + pullResult.error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (!location) {
|
|
175
|
+
context.output.info('No backup repository configured.');
|
|
176
|
+
context.output.info('Run "dev config init --force" to set up a backup repository.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const cloneResult = await github.cloneRepo(location, repoDir);
|
|
181
|
+
if (!cloneResult.success) {
|
|
182
|
+
context.output.error('Failed to clone backup repo: ' + cloneResult.error);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// List available profiles
|
|
188
|
+
const profilesDir = path.join(repoDir, 'profiles');
|
|
189
|
+
if (!fs.existsSync(profilesDir)) {
|
|
190
|
+
context.output.info('No profiles found in backup repository.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const profiles = fs.readdirSync(profilesDir, { withFileTypes: true })
|
|
195
|
+
.filter(d => d.isDirectory())
|
|
196
|
+
.map(d => d.name);
|
|
197
|
+
|
|
198
|
+
if (profiles.length === 0) {
|
|
199
|
+
context.output.info('No profiles found in backup repository.');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Select profile
|
|
204
|
+
let profileName = args.flags.profile;
|
|
205
|
+
if (!profileName) {
|
|
206
|
+
if (profiles.length === 1) {
|
|
207
|
+
profileName = profiles[0];
|
|
208
|
+
} else {
|
|
209
|
+
// Let the user pick
|
|
210
|
+
profileName = await context.prompt.choose(
|
|
211
|
+
'Which profile do you want to import?',
|
|
212
|
+
profiles
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const profileDir = path.join(profilesDir, profileName);
|
|
218
|
+
if (!fs.existsSync(profileDir)) {
|
|
219
|
+
context.output.info(`Profile "${profileName}" not found in backup.`);
|
|
220
|
+
context.output.info(`Available profiles: ${profiles.join(', ')}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Copy config files from the profile directory into ~/.devutils/
|
|
225
|
+
const filesToCopy = ['config.json', 'aliases.json', 'ai.json', 'plugins.json'];
|
|
226
|
+
let imported = 0;
|
|
227
|
+
|
|
228
|
+
for (const filename of filesToCopy) {
|
|
229
|
+
const sourcePath = path.join(profileDir, filename);
|
|
230
|
+
if (fs.existsSync(sourcePath)) {
|
|
231
|
+
fs.copyFileSync(sourcePath, path.join(DEVUTILS_DIR, filename));
|
|
232
|
+
imported++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
context.output.info(`Imported ${imported} config file(s) from profile "${profileName}".`);
|
|
237
|
+
writeSyncTimestamp(profileName);
|
|
238
|
+
printNextSteps(context);
|
|
239
|
+
|
|
240
|
+
} else if (backend === 'gist') {
|
|
241
|
+
if (!location) {
|
|
242
|
+
context.output.info('No backup gist configured.');
|
|
243
|
+
context.output.info('Run "dev config init --force" to set up a backup gist.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check gh authentication
|
|
248
|
+
const isAuth = await github.isAuthenticated();
|
|
249
|
+
if (!isAuth) {
|
|
250
|
+
context.output.info('Not authenticated with GitHub. Run: gh auth login');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const gistResult = await github.getGist(location);
|
|
255
|
+
if (!gistResult.success) {
|
|
256
|
+
context.output.error('Failed to read gist: ' + gistResult.error);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// List available profiles (each is a .json file in the gist)
|
|
261
|
+
const profileFiles = Object.keys(gistResult.files).filter(f => f.endsWith('.json'));
|
|
262
|
+
const profiles = profileFiles.map(f => f.replace('.json', ''));
|
|
263
|
+
|
|
264
|
+
if (profiles.length === 0) {
|
|
265
|
+
context.output.info('No profiles found in backup gist.');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let profileName = args.flags.profile;
|
|
270
|
+
if (!profileName) {
|
|
271
|
+
if (profiles.length === 1) {
|
|
272
|
+
profileName = profiles[0];
|
|
273
|
+
} else {
|
|
274
|
+
profileName = await context.prompt.choose(
|
|
275
|
+
'Which profile do you want to import?',
|
|
276
|
+
profiles
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const filename = profileName + '.json';
|
|
282
|
+
const content = gistResult.files[filename];
|
|
283
|
+
if (!content) {
|
|
284
|
+
context.output.info(`Profile "${profileName}" not found in gist.`);
|
|
285
|
+
context.output.info(`Available profiles: ${profiles.join(', ')}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let bundle;
|
|
290
|
+
try {
|
|
291
|
+
bundle = JSON.parse(content);
|
|
292
|
+
} catch {
|
|
293
|
+
context.output.error('Failed to parse profile data from gist.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const imported = writeImportedFiles(bundle);
|
|
298
|
+
context.output.info(`Imported ${imported} config file(s) from profile "${profileName}".`);
|
|
299
|
+
writeSyncTimestamp(profileName);
|
|
300
|
+
printNextSteps(context);
|
|
301
|
+
|
|
302
|
+
} else {
|
|
303
|
+
context.output.info(`Unknown backup backend: ${backend}`);
|
|
304
|
+
context.output.info('Run "dev config init --force" to reconfigure backup storage.');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config service registration.
|
|
3
|
+
* User configuration and onboarding.
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'config',
|
|
7
|
+
description: 'User configuration and onboarding',
|
|
8
|
+
commands: {
|
|
9
|
+
init: () => require('./init'),
|
|
10
|
+
show: () => require('./show'),
|
|
11
|
+
get: () => require('./get'),
|
|
12
|
+
set: () => require('./set'),
|
|
13
|
+
reset: () => require('./reset'),
|
|
14
|
+
export: () => require('./export'),
|
|
15
|
+
import: () => require('./import'),
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const DEVUTILS_DIR = path.join(os.homedir(), '.devutils');
|
|
8
|
+
const CONFIG_FILE = path.join(DEVUTILS_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
description: 'First-run onboarding wizard. Sets up ~/.devutils/ and creates config.json.',
|
|
12
|
+
arguments: [],
|
|
13
|
+
flags: [
|
|
14
|
+
{ name: 'force', type: 'boolean', description: 'Re-run setup even if already configured' },
|
|
15
|
+
{ name: 'profile', type: 'string', description: 'Set the profile name (skip the prompt)' },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detects the user's shell config file path for PATH modification.
|
|
21
|
+
* @returns {{ shell: string, file: string }|null}
|
|
22
|
+
*/
|
|
23
|
+
function getShellConfig() {
|
|
24
|
+
const shell = process.env.SHELL || '';
|
|
25
|
+
const home = os.homedir();
|
|
26
|
+
|
|
27
|
+
if (shell.includes('zsh')) {
|
|
28
|
+
return { shell: 'zsh', file: path.join(home, '.zshrc') };
|
|
29
|
+
}
|
|
30
|
+
if (shell.includes('bash')) {
|
|
31
|
+
return { shell: 'bash', file: path.join(home, '.bashrc') };
|
|
32
|
+
}
|
|
33
|
+
if (shell.includes('fish')) {
|
|
34
|
+
return { shell: 'fish', file: path.join(home, '.config', 'fish', 'config.fish') };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Adds the devutils bin directory to the user's PATH via shell config.
|
|
41
|
+
* Idempotent: checks if the line is already present before adding.
|
|
42
|
+
* @param {string} shellFile - The shell config file path.
|
|
43
|
+
*/
|
|
44
|
+
function addToPathFile(shellFile) {
|
|
45
|
+
const exportLine = 'export PATH="$HOME/.devutils/bin:$PATH"';
|
|
46
|
+
|
|
47
|
+
// Create the file if it doesn't exist
|
|
48
|
+
if (!fs.existsSync(shellFile)) {
|
|
49
|
+
fs.mkdirSync(path.dirname(shellFile), { recursive: true });
|
|
50
|
+
fs.writeFileSync(shellFile, '', 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = fs.readFileSync(shellFile, 'utf8');
|
|
54
|
+
|
|
55
|
+
// Check if the line is already there
|
|
56
|
+
if (content.includes('.devutils/bin')) {
|
|
57
|
+
return false; // Already present
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fs.appendFileSync(shellFile, '\n# DevUtils CLI\n' + exportLine + '\n');
|
|
61
|
+
return true; // Added
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run(args, context) {
|
|
65
|
+
// Check if already configured
|
|
66
|
+
if (fs.existsSync(CONFIG_FILE) && !args.flags.force) {
|
|
67
|
+
context.output.info('DevUtils is already configured. Use --force to re-run setup.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create the directory structure
|
|
72
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'machines'), { recursive: true });
|
|
73
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'bin'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'auth'), { recursive: true });
|
|
75
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'plugins'), { recursive: true });
|
|
76
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'utils'), { recursive: true });
|
|
77
|
+
fs.mkdirSync(path.join(DEVUTILS_DIR, 'cache'), { recursive: true });
|
|
78
|
+
|
|
79
|
+
// Prompt for user info
|
|
80
|
+
const name = await context.prompt.ask('Your full name', '');
|
|
81
|
+
const email = await context.prompt.ask('Your email address', '');
|
|
82
|
+
const url = await context.prompt.ask('Your URL (optional)', '');
|
|
83
|
+
|
|
84
|
+
// Prompt for backup backend
|
|
85
|
+
const backupBackend = await context.prompt.choose(
|
|
86
|
+
'Where should DevUtils store configuration backups?',
|
|
87
|
+
['repo', 'gist'],
|
|
88
|
+
0
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Prompt for profile name
|
|
92
|
+
const profile = args.flags.profile || await context.prompt.ask('Profile name for this machine', 'default');
|
|
93
|
+
|
|
94
|
+
// Build and write config
|
|
95
|
+
const config = {
|
|
96
|
+
user: {
|
|
97
|
+
name: name,
|
|
98
|
+
email: email,
|
|
99
|
+
url: url,
|
|
100
|
+
},
|
|
101
|
+
defaults: {
|
|
102
|
+
license: 'MIT',
|
|
103
|
+
packageManager: 'npm',
|
|
104
|
+
},
|
|
105
|
+
backup: {
|
|
106
|
+
backend: backupBackend,
|
|
107
|
+
location: null,
|
|
108
|
+
},
|
|
109
|
+
profile: profile,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
|
|
113
|
+
|
|
114
|
+
// Offer to add ~/.devutils/bin to PATH
|
|
115
|
+
const shellConfig = getShellConfig();
|
|
116
|
+
if (shellConfig) {
|
|
117
|
+
const addToPath = await context.prompt.confirm(
|
|
118
|
+
'Add ~/.devutils/bin to your PATH? (required for aliases to work)',
|
|
119
|
+
true
|
|
120
|
+
);
|
|
121
|
+
if (addToPath) {
|
|
122
|
+
const added = addToPathFile(shellConfig.file);
|
|
123
|
+
if (added) {
|
|
124
|
+
context.output.info(`Added to ${shellConfig.file}. Restart your terminal or run: source ${shellConfig.file}`);
|
|
125
|
+
} else {
|
|
126
|
+
context.output.info('PATH entry already exists. No changes made.');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Print summary
|
|
132
|
+
context.output.info('');
|
|
133
|
+
context.output.info('DevUtils configured successfully!');
|
|
134
|
+
context.output.info('');
|
|
135
|
+
context.output.info(` Name: ${name || '(not set)'}`);
|
|
136
|
+
context.output.info(` Email: ${email || '(not set)'}`);
|
|
137
|
+
context.output.info(` Profile: ${profile}`);
|
|
138
|
+
context.output.info(` Backup: ${backupBackend}`);
|
|
139
|
+
context.output.info(` Config: ${CONFIG_FILE}`);
|
|
140
|
+
context.output.info('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = path.join(os.homedir(), '.devutils', 'config.json');
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
description: 'Reset configuration to defaults. Clears user info and restores default settings.',
|
|
11
|
+
arguments: [],
|
|
12
|
+
flags: [
|
|
13
|
+
{ name: 'confirm', type: 'boolean', description: 'Skip the confirmation prompt' },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function run(args, context) {
|
|
18
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
19
|
+
context.errors.throwError(404, 'Config not found. Nothing to reset.', 'config');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Ask for confirmation unless --confirm is passed
|
|
24
|
+
if (!args.flags.confirm) {
|
|
25
|
+
const proceed = await context.prompt.confirm(
|
|
26
|
+
'This will reset all configuration to defaults. Your user info will be cleared. Continue?',
|
|
27
|
+
false
|
|
28
|
+
);
|
|
29
|
+
if (!proceed) {
|
|
30
|
+
context.output.info('Reset cancelled.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Write default config
|
|
36
|
+
const defaults = {
|
|
37
|
+
user: {
|
|
38
|
+
name: '',
|
|
39
|
+
email: '',
|
|
40
|
+
url: '',
|
|
41
|
+
},
|
|
42
|
+
defaults: {
|
|
43
|
+
license: 'MIT',
|
|
44
|
+
packageManager: 'npm',
|
|
45
|
+
},
|
|
46
|
+
backup: {
|
|
47
|
+
backend: null,
|
|
48
|
+
location: null,
|
|
49
|
+
},
|
|
50
|
+
profile: 'default',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2) + '\n');
|
|
54
|
+
context.output.info('Configuration reset to defaults.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = path.join(os.homedir(), '.devutils', 'config.json');
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
description: 'Write a config value by dot-notation key.',
|
|
11
|
+
arguments: [
|
|
12
|
+
{ name: 'key', required: true, description: 'Dot-notation path to the config value (e.g., user.email)' },
|
|
13
|
+
{ name: 'value', required: false, description: 'The value to set. Omit if using --json.' },
|
|
14
|
+
],
|
|
15
|
+
flags: [
|
|
16
|
+
{ name: 'json', type: 'string', description: 'Set a structured value using a JSON string' },
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function run(args, context) {
|
|
21
|
+
const key = args.positional[0];
|
|
22
|
+
const rawValue = args.positional[1];
|
|
23
|
+
const jsonValue = args.flags.json;
|
|
24
|
+
|
|
25
|
+
if (!key) {
|
|
26
|
+
context.errors.throwError(400, 'Missing required argument: <key>. Example: dev config set user.email fred@example.com', 'config');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!rawValue && rawValue !== '' && !jsonValue) {
|
|
31
|
+
context.errors.throwError(400, 'Missing value. Provide a value or use --json for structured data.', 'config');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (rawValue && jsonValue) {
|
|
36
|
+
context.errors.throwError(400, 'Provide either a positional value or --json, not both.', 'config');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse the value
|
|
41
|
+
let value;
|
|
42
|
+
if (jsonValue) {
|
|
43
|
+
try {
|
|
44
|
+
value = JSON.parse(jsonValue);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
context.errors.throwError(400, `Invalid JSON: ${err.message}`, 'config');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Coerce simple types
|
|
51
|
+
if (rawValue === 'true') {
|
|
52
|
+
value = true;
|
|
53
|
+
} else if (rawValue === 'false') {
|
|
54
|
+
value = false;
|
|
55
|
+
} else if (rawValue === 'null') {
|
|
56
|
+
value = null;
|
|
57
|
+
} else if (!isNaN(rawValue) && rawValue.trim() !== '') {
|
|
58
|
+
value = Number(rawValue);
|
|
59
|
+
} else {
|
|
60
|
+
value = rawValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read existing config
|
|
65
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
66
|
+
context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'config');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
71
|
+
const config = JSON.parse(raw);
|
|
72
|
+
|
|
73
|
+
// Set value by dot-notation key
|
|
74
|
+
const parts = key.split('.');
|
|
75
|
+
let target = config;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
78
|
+
const part = parts[i];
|
|
79
|
+
if (!(part in target) || typeof target[part] !== 'object' || target[part] === null) {
|
|
80
|
+
target[part] = {};
|
|
81
|
+
}
|
|
82
|
+
target = target[part];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lastPart = parts[parts.length - 1];
|
|
86
|
+
target[lastPart] = value;
|
|
87
|
+
|
|
88
|
+
// Write config back
|
|
89
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
|
|
90
|
+
context.output.out({ key: key, value: value });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
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 CONFIG_FILE = path.join(os.homedir(), '.devutils', 'config.json');
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
description: 'Display the current configuration.',
|
|
11
|
+
arguments: [],
|
|
12
|
+
flags: [
|
|
13
|
+
{ name: 'profile', type: 'string', description: 'Show a specific profile instead of the active one' },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function run(args, context) {
|
|
18
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
19
|
+
context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'config');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
24
|
+
const config = JSON.parse(raw);
|
|
25
|
+
|
|
26
|
+
// Handle --profile flag
|
|
27
|
+
if (args.flags.profile && args.flags.profile !== config.profile) {
|
|
28
|
+
context.errors.throwError(404, `Profile "${args.flags.profile}" not found. Current profile is "${config.profile}".`, 'config');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
context.output.out(config);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { meta, run };
|