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 +179 -0
- package/dist/commands/branch.js +113 -0
- package/dist/commands/commit.js +95 -0
- package/dist/commands/config.js +135 -0
- package/dist/commands/pr.js +167 -0
- package/dist/index.js +45 -0
- package/dist/services/config.js +105 -0
- package/dist/services/copilot.js +456 -0
- package/dist/services/git.js +133 -0
- package/dist/services/github.js +160 -0
- package/dist/utils/spinner.js +42 -0
- package/package.json +53 -0
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
|
+
[](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
|
+
});
|