@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,338 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * help command.
5
+ * Lists all services and top-level commands, or shows detailed help
6
+ * for a specific service or command when given arguments.
7
+ *
8
+ * Examples:
9
+ * dev help - List all services and top-level commands
10
+ * dev help config - List all commands in the config service
11
+ * dev help config set - Show detailed help for config set
12
+ * dev help version - Show help for the version command
13
+ */
14
+
15
+ const path = require('path');
16
+
17
+ const meta = {
18
+ description: 'Show usage information',
19
+ arguments: [
20
+ { name: 'command', description: 'Command path to get help for (e.g., config set)', required: false, variadic: true }
21
+ ],
22
+ flags: []
23
+ };
24
+
25
+ /**
26
+ * Known service directory names. Each one has an index.js that exports
27
+ * name, description, and a commands map.
28
+ * @type {string[]}
29
+ */
30
+ const SERVICE_NAMES = [
31
+ 'config', 'machine', 'identity', 'tools', 'ignore',
32
+ 'util', 'alias', 'auth', 'api', 'ai', 'search'
33
+ ];
34
+
35
+ /**
36
+ * Top-level commands (not inside a service folder). Each entry has a
37
+ * name and a description. Descriptions are read from the command's
38
+ * meta export when available, with fallbacks for stubs that haven't
39
+ * been filled in yet.
40
+ * @type {Array<{ name: string, fallback: string }>}
41
+ */
42
+ const TOP_LEVEL_COMMANDS = [
43
+ { name: 'status', fallback: 'Overall health check' },
44
+ { name: 'update', fallback: 'Update DevUtils CLI' },
45
+ { name: 'version', fallback: 'Show the current installed version' },
46
+ { name: 'schema', fallback: 'Show or validate config schema' },
47
+ { name: 'help', fallback: 'Show usage information' },
48
+ ];
49
+
50
+ /**
51
+ * Loads a service index.js and returns { name, description, commands }.
52
+ * Returns null if the service cannot be loaded.
53
+ *
54
+ * @param {string} serviceName - The service directory name (e.g., 'config').
55
+ * @returns {object|null}
56
+ */
57
+ function loadService(serviceName) {
58
+ try {
59
+ return require(path.join(__dirname, serviceName, 'index'));
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Loads a top-level command module and returns it.
67
+ * Returns null if the module cannot be loaded.
68
+ *
69
+ * @param {string} commandName - The command file name (e.g., 'version').
70
+ * @returns {object|null}
71
+ */
72
+ function loadTopLevelCommand(commandName) {
73
+ try {
74
+ return require(path.join(__dirname, commandName));
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Gets the description for a top-level command. Reads from the command's
82
+ * meta.description if available, otherwise uses the fallback.
83
+ *
84
+ * @param {{ name: string, fallback: string }} entry - The top-level command entry.
85
+ * @returns {string}
86
+ */
87
+ function getTopLevelDescription(entry) {
88
+ const cmd = loadTopLevelCommand(entry.name);
89
+ if (cmd && cmd.meta && cmd.meta.description) {
90
+ return cmd.meta.description;
91
+ }
92
+ return entry.fallback;
93
+ }
94
+
95
+ /**
96
+ * Builds a list of all services with their names and descriptions.
97
+ *
98
+ * @returns {Array<{ name: string, description: string }>}
99
+ */
100
+ function buildServiceList() {
101
+ const services = [];
102
+ for (const name of SERVICE_NAMES) {
103
+ const svc = loadService(name);
104
+ if (svc) {
105
+ services.push({ name: svc.name, description: svc.description });
106
+ }
107
+ }
108
+ return services;
109
+ }
110
+
111
+ /**
112
+ * Builds a list of all top-level commands with their names and descriptions.
113
+ *
114
+ * @returns {Array<{ name: string, description: string }>}
115
+ */
116
+ function buildTopLevelCommandList() {
117
+ return TOP_LEVEL_COMMANDS.map(entry => ({
118
+ name: entry.name,
119
+ description: getTopLevelDescription(entry),
120
+ }));
121
+ }
122
+
123
+ /**
124
+ * Pads a string to the given width with trailing spaces.
125
+ *
126
+ * @param {string} str - The string to pad.
127
+ * @param {number} width - The target width.
128
+ * @returns {string}
129
+ */
130
+ function pad(str, width) {
131
+ return str + ' '.repeat(Math.max(0, width - str.length));
132
+ }
133
+
134
+ /**
135
+ * Shows the top-level help listing: all services and top-level commands.
136
+ *
137
+ * @param {object} context - The CLI context object.
138
+ */
139
+ function showTopLevelHelp(context) {
140
+ const services = buildServiceList();
141
+ const commands = buildTopLevelCommandList();
142
+
143
+ if (context.flags.format === 'json') {
144
+ context.output.out({ services, commands });
145
+ return;
146
+ }
147
+
148
+ // Find the longest name for alignment
149
+ const allNames = [...services.map(s => s.name), ...commands.map(c => c.name)];
150
+ const maxLen = Math.max(...allNames.map(n => n.length));
151
+ const colWidth = maxLen + 4;
152
+
153
+ context.output.info('Usage: dev <service> <command> [arguments] [flags]');
154
+ context.output.info('');
155
+ context.output.info('Services:');
156
+ for (const svc of services) {
157
+ context.output.info(` ${pad(svc.name, colWidth)}${svc.description}`);
158
+ }
159
+ context.output.info('');
160
+ context.output.info('Commands:');
161
+ for (const cmd of commands) {
162
+ context.output.info(` ${pad(cmd.name, colWidth)}${cmd.description}`);
163
+ }
164
+ context.output.info('');
165
+ context.output.info('Run "dev help <service>" to see commands within a service.');
166
+ context.output.info('Run "dev help <service> <command>" for detailed command help.');
167
+ }
168
+
169
+ /**
170
+ * Shows help for a specific service, listing all its commands.
171
+ *
172
+ * @param {object} service - The service module (with name, description, commands).
173
+ * @param {object} context - The CLI context object.
174
+ */
175
+ function showServiceHelp(service, context) {
176
+ const commandNames = Object.keys(service.commands || {});
177
+ const commandList = [];
178
+
179
+ for (const name of commandNames) {
180
+ let description = '';
181
+ try {
182
+ const cmd = service.commands[name]();
183
+ if (cmd && cmd.meta && cmd.meta.description) {
184
+ description = cmd.meta.description;
185
+ }
186
+ } catch {
187
+ // Command module may not be fully implemented yet
188
+ }
189
+ commandList.push({ name, description });
190
+ }
191
+
192
+ if (context.flags.format === 'json') {
193
+ context.output.out({
194
+ service: service.name,
195
+ description: service.description,
196
+ commands: commandList,
197
+ });
198
+ return;
199
+ }
200
+
201
+ const maxLen = Math.max(...commandList.map(c => c.name.length));
202
+ const colWidth = maxLen + 4;
203
+
204
+ context.output.info(`${service.name}: ${service.description}`);
205
+ context.output.info('');
206
+ context.output.info('Commands:');
207
+ for (const cmd of commandList) {
208
+ const desc = cmd.description ? `${pad(cmd.name, colWidth)}${cmd.description}` : cmd.name;
209
+ context.output.info(` dev ${service.name} ${desc}`);
210
+ }
211
+ context.output.info('');
212
+ context.output.info(`Run "dev help ${service.name} <command>" for detailed help.`);
213
+ }
214
+
215
+ /**
216
+ * Shows detailed help for a specific command, including its description,
217
+ * arguments, and flags.
218
+ *
219
+ * @param {object} command - The command module (with meta.description, meta.arguments, meta.flags).
220
+ * @param {string} commandPath - The full command path for display (e.g., "config set").
221
+ * @param {object} context - The CLI context object.
222
+ */
223
+ function showCommandHelp(command, commandPath, context) {
224
+ const m = command.meta || {};
225
+ const args = m.arguments || [];
226
+ const flags = m.flags || [];
227
+
228
+ if (context.flags.format === 'json') {
229
+ context.output.out({
230
+ command: commandPath,
231
+ description: m.description || '',
232
+ arguments: args,
233
+ flags: flags,
234
+ });
235
+ return;
236
+ }
237
+
238
+ context.output.info(`dev ${commandPath}`);
239
+ context.output.info('');
240
+ if (m.description) {
241
+ context.output.info(` ${m.description}`);
242
+ context.output.info('');
243
+ }
244
+
245
+ if (args.length > 0) {
246
+ context.output.info('Arguments:');
247
+ const maxLen = Math.max(...args.map(a => a.name.length));
248
+ const colWidth = maxLen + 4;
249
+ for (const arg of args) {
250
+ const req = arg.required ? '(required)' : '(optional)';
251
+ context.output.info(` ${pad(arg.name, colWidth)}${arg.description || ''} ${req}`);
252
+ }
253
+ context.output.info('');
254
+ }
255
+
256
+ if (flags.length > 0) {
257
+ context.output.info('Flags:');
258
+ const maxLen = Math.max(...flags.map(f => `--${f.name}`.length));
259
+ const colWidth = maxLen + 4;
260
+ for (const flag of flags) {
261
+ const name = `--${flag.name}`;
262
+ const parts = [flag.description || ''];
263
+ if (flag.type) {
264
+ parts.push(`(${flag.type})`);
265
+ }
266
+ if (flag.default !== undefined) {
267
+ parts.push(`[default: ${flag.default}]`);
268
+ }
269
+ context.output.info(` ${pad(name, colWidth)}${parts.join(' ')}`);
270
+ }
271
+ context.output.info('');
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Main entry point for the help command.
277
+ *
278
+ * @param {object} args - Parsed command arguments. Positional args are the command path.
279
+ * @param {object} context - The CLI context object.
280
+ */
281
+ async function run(args, context) {
282
+ const positional = args.positional || [];
283
+
284
+ // No arguments: show top-level help
285
+ if (positional.length === 0) {
286
+ showTopLevelHelp(context);
287
+ return;
288
+ }
289
+
290
+ const firstName = positional[0];
291
+ const secondName = positional[1];
292
+
293
+ // Check if the first argument is a service name
294
+ const service = loadService(firstName);
295
+ if (service) {
296
+ if (!secondName) {
297
+ // Show service-level help (list all commands in the service)
298
+ showServiceHelp(service, context);
299
+ return;
300
+ }
301
+
302
+ // Show help for a specific command within the service
303
+ if (service.commands && service.commands[secondName]) {
304
+ try {
305
+ const cmd = service.commands[secondName]();
306
+ showCommandHelp(cmd, `${firstName} ${secondName}`, context);
307
+ return;
308
+ } catch {
309
+ // Fall through to unknown command error
310
+ }
311
+ }
312
+
313
+ // Unknown command within the service
314
+ const methods = Object.keys(service.commands || {}).join(', ');
315
+ context.errors.throwError(
316
+ 404,
317
+ `Unknown command "${secondName}" for service "${firstName}". Available commands: ${methods}`,
318
+ 'help'
319
+ );
320
+ return;
321
+ }
322
+
323
+ // Check if the first argument is a top-level command
324
+ const topCmd = loadTopLevelCommand(firstName);
325
+ if (topCmd) {
326
+ showCommandHelp(topCmd, firstName, context);
327
+ return;
328
+ }
329
+
330
+ // Unknown command
331
+ context.errors.throwError(
332
+ 404,
333
+ `Unknown command "${firstName}". Run "dev help" to see available commands.`,
334
+ 'help'
335
+ );
336
+ }
337
+
338
+ module.exports = { meta, run };
@@ -0,0 +1,133 @@
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: 'Create a new identity profile.',
11
+ arguments: [
12
+ { name: 'name', description: 'Short name for this identity (e.g., personal, work)', required: true },
13
+ ],
14
+ flags: [
15
+ { name: 'email', description: 'Git author email (required)' },
16
+ { name: 'ssh-key', description: 'Path to SSH private key file' },
17
+ { name: 'gpg-key', description: 'GPG key ID for commit signing' },
18
+ { name: 'generate-key', description: 'Generate a new SSH key pair for this identity' },
19
+ ],
20
+ };
21
+
22
+ async function run(args, context) {
23
+ const identityName = args.positional[0];
24
+ if (!identityName) {
25
+ context.errors.throwError(400, 'Missing identity name. Usage: dev identity add <name> --email <email>', 'identity');
26
+ return;
27
+ }
28
+
29
+ // Validate name format
30
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(identityName)) {
31
+ context.errors.throwError(400, 'Identity name must be lowercase letters, numbers, and hyphens only.', 'identity');
32
+ return;
33
+ }
34
+
35
+ const email = args.flags.email;
36
+ if (!email) {
37
+ context.errors.throwError(400, 'Email is required. Use --email your@email.com.', 'identity');
38
+ return;
39
+ }
40
+
41
+ // Load config
42
+ if (!fs.existsSync(CONFIG_FILE)) {
43
+ context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'identity');
44
+ return;
45
+ }
46
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
47
+ const identities = config.identities || [];
48
+
49
+ // Check for duplicates (case-insensitive)
50
+ if (identities.some(id => id.name.toLowerCase() === identityName.toLowerCase())) {
51
+ context.errors.throwError(400, `Identity '${identityName}' already exists. Use a different name or remove it first.`, 'identity');
52
+ return;
53
+ }
54
+
55
+ // Handle SSH key
56
+ let sshKeyPath = null;
57
+ const sshKeyFlag = args.flags['ssh-key'];
58
+ const generateKey = args.flags['generate-key'];
59
+
60
+ if (sshKeyFlag && generateKey) {
61
+ context.errors.throwError(400, 'Provide either --ssh-key or --generate-key, not both.', 'identity');
62
+ return;
63
+ }
64
+
65
+ if (sshKeyFlag) {
66
+ const resolved = path.resolve(sshKeyFlag);
67
+ if (!fs.existsSync(resolved)) {
68
+ context.errors.throwError(404, `SSH key file not found: ${resolved}`, 'identity');
69
+ return;
70
+ }
71
+ sshKeyPath = resolved;
72
+ } else if (generateKey) {
73
+ sshKeyPath = await generateSshKey(identityName, email, context);
74
+ if (!sshKeyPath) return; // Error already reported
75
+ }
76
+
77
+ // Handle GPG key
78
+ const gpgKey = args.flags['gpg-key'] || null;
79
+
80
+ // Build identity
81
+ const identity = {
82
+ name: identityName,
83
+ email: email,
84
+ sshKey: sshKeyPath,
85
+ gpgKey: gpgKey,
86
+ folders: [],
87
+ };
88
+
89
+ // Save
90
+ identities.push(identity);
91
+ config.identities = identities;
92
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
93
+
94
+ context.output.info(`Identity '${identityName}' created.`);
95
+ context.output.out(identity);
96
+ }
97
+
98
+ /**
99
+ * Generates an ED25519 SSH key pair for the given identity.
100
+ * @param {string} name - The identity name (used in the key filename).
101
+ * @param {string} email - The email (used as the key comment).
102
+ * @param {object} context - The command context.
103
+ * @returns {Promise<string|null>} The key path, or null on failure.
104
+ */
105
+ async function generateSshKey(name, email, context) {
106
+ const sshDir = path.join(os.homedir(), '.ssh');
107
+ if (!fs.existsSync(sshDir)) {
108
+ fs.mkdirSync(sshDir, { mode: 0o700 });
109
+ }
110
+
111
+ const keyPath = path.join(sshDir, `id_ed25519_${name}`);
112
+ if (fs.existsSync(keyPath)) {
113
+ context.errors.throwError(400, `SSH key already exists at ${keyPath}. Use --ssh-key ${keyPath} to reference it.`, 'identity');
114
+ return null;
115
+ }
116
+
117
+ const result = await context.shell.exec(
118
+ `ssh-keygen -t ed25519 -C "${email}" -f "${keyPath}" -N ""`
119
+ );
120
+
121
+ if (result.exitCode !== 0) {
122
+ context.errors.throwError(500, `Failed to generate SSH key: ${result.stderr}`, 'identity');
123
+ return null;
124
+ }
125
+
126
+ context.output.info(`SSH key generated: ${keyPath}`);
127
+ context.output.info(`Public key: ${keyPath}.pub`);
128
+ context.output.info('Add the public key to your GitHub account at https://github.com/settings/keys');
129
+
130
+ return keyPath;
131
+ }
132
+
133
+ module.exports = { meta, run };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Identity service registration.
3
+ * Git identities, SSH keys, GPG signing.
4
+ */
5
+ module.exports = {
6
+ name: 'identity',
7
+ description: 'Git identities, SSH keys, GPG signing',
8
+ commands: {
9
+ add: () => require('./add'),
10
+ remove: () => require('./remove'),
11
+ list: () => require('./list'),
12
+ show: () => require('./show'),
13
+ link: () => require('./link'),
14
+ unlink: () => require('./unlink'),
15
+ sync: () => require('./sync'),
16
+ }
17
+ };
@@ -0,0 +1,76 @@
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: 'Link an identity to a folder path.',
11
+ arguments: [
12
+ { name: 'name', description: 'Identity name', required: true },
13
+ { name: 'folder', description: 'Folder path to link (absolute or relative)', required: true },
14
+ ],
15
+ flags: [
16
+ { name: 'remote', description: 'Git remote hostname (default: github.com)' },
17
+ ],
18
+ };
19
+
20
+ async function run(args, context) {
21
+ const name = args.positional[0];
22
+ const folder = args.positional[1];
23
+
24
+ if (!name || !folder) {
25
+ context.errors.throwError(400, 'Usage: dev identity link <name> <folder> [--remote <host>]', 'identity');
26
+ return;
27
+ }
28
+
29
+ const absolutePath = path.resolve(folder);
30
+ if (!fs.existsSync(absolutePath)) {
31
+ context.errors.throwError(404, `Folder not found: ${absolutePath}`, 'identity');
32
+ return;
33
+ }
34
+
35
+ if (!fs.existsSync(CONFIG_FILE)) {
36
+ context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'identity');
37
+ return;
38
+ }
39
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
40
+ const identities = config.identities || [];
41
+
42
+ // Find the target identity
43
+ const identity = identities.find(id => id.name.toLowerCase() === name.toLowerCase());
44
+ if (!identity) {
45
+ context.errors.throwError(404, `Identity '${name}' not found.`, 'identity');
46
+ return;
47
+ }
48
+
49
+ const remote = args.flags.remote || 'github.com';
50
+
51
+ // Check if folder is linked to a different identity
52
+ for (const id of identities) {
53
+ const existing = (id.folders || []).find(f => f.path === absolutePath);
54
+ if (existing && id.name.toLowerCase() !== name.toLowerCase()) {
55
+ context.output.info(`Warning: Folder '${absolutePath}' is currently linked to identity '${id.name}'. Re-linking to '${identity.name}'.`);
56
+ }
57
+ }
58
+
59
+ // Remove folder from all identities (avoid duplicates)
60
+ for (const id of identities) {
61
+ id.folders = (id.folders || []).filter(f => f.path !== absolutePath);
62
+ }
63
+
64
+ // Add the link
65
+ identity.folders = identity.folders || [];
66
+ identity.folders.push({ path: absolutePath, remote });
67
+
68
+ // Save
69
+ config.identities = identities;
70
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
71
+
72
+ context.output.info(`Linked '${absolutePath}' to identity '${identity.name}' (remote: ${remote}).`);
73
+ context.output.info("Run 'dev identity sync' to apply changes to SSH and git config.");
74
+ }
75
+
76
+ module.exports = { meta, run };
@@ -0,0 +1,48 @@
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: 'List all configured identities.',
11
+ arguments: [],
12
+ flags: [],
13
+ };
14
+
15
+ async function run(args, context) {
16
+ if (!fs.existsSync(CONFIG_FILE)) {
17
+ context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'identity');
18
+ return;
19
+ }
20
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
21
+ const identities = config.identities || [];
22
+
23
+ if (identities.length === 0) {
24
+ context.output.info('No identities configured.');
25
+ context.output.info('');
26
+ context.output.info('Create one with:');
27
+ context.output.info(' dev identity add <name> --email <email>');
28
+ context.output.info('');
29
+ context.output.info('Example:');
30
+ context.output.info(' dev identity add personal --email fred@example.com');
31
+ return;
32
+ }
33
+
34
+ // Build table rows
35
+ const rows = identities.map(id => ({
36
+ Name: id.name,
37
+ Email: id.email,
38
+ 'SSH Key': id.sshKey ? 'yes' : 'no',
39
+ 'GPG Key': id.gpgKey ? 'yes' : 'no',
40
+ 'Linked Folders': (id.folders || []).length,
41
+ }));
42
+
43
+ context.output.out(rows);
44
+ const count = identities.length;
45
+ context.output.info(`\n${count} ${count === 1 ? 'identity' : 'identities'} configured.`);
46
+ }
47
+
48
+ module.exports = { meta, run };
@@ -0,0 +1,72 @@
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: 'Remove an identity profile.',
11
+ arguments: [
12
+ { name: 'name', description: 'Name of the identity to remove', required: true },
13
+ ],
14
+ flags: [
15
+ { name: 'confirm', description: 'Skip the confirmation prompt' },
16
+ { name: 'force', description: 'Remove even if the identity has linked folders' },
17
+ ],
18
+ };
19
+
20
+ async function run(args, context) {
21
+ const name = args.positional[0];
22
+ if (!name) {
23
+ context.errors.throwError(400, 'Missing identity name. Usage: dev identity remove <name>', 'identity');
24
+ return;
25
+ }
26
+
27
+ if (!fs.existsSync(CONFIG_FILE)) {
28
+ context.errors.throwError(404, 'Config not found. Run "dev config init" first.', 'identity');
29
+ return;
30
+ }
31
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
32
+ const identities = config.identities || [];
33
+
34
+ // Find the identity
35
+ const identity = identities.find(id => id.name.toLowerCase() === name.toLowerCase());
36
+ if (!identity) {
37
+ context.errors.throwError(404, `Identity '${name}' not found.`, 'identity');
38
+ return;
39
+ }
40
+
41
+ // Check for linked folders
42
+ const folders = identity.folders || [];
43
+ if (folders.length > 0 && !args.flags.force) {
44
+ const folderList = folders.map(f => ` ${f.path || f}`).join('\n');
45
+ context.errors.throwError(
46
+ 400,
47
+ `Cannot remove identity '${identity.name}' because it has ${folders.length} linked folder(s):\n${folderList}\nUse --force to remove anyway, or unlink the folders first.`,
48
+ 'identity'
49
+ );
50
+ return;
51
+ }
52
+
53
+ // Confirm
54
+ if (!args.flags.confirm) {
55
+ const proceed = await context.prompt.confirm(`Remove identity '${identity.name}'? This cannot be undone.`, false);
56
+ if (!proceed) {
57
+ context.output.info('Cancelled.');
58
+ return;
59
+ }
60
+ }
61
+
62
+ // Remove from array
63
+ config.identities = identities.filter(id => id.name.toLowerCase() !== name.toLowerCase());
64
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
65
+
66
+ context.output.info(`Identity '${identity.name}' removed.`);
67
+ if (identity.sshKey) {
68
+ context.output.info(`Note: SSH key at ${identity.sshKey} was not deleted. Remove it manually if no longer needed.`);
69
+ }
70
+ }
71
+
72
+ module.exports = { meta, run };