@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,18 @@
1
+ # Linux
2
+ # Patterns for Linux system files and editor artifacts.
3
+
4
+ # Backup files
5
+ *~
6
+
7
+ # NFS temp files
8
+ .nfs*
9
+
10
+ # KDE directory preferences
11
+ .directory
12
+
13
+ # Swap files
14
+ *.swp
15
+ *.swo
16
+
17
+ # Trash directories
18
+ .Trash-*
@@ -0,0 +1,27 @@
1
+ # macOS
2
+ # Patterns for macOS system and Finder metadata files.
3
+
4
+ # Finder metadata
5
+ .DS_Store
6
+ .AppleDouble
7
+ .LSOverride
8
+
9
+ # Thumbnail cache
10
+ ._*
11
+
12
+ # Spotlight indexing
13
+ .Spotlight-V100
14
+
15
+ # Trash folder state
16
+ .Trashes
17
+
18
+ # Volume metadata
19
+ .VolumeIcon.icns
20
+ .com.apple.timemachine.donotpresent
21
+
22
+ # Finder directory metadata
23
+ .fseventsd
24
+ .apdisk
25
+
26
+ # macOS resource forks
27
+ Icon?
@@ -0,0 +1,51 @@
1
+ # Node.js
2
+ # Patterns for Node.js and JavaScript/TypeScript projects.
3
+
4
+ # Dependencies
5
+ node_modules/
6
+
7
+ # Build output
8
+ dist/
9
+ build/
10
+ out/
11
+ .next/
12
+ .nuxt/
13
+
14
+ # Environment and secrets
15
+ .env
16
+ .env.*
17
+ !.env.example
18
+
19
+ # Package manager files
20
+ package-lock.json
21
+ yarn.lock
22
+ pnpm-lock.yaml
23
+ .yarn/
24
+ .pnp.*
25
+
26
+ # Debug and logs
27
+ npm-debug.log*
28
+ yarn-debug.log*
29
+ yarn-error.log*
30
+ pnpm-debug.log*
31
+ lerna-debug.log*
32
+ *.log
33
+
34
+ # Coverage and test output
35
+ coverage/
36
+ .nyc_output/
37
+ *.lcov
38
+
39
+ # TypeScript cache
40
+ *.tsbuildinfo
41
+
42
+ # Cache directories
43
+ .cache/
44
+ .parcel-cache/
45
+ .eslintcache
46
+
47
+ # Runtime data
48
+ pids/
49
+ *.pid
50
+ *.seed
51
+ *.pid.lock
@@ -0,0 +1,55 @@
1
+ # Python
2
+ # Patterns for Python projects.
3
+
4
+ # Byte-compiled files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+ # Distribution and packaging
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ *.egg
14
+ eggs/
15
+ wheels/
16
+ sdist/
17
+
18
+ # Virtual environments
19
+ .venv/
20
+ venv/
21
+ env/
22
+ ENV/
23
+
24
+ # Environment and secrets
25
+ .env
26
+ .env.*
27
+ !.env.example
28
+
29
+ # Installer logs
30
+ pip-log.txt
31
+ pip-delete-this-directory.txt
32
+
33
+ # Unit test and coverage
34
+ htmlcov/
35
+ .tox/
36
+ .nox/
37
+ .coverage
38
+ .coverage.*
39
+ coverage.xml
40
+ *.cover
41
+ *.py,cover
42
+ .hypothesis/
43
+ .pytest_cache/
44
+ nosetests.xml
45
+
46
+ # Type checker cache
47
+ .mypy_cache/
48
+ .pytype/
49
+ .pyre/
50
+
51
+ # Jupyter Notebook checkpoints
52
+ .ipynb_checkpoints/
53
+
54
+ # pyenv
55
+ .python-version
@@ -0,0 +1,14 @@
1
+ # Rust
2
+ # Patterns for Rust projects.
3
+
4
+ # Compiled output
5
+ target/
6
+
7
+ # Cargo.lock for libraries (remove this line for applications)
8
+ # Cargo.lock
9
+
10
+ # Backup files generated by rustfmt
11
+ *.rs.bk
12
+
13
+ # MSVC debug information
14
+ *.pdb
@@ -0,0 +1,30 @@
1
+ # Terraform
2
+ # Patterns for Terraform and OpenTofu infrastructure-as-code projects.
3
+
4
+ # Provider plugins and modules
5
+ .terraform/
6
+
7
+ # State files (contain secrets -- never commit these)
8
+ *.tfstate
9
+ *.tfstate.*
10
+
11
+ # Crash logs
12
+ crash.log
13
+ crash.*.log
14
+
15
+ # Variable files that may contain secrets
16
+ *.tfvars
17
+ *.tfvars.json
18
+ !*.tfvars.example
19
+
20
+ # Override files (local developer overrides)
21
+ override.tf
22
+ override.tf.json
23
+ *_override.tf
24
+ *_override.tf.json
25
+
26
+ # Plan files (binary, can contain secrets)
27
+ *.tfplan
28
+
29
+ # Lock file (commit this for consistent provider versions)
30
+ # .terraform.lock.hcl
@@ -0,0 +1,15 @@
1
+ # Visual Studio Code
2
+ # Patterns for VS Code workspace and user-level files.
3
+
4
+ # Workspace settings (user-specific)
5
+ .vscode/
6
+
7
+ # Workspace file
8
+ *.code-workspace
9
+
10
+ # VS Code local history
11
+ .history/
12
+
13
+ # VS Code extensions recommendation (optional -- remove this line
14
+ # if your team uses shared extension recommendations)
15
+ # .vscode/extensions.json
@@ -0,0 +1,25 @@
1
+ # Windows
2
+ # Patterns for Windows system and Explorer metadata files.
3
+
4
+ # Thumbnail cache
5
+ Thumbs.db
6
+ Thumbs.db:encryptable
7
+ ehthumbs.db
8
+ ehthumbs_vista.db
9
+
10
+ # Folder config
11
+ desktop.ini
12
+ Desktop.ini
13
+
14
+ # Recycle Bin
15
+ $RECYCLE.BIN/
16
+
17
+ # Shortcut files
18
+ *.lnk
19
+
20
+ # Windows Installer files
21
+ *.cab
22
+ *.msi
23
+ *.msix
24
+ *.msm
25
+ *.msp
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const meta = {
8
+ name: 'clone',
9
+ description: 'Clone a git repo and auto-install dependencies using the detected package manager',
10
+ platforms: ['macos', 'ubuntu', 'raspbian', 'amazon-linux', 'windows', 'gitbash'],
11
+ arguments: [
12
+ { name: 'url', required: true, description: 'Git repository URL (SSH or HTTPS)' },
13
+ { name: 'directory', required: false, description: 'Target directory name (defaults to repo name)' },
14
+ ],
15
+ flags: [],
16
+ };
17
+
18
+ /**
19
+ * Extract the repository name from a git URL.
20
+ * Handles HTTPS, SSH, and bare path formats.
21
+ *
22
+ * @param {string} url - The git repository URL.
23
+ * @returns {string} The extracted repo name.
24
+ */
25
+ function extractRepoName(url) {
26
+ let cleaned = url.replace(/\/+$/, '');
27
+ cleaned = cleaned.replace(/\.git$/, '');
28
+
29
+ const lastSegment = cleaned.split('/').pop() || '';
30
+
31
+ if (lastSegment.includes(':')) {
32
+ return lastSegment.split(':').pop() || 'cloned-repo';
33
+ }
34
+
35
+ return lastSegment || 'cloned-repo';
36
+ }
37
+
38
+ /**
39
+ * Detect which package manager to use based on lock files in the directory.
40
+ * Checks lock files in order: yarn.lock, pnpm-lock.yaml, package-lock.json.
41
+ * Falls back to npm if no lock file is found but package.json exists.
42
+ *
43
+ * @param {string} dir - The directory to check.
44
+ * @returns {{ name: string, installCmd: string }|null} Package manager info, or null if not a JS project.
45
+ */
46
+ function detectPackageManager(dir) {
47
+ const hasPackageJson = fs.existsSync(path.join(dir, 'package.json'));
48
+ if (!hasPackageJson) {
49
+ return null;
50
+ }
51
+
52
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) {
53
+ return { name: 'yarn', installCmd: 'yarn install' };
54
+ }
55
+
56
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) {
57
+ return { name: 'pnpm', installCmd: 'pnpm install' };
58
+ }
59
+
60
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) {
61
+ return { name: 'npm', installCmd: 'npm install' };
62
+ }
63
+
64
+ // package.json exists but no lock file -- default to npm
65
+ return { name: 'npm', installCmd: 'npm install' };
66
+ }
67
+
68
+ /**
69
+ * Check if a command is available on the system.
70
+ *
71
+ * @param {string} cmd - The command name to check.
72
+ * @returns {boolean} True if the command exists on PATH.
73
+ */
74
+ function isAvailable(cmd) {
75
+ const shell = require('../../lib/shell');
76
+ return shell.commandExists(cmd);
77
+ }
78
+
79
+ async function run(args) {
80
+ const shell = require('../../lib/shell');
81
+
82
+ if (!shell.commandExists('git')) {
83
+ console.error('Error: git is not installed.');
84
+ console.error('Install git first, then try again.');
85
+ return;
86
+ }
87
+
88
+ const url = args[0];
89
+ if (!url) {
90
+ console.error('Usage: dev util run clone <url> [directory]');
91
+ console.error('');
92
+ console.error('Examples:');
93
+ console.error(' dev util run clone git@github.com:user/repo.git');
94
+ console.error(' dev util run clone https://github.com/user/repo.git');
95
+ console.error(' dev util run clone git@github.com:user/repo.git my-project');
96
+ return;
97
+ }
98
+
99
+ const targetDir = args[1] || extractRepoName(url);
100
+ const fullPath = path.resolve(process.cwd(), targetDir);
101
+
102
+ if (fs.existsSync(fullPath)) {
103
+ console.error(`Error: Directory "${targetDir}" already exists.`);
104
+ return;
105
+ }
106
+
107
+ console.log(`Cloning ${url} into ${targetDir}...`);
108
+ console.log('');
109
+
110
+ const cloneResult = spawnSync('git', ['clone', url, targetDir], {
111
+ stdio: 'inherit',
112
+ cwd: process.cwd(),
113
+ });
114
+
115
+ if (cloneResult.status !== 0) {
116
+ console.error('');
117
+ console.error('git clone failed.');
118
+ return;
119
+ }
120
+
121
+ if (!fs.existsSync(fullPath)) {
122
+ console.error('Clone appeared to succeed but the directory was not created.');
123
+ return;
124
+ }
125
+
126
+ const pm = detectPackageManager(fullPath);
127
+
128
+ if (!pm) {
129
+ console.log('');
130
+ console.log(`Cloned into "${targetDir}".`);
131
+ console.log('No package.json found -- skipping dependency installation.');
132
+ return;
133
+ }
134
+
135
+ if (!isAvailable(pm.name)) {
136
+ console.log('');
137
+ console.log(`Cloned into "${targetDir}".`);
138
+ console.log(`package.json found, but ${pm.name} is not installed.`);
139
+ console.log(`Install ${pm.name} and run: ${pm.installCmd}`);
140
+ return;
141
+ }
142
+
143
+ console.log('');
144
+ console.log(`Installing dependencies with ${pm.name}...`);
145
+ console.log('');
146
+
147
+ const installResult = spawnSync(pm.name, ['install'], {
148
+ stdio: 'inherit',
149
+ cwd: fullPath,
150
+ shell: true,
151
+ });
152
+
153
+ if (installResult.status !== 0) {
154
+ console.error('');
155
+ console.error(`${pm.name} install finished with errors. Check the output above.`);
156
+ } else {
157
+ console.log('');
158
+ console.log('Dependencies installed.');
159
+ }
160
+
161
+ console.log('');
162
+ console.log(`Done. To enter the project: cd ${targetDir}`);
163
+ }
164
+
165
+ module.exports = { meta, run };
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ const meta = {
4
+ name: 'git-push',
5
+ description: 'Safer git push: shows what will be pushed, confirms before pushing, and protects main/master',
6
+ platforms: ['macos', 'ubuntu', 'raspbian', 'amazon-linux', 'windows', 'gitbash'],
7
+ arguments: [
8
+ { name: 'message', required: true, description: 'Commit message (all arguments after the name are joined)' },
9
+ ],
10
+ flags: [
11
+ { name: 'force', type: 'boolean', description: 'Allow pushing to protected branches (main/master)' },
12
+ { name: 'yes', type: 'boolean', description: 'Skip the confirmation prompt' },
13
+ ],
14
+ };
15
+
16
+ const shell = require('../../lib/shell');
17
+
18
+ /**
19
+ * Check if the current directory is inside a git repo.
20
+ * @returns {boolean}
21
+ */
22
+ function isGitRepo() {
23
+ const result = shell.execSync('git rev-parse --is-inside-work-tree');
24
+ return result === 'true';
25
+ }
26
+
27
+ /**
28
+ * Get the current branch name.
29
+ * @returns {string|null} Branch name, or null if in detached HEAD state.
30
+ */
31
+ function getCurrentBranch() {
32
+ return shell.execSync('git symbolic-ref --short HEAD') || null;
33
+ }
34
+
35
+ /**
36
+ * Get a list of changed files (staged, unstaged, and untracked).
37
+ * @returns {Array<{ status: string, file: string }>}
38
+ */
39
+ function getChangedFiles() {
40
+ const result = shell.execSync('git status --porcelain');
41
+ if (!result) return [];
42
+
43
+ return result.split('\n').filter(Boolean).map(line => {
44
+ const status = line.substring(0, 2).trim();
45
+ const file = line.substring(3);
46
+ return { status, file };
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Get the list of commits that would be pushed (ahead of remote).
52
+ * @returns {string[]} Array of commit summary lines.
53
+ */
54
+ function getUnpushedCommits() {
55
+ const result = shell.execSync('git log @{upstream}..HEAD --oneline');
56
+ if (!result) return [];
57
+ return result.split('\n').filter(Boolean);
58
+ }
59
+
60
+ /**
61
+ * Check if there is a tracking branch configured.
62
+ * @returns {boolean}
63
+ */
64
+ function hasTrackingBranch() {
65
+ const result = shell.execSync('git rev-parse --abbrev-ref @{upstream}');
66
+ return result !== null;
67
+ }
68
+
69
+ /**
70
+ * Check if --force flag was passed.
71
+ * @param {string[]} args - Raw args array.
72
+ * @param {object} context - Command context.
73
+ * @returns {boolean}
74
+ */
75
+ function isForceFlag(args, context) {
76
+ if (context && context.flags && context.flags.force) return true;
77
+ return args.includes('--force');
78
+ }
79
+
80
+ /**
81
+ * Check if --yes flag was passed.
82
+ * @param {string[]} args - Raw args array.
83
+ * @param {object} context - Command context.
84
+ * @returns {boolean}
85
+ */
86
+ function isYesFlag(args, context) {
87
+ if (context && context.flags && context.flags.yes) return true;
88
+ return args.includes('--yes') || args.includes('-y');
89
+ }
90
+
91
+ /**
92
+ * Convert a git status code to a human-readable label.
93
+ * @param {string} status - Status code from git status --porcelain.
94
+ * @returns {string}
95
+ */
96
+ function statusLabel(status) {
97
+ const labels = {
98
+ 'M': 'modified',
99
+ 'A': 'added',
100
+ 'D': 'deleted',
101
+ 'R': 'renamed',
102
+ 'C': 'copied',
103
+ '?': 'untracked',
104
+ '!': 'ignored',
105
+ };
106
+ const char = status.replace(/\s/g, '') || '?';
107
+ return labels[char[0]] || status;
108
+ }
109
+
110
+ async function run(args, context) {
111
+ if (!shell.commandExists('git')) {
112
+ console.error('Error: git is not installed.');
113
+ return;
114
+ }
115
+
116
+ if (!isGitRepo()) {
117
+ console.error('Error: Not inside a git repository.');
118
+ return;
119
+ }
120
+
121
+ // Filter out flags from args to build the commit message
122
+ const messageArgs = args.filter(a => !a.startsWith('--') && a !== '-y');
123
+ const message = messageArgs.join(' ').trim();
124
+ if (!message) {
125
+ console.error('Usage: dev util run git-push "your commit message"');
126
+ console.error('');
127
+ console.error('All arguments after the utility name become the commit message.');
128
+ console.error('Example: dev util run git-push Fix the login button color');
129
+ return;
130
+ }
131
+
132
+ const branch = getCurrentBranch();
133
+ if (!branch) {
134
+ console.error('Error: Not on a branch (detached HEAD).');
135
+ console.error('Check out a branch first: git checkout <branch>');
136
+ return;
137
+ }
138
+
139
+ // Check for protected branches
140
+ const protectedBranches = ['main', 'master'];
141
+ if (protectedBranches.includes(branch) && !isForceFlag(args, context)) {
142
+ console.error(`Warning: You are on the "${branch}" branch.`);
143
+ console.error('Pushing directly to this branch is usually not recommended.');
144
+ console.error('');
145
+
146
+ if (!context || !context.prompt) {
147
+ console.error('Use --force to push to protected branches in non-interactive mode.');
148
+ return;
149
+ }
150
+
151
+ const proceed = await context.prompt.confirm(
152
+ `Are you sure you want to push directly to ${branch}?`,
153
+ false
154
+ );
155
+ if (!proceed) {
156
+ console.log('Cancelled.');
157
+ return;
158
+ }
159
+ }
160
+
161
+ const changedFiles = getChangedFiles();
162
+ const existingUnpushed = hasTrackingBranch() ? getUnpushedCommits() : [];
163
+
164
+ if (changedFiles.length === 0 && existingUnpushed.length === 0) {
165
+ console.log('Nothing to commit or push. Working tree is clean.');
166
+ return;
167
+ }
168
+
169
+ // Display what will happen
170
+ console.log('');
171
+ console.log(`Branch: ${branch}`);
172
+ console.log(`Commit message: "${message}"`);
173
+ console.log('');
174
+
175
+ if (changedFiles.length > 0) {
176
+ console.log(`Files to be committed (${changedFiles.length}):`);
177
+ for (const f of changedFiles) {
178
+ const label = statusLabel(f.status);
179
+ console.log(` ${label} ${f.file}`);
180
+ }
181
+ console.log('');
182
+ }
183
+
184
+ if (existingUnpushed.length > 0) {
185
+ console.log(`Already unpushed commits (${existingUnpushed.length}):`);
186
+ for (const c of existingUnpushed) {
187
+ console.log(` ${c}`);
188
+ }
189
+ console.log('');
190
+ }
191
+
192
+ // Confirm
193
+ if (!isYesFlag(args, context)) {
194
+ if (context && context.prompt) {
195
+ const ok = await context.prompt.confirm('Proceed with commit and push?', true);
196
+ if (!ok) {
197
+ console.log('Cancelled.');
198
+ return;
199
+ }
200
+ }
201
+ }
202
+
203
+ // Execute: add, commit, push
204
+ console.log('Staging all changes...');
205
+ const addResult = await shell.exec('git add -A');
206
+ if (addResult.exitCode !== 0) {
207
+ console.error('git add failed: ' + addResult.stderr);
208
+ return;
209
+ }
210
+
211
+ console.log('Committing...');
212
+ const escapedMessage = message.replace(/"/g, '\\"');
213
+ const commitResult = await shell.exec(`git commit -m "${escapedMessage}"`);
214
+ if (commitResult.exitCode !== 0) {
215
+ console.error('git commit failed: ' + commitResult.stderr);
216
+ return;
217
+ }
218
+
219
+ console.log(`Pushing to origin/${branch}...`);
220
+ const pushResult = await shell.exec(`git push origin "${branch}"`);
221
+ if (pushResult.exitCode !== 0) {
222
+ console.error('git push failed: ' + pushResult.stderr);
223
+ return;
224
+ }
225
+
226
+ console.log('');
227
+ console.log('Done. Changes committed and pushed.');
228
+ }
229
+
230
+ module.exports = { meta, run };