@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,41 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * Platform name identifier.
7
+ * @type {string}
8
+ */
9
+ const name = 'raspbian';
10
+
11
+ /**
12
+ * Default package manager for Raspberry Pi OS (Raspbian).
13
+ * Raspbian is Debian-based, so it uses apt just like Ubuntu.
14
+ * @type {string}
15
+ */
16
+ const packageManager = 'apt';
17
+
18
+ /**
19
+ * Checks if a binary is available on the system PATH.
20
+ * @param {string} binary - The name of the binary to look for (e.g. 'node', 'git').
21
+ * @returns {boolean} True if the binary exists on the PATH, false otherwise.
22
+ */
23
+ function isInstalled(binary) {
24
+ try {
25
+ execFileSync('which', [binary], { stdio: 'ignore' });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns common application directories for Raspberry Pi OS.
34
+ * Same as Ubuntu since Raspbian is Debian-based.
35
+ * @returns {string[]} Array of directory paths where applications are typically installed.
36
+ */
37
+ function getAppPaths() {
38
+ return ['/usr/bin', '/usr/local/bin', '/snap/bin'];
39
+ }
40
+
41
+ module.exports = { name, packageManager, isInstalled, getAppPaths };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * Platform name identifier.
7
+ * @type {string}
8
+ */
9
+ const name = 'ubuntu';
10
+
11
+ /**
12
+ * Default package manager for Ubuntu.
13
+ * @type {string}
14
+ */
15
+ const packageManager = 'apt';
16
+
17
+ /**
18
+ * Checks if a binary is available on the system PATH.
19
+ * @param {string} binary - The name of the binary to look for (e.g. 'node', 'git').
20
+ * @returns {boolean} True if the binary exists on the PATH, false otherwise.
21
+ */
22
+ function isInstalled(binary) {
23
+ try {
24
+ execFileSync('which', [binary], { stdio: 'ignore' });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Returns common application directories for Ubuntu.
33
+ * @returns {string[]} Array of directory paths where applications are typically installed.
34
+ */
35
+ function getAppPaths() {
36
+ return ['/usr/bin', '/usr/local/bin', '/snap/bin'];
37
+ }
38
+
39
+ module.exports = { name, packageManager, isInstalled, getAppPaths };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * Platform name identifier.
7
+ * @type {string}
8
+ */
9
+ const name = 'windows';
10
+
11
+ /**
12
+ * Default package manager for Windows.
13
+ * @type {string}
14
+ */
15
+ const packageManager = 'choco';
16
+
17
+ /**
18
+ * Checks if a binary is available on the system PATH.
19
+ * Windows uses "where" instead of "which" for binary lookup.
20
+ * @param {string} binary - The name of the binary to look for (e.g. 'node', 'git').
21
+ * @returns {boolean} True if the binary exists on the PATH, false otherwise.
22
+ */
23
+ function isInstalled(binary) {
24
+ try {
25
+ execFileSync('where', [binary], { stdio: 'ignore' });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns common application directories for Windows.
34
+ * Filters out undefined entries in case environment variables aren't set.
35
+ * @returns {string[]} Array of directory paths where applications are typically installed.
36
+ */
37
+ function getAppPaths() {
38
+ return [
39
+ process.env.ProgramFiles,
40
+ process.env['ProgramFiles(x86)'],
41
+ process.env.LOCALAPPDATA,
42
+ ].filter(Boolean);
43
+ }
44
+
45
+ module.exports = { name, packageManager, isInstalled, getAppPaths };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const { detectOutputMode } = require('./detect');
5
+
6
+ /**
7
+ * Checks if the current environment is interactive (human at a terminal).
8
+ * Only 'tty' callers are interactive. AI tools, CI systems, and piped output
9
+ * are all non-interactive.
10
+ *
11
+ * @returns {boolean}
12
+ */
13
+ function isInteractive() {
14
+ const { caller } = detectOutputMode();
15
+ return caller === 'tty';
16
+ }
17
+
18
+ /**
19
+ * Opens a readline interface, asks one question, and closes it.
20
+ * Writes prompt text to stderr (not stdout) so piped output stays clean.
21
+ *
22
+ * @param {string} question - The question text to display.
23
+ * @returns {Promise<string>} The user's answer.
24
+ */
25
+ function askReadline(question) {
26
+ return new Promise((resolve) => {
27
+ const rl = readline.createInterface({
28
+ input: process.stdin,
29
+ output: process.stderr,
30
+ });
31
+ rl.question(question, (answer) => {
32
+ rl.close();
33
+ resolve(answer);
34
+ });
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Asks a free-text question. Returns the user's answer, or the default value
40
+ * in non-interactive mode or when the user presses Enter without typing.
41
+ *
42
+ * @param {string} question - The question to ask.
43
+ * @param {string} [defaultValue=''] - The default value if no input is given.
44
+ * @returns {Promise<string>}
45
+ */
46
+ async function ask(question, defaultValue = '') {
47
+ if (!isInteractive()) {
48
+ return defaultValue;
49
+ }
50
+
51
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
52
+ const answer = await askReadline(`${question}${suffix}: `);
53
+ return answer.trim() || defaultValue;
54
+ }
55
+
56
+ /**
57
+ * Asks a yes/no question. Returns true or false.
58
+ * The hint shows which option is default: (Y/n) means yes, (y/N) means no.
59
+ *
60
+ * @param {string} question - The question to ask.
61
+ * @param {boolean} [defaultValue=false] - The default value.
62
+ * @returns {Promise<boolean>}
63
+ */
64
+ async function confirm(question, defaultValue = false) {
65
+ if (!isInteractive()) {
66
+ return defaultValue;
67
+ }
68
+
69
+ const hint = defaultValue ? '(Y/n)' : '(y/N)';
70
+ const answer = await askReadline(`${question} ${hint}: `);
71
+ const trimmed = answer.trim().toLowerCase();
72
+
73
+ if (trimmed === '') return defaultValue;
74
+ return trimmed === 'y' || trimmed === 'yes';
75
+ }
76
+
77
+ /**
78
+ * Presents a numbered list of choices and asks the user to pick one.
79
+ * Returns the selected choice string.
80
+ * Invalid input falls back to the default with a message.
81
+ *
82
+ * @param {string} question - The question to display above the choices.
83
+ * @param {string[]} choices - The list of choices.
84
+ * @param {number} [defaultIndex=0] - The index of the default choice.
85
+ * @returns {Promise<string>}
86
+ */
87
+ async function choose(question, choices, defaultIndex = 0) {
88
+ if (!isInteractive()) {
89
+ return choices[defaultIndex] || choices[0];
90
+ }
91
+
92
+ // Display the choices on stderr
93
+ process.stderr.write(`${question}\n`);
94
+ for (let i = 0; i < choices.length; i++) {
95
+ const marker = i === defaultIndex ? '>' : ' ';
96
+ process.stderr.write(` ${marker} ${i + 1}. ${choices[i]}\n`);
97
+ }
98
+
99
+ const answer = await askReadline(`Choice (1-${choices.length}) [${defaultIndex + 1}]: `);
100
+ const trimmed = answer.trim();
101
+
102
+ if (trimmed === '') return choices[defaultIndex];
103
+
104
+ const index = parseInt(trimmed, 10) - 1;
105
+ if (isNaN(index) || index < 0 || index >= choices.length) {
106
+ process.stderr.write(`Invalid choice. Using default: ${choices[defaultIndex]}\n`);
107
+ return choices[defaultIndex];
108
+ }
109
+
110
+ return choices[index];
111
+ }
112
+
113
+ /**
114
+ * Asks for sensitive input (passwords, API keys). The input is not echoed.
115
+ * Returns an empty string in non-interactive mode.
116
+ *
117
+ * @param {string} question - The prompt text.
118
+ * @returns {Promise<string>}
119
+ */
120
+ async function password(question) {
121
+ if (!isInteractive()) {
122
+ return '';
123
+ }
124
+
125
+ return new Promise((resolve) => {
126
+ const rl = readline.createInterface({
127
+ input: process.stdin,
128
+ output: process.stderr,
129
+ terminal: true,
130
+ });
131
+
132
+ const origWrite = rl.output.write.bind(rl.output);
133
+ let muted = false;
134
+
135
+ rl.output.write = function(chunk) {
136
+ if (muted) return;
137
+ return origWrite(chunk);
138
+ };
139
+
140
+ rl.question(`${question}: `, (answer) => {
141
+ muted = false;
142
+ rl.output.write('\n');
143
+ rl.close();
144
+ resolve(answer);
145
+ });
146
+
147
+ muted = true;
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Alias for ask(). Command stories may use context.prompt.input().
153
+ */
154
+ const input = ask;
155
+
156
+ /**
157
+ * Alias for choose(). Command stories may use context.prompt.select().
158
+ */
159
+ const select = choose;
160
+
161
+ module.exports = { ask, input, confirm, choose, select, password, isInteractive };
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const SERVICE_NAMES = [
8
+ 'config', 'machine', 'identity', 'tools',
9
+ 'ignore', 'util', 'alias', 'auth', 'api', 'ai', 'search',
10
+ ];
11
+
12
+ const TOP_LEVEL_COMMANDS = ['status', 'update', 'version', 'schema', 'help'];
13
+
14
+ let _registry = null;
15
+
16
+ /**
17
+ * Build the full schema registry by walking all service index.js files,
18
+ * top-level commands, and installed API plugins.
19
+ *
20
+ * @returns {object} The nested registry object.
21
+ */
22
+ function buildRegistry() {
23
+ const registry = {};
24
+
25
+ // Load services
26
+ for (const name of SERVICE_NAMES) {
27
+ try {
28
+ const service = require(`../commands/${name}/index`);
29
+ const entry = {
30
+ type: 'service',
31
+ name: service.name || name,
32
+ description: service.description || '',
33
+ commands: {},
34
+ };
35
+
36
+ for (const [cmdName, loader] of Object.entries(service.commands || {})) {
37
+ try {
38
+ const cmd = typeof loader === 'function' ? loader() : loader;
39
+ entry.commands[cmdName] = {
40
+ type: 'command',
41
+ name: cmdName,
42
+ description: (cmd.meta && cmd.meta.description) || '',
43
+ arguments: (cmd.meta && cmd.meta.arguments) || [],
44
+ flags: (cmd.meta && cmd.meta.flags) || [],
45
+ };
46
+ } catch {
47
+ // Skip commands that fail to load
48
+ }
49
+ }
50
+
51
+ registry[name] = entry;
52
+ } catch {
53
+ // Skip services that fail to load
54
+ }
55
+ }
56
+
57
+ // Load top-level commands
58
+ for (const name of TOP_LEVEL_COMMANDS) {
59
+ try {
60
+ const cmd = require(`../commands/${name}`);
61
+ registry[name] = {
62
+ type: 'command',
63
+ name,
64
+ description: (cmd.meta && cmd.meta.description) || '',
65
+ arguments: (cmd.meta && cmd.meta.arguments) || [],
66
+ flags: (cmd.meta && cmd.meta.flags) || [],
67
+ };
68
+ } catch {
69
+ // Skip commands that fail to load
70
+ }
71
+ }
72
+
73
+ // Load API plugins
74
+ loadPlugins(registry);
75
+
76
+ return registry;
77
+ }
78
+
79
+ /**
80
+ * Load installed API plugins into the registry under api.plugins.
81
+ * @param {object} registry - The registry to extend.
82
+ */
83
+ function loadPlugins(registry) {
84
+ const pluginsFile = path.join(os.homedir(), '.devutils', 'plugins.json');
85
+
86
+ if (!fs.existsSync(pluginsFile)) {
87
+ return;
88
+ }
89
+
90
+ let plugins;
91
+ try {
92
+ plugins = JSON.parse(fs.readFileSync(pluginsFile, 'utf8'));
93
+ } catch {
94
+ return;
95
+ }
96
+
97
+ const pluginsDir = path.join(os.homedir(), '.devutils', 'plugins');
98
+
99
+ if (!registry.api) {
100
+ registry.api = { type: 'service', name: 'api', description: 'API plugin system', commands: {} };
101
+ }
102
+
103
+ for (const [serviceName, info] of Object.entries(plugins)) {
104
+ try {
105
+ const pluginPath = path.join(pluginsDir, 'node_modules', info.package);
106
+ const plugin = require(pluginPath);
107
+
108
+ const serviceEntry = {
109
+ type: 'plugin',
110
+ name: plugin.name || serviceName,
111
+ description: plugin.description || '',
112
+ version: plugin.version || '',
113
+ auth: plugin.auth || null,
114
+ resources: {},
115
+ };
116
+
117
+ for (const [resName, resource] of Object.entries(plugin.resources || {})) {
118
+ const resEntry = {
119
+ type: 'resource',
120
+ name: resName,
121
+ description: (resource.description) || '',
122
+ commands: {},
123
+ };
124
+
125
+ for (const [cmdName, loader] of Object.entries(resource.commands || {})) {
126
+ try {
127
+ const cmd = typeof loader === 'function' ? loader() : loader;
128
+ resEntry.commands[cmdName] = {
129
+ type: 'command',
130
+ name: cmdName,
131
+ description: (cmd.meta && cmd.meta.description) || '',
132
+ arguments: (cmd.meta && cmd.meta.arguments) || [],
133
+ flags: (cmd.meta && cmd.meta.flags) || [],
134
+ };
135
+ } catch {
136
+ // Skip commands that fail to load
137
+ }
138
+ }
139
+
140
+ serviceEntry.resources[resName] = resEntry;
141
+ }
142
+
143
+ if (!registry.api.plugins) {
144
+ registry.api.plugins = {};
145
+ }
146
+ registry.api.plugins[serviceName] = serviceEntry;
147
+ } catch {
148
+ // Plugin failed to load, skip it
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get the schema registry. Built lazily on first access and cached.
155
+ * @returns {object} The registry.
156
+ */
157
+ function getRegistry() {
158
+ if (!_registry) {
159
+ _registry = buildRegistry();
160
+ }
161
+ return _registry;
162
+ }
163
+
164
+ /**
165
+ * Resolve a dot-notation path to a registry entry.
166
+ * Handles up to 4 levels: api.plugin.resource.command.
167
+ *
168
+ * @param {string} dotPath - Dot-notation path (e.g., "config.set", "api.gmail.messages.list").
169
+ * @returns {object|null} The matching entry, or null if not found.
170
+ */
171
+ function resolve(dotPath) {
172
+ const registry = getRegistry();
173
+ const parts = dotPath.split('.');
174
+
175
+ // Single segment: top-level service or command
176
+ if (parts.length === 1) {
177
+ return registry[parts[0]] || null;
178
+ }
179
+
180
+ // Two segments: service.command
181
+ if (parts.length === 2) {
182
+ const service = registry[parts[0]];
183
+ if (!service) return null;
184
+ if (service.type === 'service' && service.commands && service.commands[parts[1]]) {
185
+ return service.commands[parts[1]];
186
+ }
187
+ return null;
188
+ }
189
+
190
+ // Three segments: api.plugin.resource
191
+ if (parts.length === 3 && parts[0] === 'api') {
192
+ const plugins = (registry.api && registry.api.plugins) || {};
193
+ const plugin = plugins[parts[1]];
194
+ if (!plugin) return null;
195
+ return (plugin.resources && plugin.resources[parts[2]]) || null;
196
+ }
197
+
198
+ // Four segments: api.plugin.resource.command
199
+ if (parts.length === 4 && parts[0] === 'api') {
200
+ const plugins = (registry.api && registry.api.plugins) || {};
201
+ const plugin = plugins[parts[1]];
202
+ if (!plugin) return null;
203
+ const resource = plugin.resources && plugin.resources[parts[2]];
204
+ if (!resource) return null;
205
+ return (resource.commands && resource.commands[parts[3]]) || null;
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ module.exports = { getRegistry, resolve };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { exec: cpExec, execSync: cpExecSync } = require('child_process');
4
+
5
+ /**
6
+ * Runs a shell command asynchronously.
7
+ * Always resolves (never rejects). The caller checks exitCode to determine success.
8
+ *
9
+ * @param {string} cmd - The command to run.
10
+ * @param {object} [opts] - Options passed through to child_process.exec (cwd, env, timeout, etc.).
11
+ * @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
12
+ */
13
+ async function exec(cmd, opts = {}) {
14
+ return new Promise((resolve) => {
15
+ cpExec(cmd, opts, (error, stdout, stderr) => {
16
+ resolve({
17
+ stdout: stdout ? stdout.toString().trim() : '',
18
+ stderr: stderr ? stderr.toString().trim() : '',
19
+ exitCode: error ? error.code || 1 : 0,
20
+ });
21
+ });
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Runs a shell command synchronously.
27
+ * Returns the trimmed stdout string on success, or null on failure.
28
+ *
29
+ * An empty string means "command ran but produced no output."
30
+ * Null means "command failed."
31
+ *
32
+ * @param {string} cmd - The command to run.
33
+ * @param {object} [opts] - Options passed through to child_process.execSync.
34
+ * @returns {string|null}
35
+ */
36
+ function execSync(cmd, opts = {}) {
37
+ try {
38
+ const result = cpExecSync(cmd, { ...opts, encoding: 'utf8' });
39
+ return result ? result.trim() : '';
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Finds the full path to a binary on the system PATH.
47
+ * Uses "which" on unix-like systems and "where" on native Windows.
48
+ * Git Bash provides its own "which", so it uses the unix path.
49
+ *
50
+ * @param {string} binary - The name of the binary to find (e.g. 'node', 'git').
51
+ * @returns {string|null} The full path to the binary, or null if not found.
52
+ */
53
+ function which(binary) {
54
+ const platform = require('./platform').detect();
55
+ const cmd = platform.type === 'windows' ? `where ${binary}` : `which ${binary}`;
56
+ const result = execSync(cmd);
57
+ if (result === null) {
58
+ return null;
59
+ }
60
+ // 'where' on Windows can return multiple lines; take the first one
61
+ return result.split('\n')[0].trim();
62
+ }
63
+
64
+ /**
65
+ * Checks if a binary exists on the system PATH.
66
+ * A boolean wrapper around which().
67
+ *
68
+ * @param {string} binary - The name of the binary to check.
69
+ * @returns {boolean} True if the binary exists, false otherwise.
70
+ */
71
+ function commandExists(binary) {
72
+ return which(binary) !== null;
73
+ }
74
+
75
+ module.exports = { exec, execSync, which, commandExists };
@@ -0,0 +1,25 @@
1
+ # Claude Code
2
+ # Patterns for Claude Code, Sidecar, and other AI coding tool artifacts.
3
+
4
+ # Claude Code local state
5
+ .claude/
6
+
7
+ # Sidecar local state (TUI task runner)
8
+ .sidecar/
9
+
10
+ # Sidecar todo tracking
11
+ .todos/
12
+
13
+ # AI session artifacts
14
+ .ai-sessions/
15
+
16
+ # Cursor AI
17
+ .cursorignore
18
+ .cursorindexingignore
19
+ .cursor/
20
+
21
+ # Aider
22
+ .aider*
23
+
24
+ # Codeium
25
+ .codeium/
@@ -0,0 +1,15 @@
1
+ # Docker
2
+ # Patterns for Docker projects. These are files that should not be
3
+ # included in a Docker build context or committed to version control.
4
+
5
+ # Docker Compose override (local dev customization)
6
+ docker-compose.override.yml
7
+ docker-compose.override.yaml
8
+
9
+ # Docker environment files
10
+ .docker/
11
+ .dockerenv
12
+
13
+ # Local data volumes (often mounted for development)
14
+ data/
15
+ volumes/
@@ -0,0 +1,24 @@
1
+ # Go
2
+ # Patterns for Go projects.
3
+
4
+ # Compiled binaries
5
+ *.exe
6
+ *.exe~
7
+ *.dll
8
+ *.so
9
+ *.dylib
10
+
11
+ # Test binary (created by go test -c)
12
+ *.test
13
+
14
+ # Output of go coverage tool
15
+ *.out
16
+
17
+ # Go workspace file
18
+ go.work
19
+
20
+ # Vendor directory (if not committing dependencies)
21
+ # vendor/
22
+
23
+ # Build output directory
24
+ bin/
@@ -0,0 +1,38 @@
1
+ # Java
2
+ # Patterns for Java and JVM-based projects.
3
+
4
+ # Compiled class files
5
+ *.class
6
+
7
+ # Package files
8
+ *.jar
9
+ *.war
10
+ *.nar
11
+ *.ear
12
+ *.zip
13
+ *.tar.gz
14
+ *.rar
15
+
16
+ # Build output
17
+ target/
18
+ build/
19
+ out/
20
+
21
+ # Gradle
22
+ .gradle/
23
+ gradle-app.setting
24
+ !gradle-wrapper.jar
25
+
26
+ # Maven
27
+ pom.xml.tag
28
+ pom.xml.releaseBackup
29
+ pom.xml.versionsBackup
30
+ pom.xml.next
31
+ release.properties
32
+
33
+ # Log files
34
+ *.log
35
+
36
+ # Virtual machine crash logs
37
+ hs_err_pid*
38
+ replay_pid*
@@ -0,0 +1,26 @@
1
+ # JetBrains
2
+ # Patterns for JetBrains IDEs (IntelliJ, WebStorm, PyCharm, etc.).
3
+
4
+ # IDE project settings
5
+ .idea/
6
+
7
+ # File-based project format
8
+ *.iws
9
+ *.iml
10
+ *.ipr
11
+
12
+ # CMake build output
13
+ cmake-build-*/
14
+
15
+ # Crashlytics plugin (for Android projects)
16
+ com_crashlytics_export_strings.xml
17
+ crashlytics.properties
18
+ crashlytics-build.properties
19
+ fabric.properties
20
+
21
+ # Editor-based HTTP client requests
22
+ httpRequests/
23
+
24
+ # DataSources local storage
25
+ dataSources/
26
+ dataSources.local.xml