git-devflow 1.0.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 ADDED
@@ -0,0 +1,179 @@
1
+ # DevFlow ๐Ÿš€
2
+
3
+ AI-powered Git workflow automation using GitHub Copilot CLI. Streamline your development workflow with intelligent commit messages, PR descriptions, and branch naming.
4
+
5
+ [![GitHub Copilot CLI Challenge](https://img.shields.io/badge/GitHub-Copilot_CLI_Challenge-blue)](https://dev.to/challenges/github-2026-01-21)
6
+
7
+ ## โœจ Features
8
+
9
+ - **๐Ÿค– Smart Commit Messages** - AI-generated conventional commit messages from your changes
10
+ - **๐Ÿ“ PR Generation** - Automatic pull request descriptions with summary, changes, and testing notes
11
+ - **๐ŸŒฟ Branch Naming** - Semantic branch names from GitHub issues or descriptions
12
+ - **โš™๏ธ Config Management** - Secure token storage with interactive setup wizard
13
+ - **๐Ÿ’ฐ Cost-Aware Model Selection** - Dynamically fetches available models from Copilot CLI with cost info
14
+ - **๐Ÿ”„ Quota Auto-Retry** - Automatically falls back to a free model when premium requests are exhausted
15
+ - **๐Ÿ” Security First** - Tokens never displayed, error messages sanitized, config file locked down
16
+
17
+ ## ๐ŸŽฅ Demo
18
+
19
+ <img width="1178" height="559" alt="image" src="https://github.com/user-attachments/assets/b1f49000-7ecd-4415-bf69-670c7a1c4b36" />
20
+
21
+ <img width="1372" height="886" alt="image" src="https://github.com/user-attachments/assets/f04027fb-577d-4152-a9c4-2dfbdb3b6242" />
22
+
23
+ <img width="1372" height="886" alt="image" src="https://github.com/user-attachments/assets/1c185604-9328-4a97-935f-31cf24b92e4a" />
24
+
25
+ <img width="1372" height="886" alt="image" src="https://github.com/user-attachments/assets/1ad5c64d-5138-4d3d-b9a4-35a994be7383" />
26
+
27
+
28
+ ## ๐Ÿ“ฆ Installation
29
+
30
+ ```bash
31
+ npm install -g devflow-cli
32
+ ```
33
+
34
+ ## ๐Ÿ”ง Setup
35
+
36
+ ```bash
37
+ # Run interactive setup (single source of truth - no .env file needed)
38
+ devflow config setup
39
+ ```
40
+
41
+ The setup wizard will prompt you for:
42
+ - **GitHub Personal Access Token** - for creating PRs and fetching issues
43
+ - **Preferred AI model** - fetched dynamically from Copilot CLI with cost info
44
+ - **Default base branch** - typically `main` or `develop`
45
+
46
+ All config is stored securely in `~/.devflow/config.json` with restricted file permissions.
47
+
48
+ ## ๐ŸŽฏ Usage
49
+
50
+ ### Generate Commit Messages
51
+
52
+ ```bash
53
+ # Stage your changes
54
+ git add .
55
+
56
+ # Generate AI-powered commit message
57
+ devflow commit
58
+
59
+ # Or stage all changes automatically
60
+ devflow commit -a
61
+ ```
62
+
63
+ Each operation shows the model being used and its cost:
64
+ ```
65
+ ๐Ÿค– Model: claude-sonnet-4.5 Balanced ยท 1 premium request(s) per prompt
66
+ ```
67
+
68
+ ### Create Pull Requests
69
+
70
+ ```bash
71
+ # On your feature branch
72
+ devflow pr create
73
+
74
+ # Specify custom base branch
75
+ devflow pr create --base develop
76
+ ```
77
+
78
+ ### Create Branches
79
+
80
+ ```bash
81
+ # From GitHub issue
82
+ devflow branch create --issue 123
83
+
84
+ # From description
85
+ devflow branch create "add user authentication"
86
+
87
+ # Interactive mode
88
+ devflow branch create
89
+ ```
90
+
91
+ ### Configuration
92
+
93
+ ```bash
94
+ # Show current config
95
+ devflow config show
96
+
97
+ # Update specific setting
98
+ devflow config set copilotModel claude-haiku-4.5
99
+
100
+ # Get config value
101
+ devflow config get copilotModel
102
+
103
+ # Re-run full setup wizard
104
+ devflow config setup
105
+ ```
106
+
107
+ ## ๐Ÿ—๏ธ How It Works
108
+
109
+ DevFlow uses **GitHub Copilot CLI** as its AI engine:
110
+
111
+ 1. **Commit Command**: Reads `git diff`, sends to Copilot CLI with conventional commit format instructions
112
+ 2. **PR Command**: Analyzes commits since branch point, generates structured PR description
113
+ 3. **Branch Command**: Fetches GitHub issue or uses description, generates semantic branch name
114
+
115
+ ### Quota Handling
116
+
117
+ When your premium request quota is exceeded, DevFlow automatically:
118
+ 1. Detects the quota error from the Copilot CLI
119
+ 2. Retries the request with a **free model** (`gpt-4.1`) at no cost
120
+ 3. Suggests switching your default model via `devflow config setup`
121
+
122
+ If the free model also fails, DevFlow falls back to intelligent rule-based generation.
123
+
124
+ ## ๐ŸŽจ AI Model Selection
125
+
126
+ During setup, models are **fetched dynamically** from the Copilot CLI โ€” no hardcoded list. Each model is shown with its cost tier, sorted cheapest-first:
127
+
128
+ | Tier | Examples | Cost |
129
+ |------|----------|------|
130
+ | **Free** | `gpt-4.1`, `gpt-5-mini` | No premium requests |
131
+ | **Cheap** | `claude-haiku-4.5`, `gpt-5.1-codex-mini` | 0.33x per prompt |
132
+ | **Balanced** | `claude-sonnet-4.5`, `gpt-5.1-codex` | 1x per prompt |
133
+ | **Expensive** | `claude-opus-4.5` | 3x per prompt |
134
+
135
+ As GitHub adds or removes models, DevFlow automatically reflects the changes.
136
+
137
+ ## ๐Ÿ” Security
138
+
139
+ - GitHub tokens stored in `~/.devflow/config.json` with **600 file permissions** (owner-only)
140
+ - Tokens are **never displayed** in config output, error messages, or logs
141
+ - Sensitive config keys are redacted in `config get` and `config set` output
142
+ - Error messages are sanitized to prevent leaking auth headers or request details
143
+ - No `.env` file required โ€” config setup is the single source of truth
144
+ - Environment variables (`GITHUB_TOKEN`, `GH_TOKEN`) supported as fallback for CI
145
+
146
+ ## ๐Ÿ“ Requirements
147
+
148
+ - Node.js 18+
149
+ - Git
150
+ - [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) (`npm install -g @githubnext/github-copilot-cli`)
151
+ - GitHub account with Copilot access
152
+
153
+ ## Future Enhancements
154
+
155
+ - [ ] Ollama fallback for offline/quota-exceeded scenarios
156
+ - [ ] Custom prompt templates
157
+ - [ ] Team collaboration features
158
+
159
+ ## ๐Ÿค Contributing
160
+
161
+ Contributions welcome! This project was built for the [GitHub Copilot CLI Challenge](https://dev.to/challenges/github-2026-01-21).
162
+
163
+ ## ๐Ÿ“„ License
164
+
165
+ MIT
166
+
167
+ ## ๐Ÿ™ Acknowledgments
168
+
169
+ Built with โค๏ธ using:
170
+
171
+ - [GitHub Copilot CLI](https://githubnext.com/projects/copilot-cli)
172
+ - [Commander.js](https://github.com/tj/commander.js)
173
+ - [Inquirer Prompts](https://github.com/SBoudrias/Inquirer.js)
174
+ - [simple-git](https://github.com/steveukx/git-js)
175
+ - [@octokit/rest](https://github.com/octokit/rest.js)
176
+
177
+ ---
178
+
179
+ **Made for the GitHub Copilot CLI Challenge 2026** ๐Ÿš€
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.branchCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const git_js_1 = require("../services/git.js");
11
+ const github_js_1 = require("../services/github.js");
12
+ const copilot_js_1 = require("../services/copilot.js");
13
+ const spinner_js_1 = require("../utils/spinner.js");
14
+ exports.branchCommand = new commander_1.Command('branch')
15
+ .description('Branch management commands');
16
+ // Create branch command
17
+ exports.branchCommand
18
+ .command('create [description]')
19
+ .description('Create a new branch with AI-generated name')
20
+ .option('-i, --issue <number>', 'Create branch from GitHub issue number')
21
+ .option('-t, --type <type>', 'Branch type (feature/fix/chore/docs)', 'feature')
22
+ .action(async (description, options) => {
23
+ try {
24
+ const git = new git_js_1.GitService();
25
+ const github = new github_js_1.GitHubService();
26
+ await github.init();
27
+ const copilot = new copilot_js_1.CopilotService();
28
+ // Check if we're in a git repository
29
+ const isRepo = await git.isGitRepo();
30
+ if (!isRepo) {
31
+ console.log(chalk_1.default.red('โŒ Not a git repository'));
32
+ console.log(chalk_1.default.dim('Run: git init'));
33
+ process.exit(1);
34
+ }
35
+ let branchDescription = description;
36
+ let branchType = options?.type || 'feature';
37
+ // Scenario 1: Create from GitHub issue
38
+ if (options?.issue) {
39
+ console.log(chalk_1.default.cyan(`\n๐Ÿ” Fetching issue #${options.issue}...\n`));
40
+ const spinner = new spinner_js_1.Spinner('Fetching issue from GitHub...');
41
+ spinner.start();
42
+ try {
43
+ const issue = await github.getIssue(parseInt(options.issue));
44
+ spinner.succeed(`Found issue: ${issue.title}`);
45
+ branchDescription = issue.title;
46
+ console.log(chalk_1.default.dim(`\nIssue: ${issue.title}\n`));
47
+ // Ask for confirmation
48
+ const useIssue = await (0, prompts_1.confirm)({
49
+ message: 'Use this issue title for branch name?',
50
+ default: true
51
+ });
52
+ if (!useIssue) {
53
+ branchDescription = await (0, prompts_1.input)({
54
+ message: 'Enter branch description:',
55
+ validate: (value) => value.length > 0 || 'Description cannot be empty'
56
+ });
57
+ }
58
+ }
59
+ catch (error) {
60
+ spinner.fail('Failed to fetch issue');
61
+ const safeMsg = error instanceof Error ? error.message : 'Could not fetch issue';
62
+ console.log(chalk_1.default.red(`\nโŒ ${safeMsg}\n`));
63
+ process.exit(1);
64
+ }
65
+ }
66
+ // Scenario 2: No description provided - prompt user
67
+ if (!branchDescription) {
68
+ console.log(chalk_1.default.cyan('\n๐ŸŒฟ Create New Branch\n'));
69
+ branchType = await (0, prompts_1.select)({
70
+ message: 'Branch type:',
71
+ choices: [
72
+ { name: 'feature - New feature', value: 'feature' },
73
+ { name: 'fix - Bug fix', value: 'fix' },
74
+ { name: 'chore - Maintenance', value: 'chore' },
75
+ { name: 'docs - Documentation', value: 'docs' },
76
+ { name: 'refactor - Code refactoring', value: 'refactor' }
77
+ ]
78
+ });
79
+ branchDescription = await (0, prompts_1.input)({
80
+ message: 'Branch description:',
81
+ validate: (value) => value.length > 0 || 'Description cannot be empty'
82
+ });
83
+ }
84
+ // Generate branch name with Copilot
85
+ console.log(chalk_1.default.cyan('\n๐Ÿค– Generating branch name...\n'));
86
+ const spinner = new spinner_js_1.Spinner('Asking Copilot for branch name...');
87
+ spinner.start();
88
+ const branchName = await copilot.generateBranchName(branchDescription, branchType, options?.issue);
89
+ spinner.succeed('Branch name generated');
90
+ // Show generated name and confirm
91
+ console.log(chalk_1.default.cyan('\n๐Ÿ“ Generated branch name:'));
92
+ console.log(chalk_1.default.bold(` ${branchName}\n`));
93
+ const shouldCreate = await (0, prompts_1.confirm)({
94
+ message: `Create and checkout branch "${branchName}"?`,
95
+ default: true
96
+ });
97
+ if (!shouldCreate) {
98
+ console.log(chalk_1.default.yellow('โŒ Branch creation cancelled'));
99
+ process.exit(0);
100
+ }
101
+ // Create and checkout branch
102
+ const createSpinner = new spinner_js_1.Spinner('Creating branch...');
103
+ createSpinner.start();
104
+ await git.createAndCheckoutBranch(branchName);
105
+ createSpinner.succeed(`Switched to new branch: ${branchName}`);
106
+ console.log(chalk_1.default.green(`\nโœจ Successfully created branch: ${chalk_1.default.bold(branchName)}\n`));
107
+ }
108
+ catch (error) {
109
+ const safeMsg = error instanceof Error ? error.message : 'Something went wrong';
110
+ console.log(chalk_1.default.red(`\nโŒ ${safeMsg}\n`));
111
+ process.exit(1);
112
+ }
113
+ });
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.commitCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const git_js_1 = require("../services/git.js");
11
+ const copilot_js_1 = require("../services/copilot.js");
12
+ const spinner_js_1 = require("../utils/spinner.js");
13
+ // Force TTY mode for inquirer
14
+ process.stdin.isTTY = true;
15
+ process.stdout.isTTY = true;
16
+ exports.commitCommand = new commander_1.Command('commit')
17
+ .description('Generate AI-powered commit messages using GitHub Copilot CLI')
18
+ .option('-a, --all', 'Stage all changes before committing')
19
+ .action(async (options) => {
20
+ const git = new git_js_1.GitService();
21
+ const copilot = new copilot_js_1.CopilotService();
22
+ try {
23
+ // Check if we're in a git repo
24
+ if (!(await git.isGitRepo())) {
25
+ console.log(chalk_1.default.red('โŒ Not a git repository!'));
26
+ process.exit(1);
27
+ }
28
+ // Stage all if --all flag is used
29
+ if (options.all) {
30
+ const spinner = new spinner_js_1.Spinner('Staging all changes...');
31
+ spinner.start();
32
+ await git.stageAll();
33
+ spinner.succeed('Staged all changes');
34
+ }
35
+ // Check if there are staged changes
36
+ if (!(await git.hasStagedChanges())) {
37
+ console.log(chalk_1.default.yellow('โš ๏ธ No staged changes to commit'));
38
+ console.log(chalk_1.default.dim('Tip: Use `git add <files>` or `devflow commit --all`'));
39
+ process.exit(0);
40
+ }
41
+ // Get the diff
42
+ const spinner = new spinner_js_1.Spinner('Analyzing changes...');
43
+ spinner.start();
44
+ const diff = await git.getStagedDiff();
45
+ if (!diff) {
46
+ spinner.fail('No changes detected');
47
+ process.exit(0);
48
+ }
49
+ // Generate commit messages using Copilot CLI
50
+ spinner.update('๐Ÿค– Asking GitHub Copilot CLI for suggestions...');
51
+ const messages = await copilot.generateCommitMessage(diff);
52
+ spinner.succeed('Generated commit message options');
53
+ // Show options to user
54
+ console.log('\n');
55
+ const choices = messages.map((msg, idx) => ({
56
+ name: `${idx + 1}. ${msg}`,
57
+ value: msg
58
+ }));
59
+ choices.push({
60
+ name: chalk_1.default.dim('โœ๏ธ Write custom message'),
61
+ value: '__custom__'
62
+ });
63
+ const selectedMessage = await (0, prompts_1.select)({
64
+ message: 'Select a commit message:',
65
+ choices: choices,
66
+ pageSize: 10
67
+ });
68
+ // If custom, ask for message
69
+ let finalMessage = selectedMessage;
70
+ if (selectedMessage === '__custom__') {
71
+ finalMessage = await (0, prompts_1.input)({
72
+ message: 'Enter your commit message:',
73
+ validate: (value) => value.length > 0 || 'Commit message cannot be empty'
74
+ });
75
+ }
76
+ // Confirm before committing
77
+ const shouldCommit = await (0, prompts_1.confirm)({
78
+ message: `Commit with message: "${finalMessage}"?`,
79
+ default: true
80
+ });
81
+ if (!shouldCommit) {
82
+ console.log(chalk_1.default.yellow('โŒ Commit cancelled'));
83
+ process.exit(0);
84
+ }
85
+ // Create the commit
86
+ const commitSpinner = new spinner_js_1.Spinner('Creating commit...');
87
+ commitSpinner.start();
88
+ await git.commit(finalMessage);
89
+ commitSpinner.succeed(chalk_1.default.green(`โœจ Successfully committed: "${finalMessage}"`));
90
+ }
91
+ catch (error) {
92
+ console.log(chalk_1.default.red(`\nโŒ Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
93
+ process.exit(1);
94
+ }
95
+ });
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const config_js_1 = require("../services/config.js");
11
+ const copilot_js_1 = require("../services/copilot.js");
12
+ exports.configCommand = new commander_1.Command('config')
13
+ .description('Configure DevFlow settings');
14
+ // Setup wizard
15
+ exports.configCommand
16
+ .command('setup')
17
+ .description('Interactive setup wizard')
18
+ .action(async () => {
19
+ console.log(chalk_1.default.cyan('\nโœจ DevFlow Setup Wizard\n'));
20
+ console.log(chalk_1.default.yellow('๐Ÿ”’ Security Note:'));
21
+ console.log(chalk_1.default.dim('Your token will be stored in ~/.devflow/config.json'));
22
+ console.log(chalk_1.default.dim('with restricted permissions (600 - owner access only)\n'));
23
+ const config = new config_js_1.ConfigService();
24
+ const currentConfig = config.load();
25
+ try {
26
+ // GitHub Token
27
+ const needsToken = await (0, prompts_1.confirm)({
28
+ message: 'Do you want to configure a GitHub token?',
29
+ default: !config.hasToken()
30
+ });
31
+ if (needsToken) {
32
+ const token = await (0, prompts_1.password)({
33
+ message: 'Enter your GitHub Personal Access Token:',
34
+ mask: '*',
35
+ validate: (value) => {
36
+ if (!value)
37
+ return 'Token cannot be empty';
38
+ if (!value.startsWith('ghp_') && !value.startsWith('github_pat_')) {
39
+ return 'Token should start with ghp_ or github_pat_';
40
+ }
41
+ return true;
42
+ }
43
+ });
44
+ config.set('githubToken', token);
45
+ }
46
+ // Copilot Model Selection - fetch available models dynamically
47
+ const copilot = new copilot_js_1.CopilotService();
48
+ console.log(chalk_1.default.dim('Fetching available models from Copilot CLI...\n'));
49
+ const availableModels = await copilot.fetchAvailableModels();
50
+ // Sort: Free first, then Cheap, Balanced, Expensive, New
51
+ const tagOrder = {
52
+ 'Free': 0, 'Cheap': 1, 'Balanced': 2, 'Expensive': 3, 'New': 4
53
+ };
54
+ const sorted = [...availableModels].sort((a, b) => (tagOrder[a.tag] ?? 99) - (tagOrder[b.tag] ?? 99));
55
+ const tagColor = (tag) => {
56
+ switch (tag) {
57
+ case 'Free': return chalk_1.default.green(tag);
58
+ case 'Cheap': return chalk_1.default.cyan(tag);
59
+ case 'Balanced': return chalk_1.default.yellow(tag);
60
+ case 'Expensive': return chalk_1.default.red(tag);
61
+ default: return chalk_1.default.dim(tag);
62
+ }
63
+ };
64
+ const model = await (0, prompts_1.select)({
65
+ message: 'Select your preferred Copilot model:',
66
+ choices: sorted.map(m => ({
67
+ name: `${m.id} ${tagColor(m.tag)}`,
68
+ value: m.id,
69
+ description: m.description
70
+ })),
71
+ default: currentConfig.copilotModel || config_js_1.DEFAULT_COPILOT_MODEL
72
+ });
73
+ config.set('copilotModel', model);
74
+ // Default base branch
75
+ const baseBranch = await (0, prompts_1.input)({
76
+ message: 'Default base branch:',
77
+ default: currentConfig.defaultBaseBranch || 'main'
78
+ });
79
+ config.set('defaultBaseBranch', baseBranch);
80
+ console.log(chalk_1.default.green('\nโœ“ Setup complete!\n'));
81
+ config.display();
82
+ }
83
+ catch (error) {
84
+ console.log(chalk_1.default.yellow('\nโŒ Setup cancelled'));
85
+ process.exit(0);
86
+ }
87
+ });
88
+ // Show current config
89
+ exports.configCommand
90
+ .command('show')
91
+ .description('Show current configuration')
92
+ .action(() => {
93
+ const config = new config_js_1.ConfigService();
94
+ config.display();
95
+ });
96
+ // Keys that should never have their values printed
97
+ const SENSITIVE_KEYS = ['githubToken'];
98
+ // Set individual values
99
+ exports.configCommand
100
+ .command('set <key> <value>')
101
+ .description('Set a configuration value')
102
+ .action((key, value) => {
103
+ const config = new config_js_1.ConfigService();
104
+ const validKeys = ['githubToken', 'copilotModel', 'defaultBaseBranch'];
105
+ if (!validKeys.includes(key)) {
106
+ console.log(chalk_1.default.red(`โŒ Invalid key: ${key}`));
107
+ console.log(chalk_1.default.dim(`Valid keys: ${validKeys.join(', ')}`));
108
+ process.exit(1);
109
+ }
110
+ config.set(key, value);
111
+ if (SENSITIVE_KEYS.includes(key)) {
112
+ console.log(chalk_1.default.green(`โœ“ ${key} updated`));
113
+ }
114
+ else {
115
+ console.log(chalk_1.default.green(`โœ“ Set ${key} = ${value}`));
116
+ }
117
+ });
118
+ // Get individual values
119
+ exports.configCommand
120
+ .command('get <key>')
121
+ .description('Get a configuration value')
122
+ .action((key) => {
123
+ const config = new config_js_1.ConfigService();
124
+ const value = config.get(key);
125
+ if (!value) {
126
+ console.log(chalk_1.default.yellow(`${key} is not set`));
127
+ return;
128
+ }
129
+ if (SENSITIVE_KEYS.includes(key)) {
130
+ console.log(chalk_1.default.green('Configured'));
131
+ }
132
+ else {
133
+ console.log(value);
134
+ }
135
+ });