@proletariat/cli 0.3.24 → 0.3.25
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/dist/commands/action/create.js +3 -3
- package/dist/commands/action/update.js +3 -3
- package/dist/commands/epic/activate.js +9 -17
- package/dist/commands/epic/archive.js +13 -24
- package/dist/commands/epic/create.js +7 -6
- package/dist/commands/epic/move.js +28 -47
- package/dist/commands/epic/progress.js +10 -14
- package/dist/commands/epic/project.js +42 -59
- package/dist/commands/epic/reorder.js +25 -30
- package/dist/commands/epic/spec.d.ts +1 -0
- package/dist/commands/epic/spec.js +39 -40
- package/dist/commands/epic/ticket.d.ts +2 -0
- package/dist/commands/epic/ticket.js +63 -37
- package/dist/commands/feedback/index.d.ts +10 -0
- package/dist/commands/feedback/index.js +60 -0
- package/dist/commands/feedback/list.d.ts +12 -0
- package/dist/commands/feedback/list.js +126 -0
- package/dist/commands/feedback/submit.d.ts +16 -0
- package/dist/commands/feedback/submit.js +220 -0
- package/dist/commands/feedback/view.d.ts +15 -0
- package/dist/commands/feedback/view.js +109 -0
- package/dist/commands/gh/index.js +4 -0
- package/dist/commands/repo/create.d.ts +38 -0
- package/dist/commands/repo/create.js +283 -0
- package/dist/commands/repo/index.js +7 -0
- package/dist/commands/roadmap/add-project.js +9 -22
- package/dist/commands/roadmap/create.d.ts +0 -1
- package/dist/commands/roadmap/create.js +46 -40
- package/dist/commands/roadmap/delete.js +10 -24
- package/dist/commands/roadmap/generate.d.ts +1 -0
- package/dist/commands/roadmap/generate.js +21 -22
- package/dist/commands/roadmap/remove-project.js +14 -34
- package/dist/commands/roadmap/reorder.js +19 -26
- package/dist/commands/roadmap/update.js +27 -26
- package/dist/commands/roadmap/view.js +5 -12
- package/dist/commands/session/attach.d.ts +1 -8
- package/dist/commands/session/attach.js +93 -59
- package/dist/commands/session/list.d.ts +0 -8
- package/dist/commands/session/list.js +130 -81
- package/dist/commands/spec/create.js +1 -1
- package/dist/commands/spec/edit.js +63 -33
- package/dist/commands/support/book.d.ts +10 -0
- package/dist/commands/support/book.js +54 -0
- package/dist/commands/support/discord.d.ts +10 -0
- package/dist/commands/support/discord.js +54 -0
- package/dist/commands/support/docs.d.ts +10 -0
- package/dist/commands/support/docs.js +54 -0
- package/dist/commands/support/index.d.ts +19 -0
- package/dist/commands/support/index.js +81 -0
- package/dist/commands/support/issues.d.ts +11 -0
- package/dist/commands/support/issues.js +77 -0
- package/dist/commands/support/logs.d.ts +18 -0
- package/dist/commands/support/logs.js +247 -0
- package/dist/commands/ticket/create.js +21 -13
- package/dist/commands/ticket/edit.js +44 -13
- package/dist/commands/ticket/move.d.ts +7 -0
- package/dist/commands/ticket/move.js +132 -0
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +71 -7
- package/dist/commands/work/start.js +6 -0
- package/dist/lib/execution/runners.js +21 -17
- package/dist/lib/execution/session-utils.d.ts +60 -0
- package/dist/lib/execution/session-utils.js +162 -0
- package/dist/lib/execution/spawner.d.ts +2 -0
- package/dist/lib/execution/spawner.js +42 -0
- package/dist/lib/flags/resolver.d.ts +2 -2
- package/dist/lib/flags/resolver.js +15 -0
- package/dist/lib/init/index.js +18 -0
- package/dist/lib/multiline-input.d.ts +63 -0
- package/dist/lib/multiline-input.js +360 -0
- package/dist/lib/prompt-json.d.ts +5 -5
- package/dist/lib/repos/git.d.ts +7 -0
- package/dist/lib/repos/git.js +20 -0
- package/oclif.manifest.json +2206 -1607
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { styles } from '../../lib/styles.js';
|
|
7
|
+
import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
|
|
8
|
+
import { isMachineOutput, outputSuccessAsJson, outputErrorAsJson, outputPromptAsJson, buildPromptConfig, createMetadata, } from '../../lib/prompt-json.js';
|
|
9
|
+
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
10
|
+
// Target repository for feedback issues
|
|
11
|
+
const FEEDBACK_REPO = 'chrismcdermut/proletariat';
|
|
12
|
+
// Category to GitHub label mapping
|
|
13
|
+
const CATEGORY_LABELS = {
|
|
14
|
+
bug: 'bug',
|
|
15
|
+
feature: 'enhancement',
|
|
16
|
+
general: 'feedback',
|
|
17
|
+
};
|
|
18
|
+
export default class FeedbackSubmit extends Command {
|
|
19
|
+
static description = 'Submit feedback, bug reports, or feature requests to GitHub Issues';
|
|
20
|
+
static examples = [
|
|
21
|
+
'<%= config.bin %> <%= command.id %>',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> --title "Bug in CLI" --body "Description here" --category bug',
|
|
23
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
...machineOutputFlags,
|
|
27
|
+
title: Flags.string({
|
|
28
|
+
char: 't',
|
|
29
|
+
description: 'Issue title (one-liner) [required for non-interactive]',
|
|
30
|
+
}),
|
|
31
|
+
body: Flags.string({
|
|
32
|
+
char: 'b',
|
|
33
|
+
description: 'Issue description [required for non-interactive]',
|
|
34
|
+
}),
|
|
35
|
+
category: Flags.string({
|
|
36
|
+
char: 'c',
|
|
37
|
+
description: 'Feedback category',
|
|
38
|
+
options: ['bug', 'feature', 'general'],
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { flags } = await this.parse(FeedbackSubmit);
|
|
43
|
+
const jsonMode = isMachineOutput(flags);
|
|
44
|
+
// Helper to handle errors in JSON mode
|
|
45
|
+
const handleError = (code, message) => {
|
|
46
|
+
if (jsonMode) {
|
|
47
|
+
outputErrorAsJson(code, message, createMetadata('feedback submit', flags));
|
|
48
|
+
}
|
|
49
|
+
this.error(message);
|
|
50
|
+
};
|
|
51
|
+
// Check if gh CLI is installed
|
|
52
|
+
if (!isGHInstalled()) {
|
|
53
|
+
return handleError('GH_NOT_INSTALLED', 'GitHub CLI (gh) is not installed. Install it with: brew install gh');
|
|
54
|
+
}
|
|
55
|
+
// Check if gh is authenticated
|
|
56
|
+
if (!isGHAuthenticated()) {
|
|
57
|
+
return handleError('GH_NOT_AUTHENTICATED', 'GitHub CLI is not authenticated. Run: gh auth login');
|
|
58
|
+
}
|
|
59
|
+
// Collect category
|
|
60
|
+
let category = flags.category;
|
|
61
|
+
if (!category) {
|
|
62
|
+
const categoryChoices = [
|
|
63
|
+
{ name: 'Bug report', value: 'bug' },
|
|
64
|
+
{ name: 'Feature request', value: 'feature' },
|
|
65
|
+
{ name: 'General feedback', value: 'general' },
|
|
66
|
+
];
|
|
67
|
+
const categoryMessage = 'What type of feedback would you like to submit?';
|
|
68
|
+
if (jsonMode) {
|
|
69
|
+
outputPromptAsJson(buildPromptConfig('list', 'category', categoryMessage, categoryChoices), createMetadata('feedback submit', flags));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { selectedCategory } = await inquirer.prompt([{
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'selectedCategory',
|
|
75
|
+
message: categoryMessage,
|
|
76
|
+
choices: categoryChoices,
|
|
77
|
+
}]);
|
|
78
|
+
category = selectedCategory;
|
|
79
|
+
}
|
|
80
|
+
// Collect title
|
|
81
|
+
let title = flags.title;
|
|
82
|
+
if (!title) {
|
|
83
|
+
const titleMessage = 'Enter a brief title for your feedback:';
|
|
84
|
+
if (jsonMode) {
|
|
85
|
+
outputPromptAsJson(buildPromptConfig('input', 'title', titleMessage), createMetadata('feedback submit', { ...flags, category }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { inputTitle } = await inquirer.prompt([{
|
|
89
|
+
type: 'input',
|
|
90
|
+
name: 'inputTitle',
|
|
91
|
+
message: titleMessage,
|
|
92
|
+
validate: (input) => {
|
|
93
|
+
if (!input.trim()) {
|
|
94
|
+
return 'Title is required';
|
|
95
|
+
}
|
|
96
|
+
if (input.length > 200) {
|
|
97
|
+
return 'Title must be less than 200 characters';
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
}]);
|
|
102
|
+
title = inputTitle;
|
|
103
|
+
}
|
|
104
|
+
// Collect body/description
|
|
105
|
+
let body = flags.body;
|
|
106
|
+
if (!body) {
|
|
107
|
+
const bodyMessage = 'Describe your feedback in detail:';
|
|
108
|
+
if (jsonMode) {
|
|
109
|
+
outputPromptAsJson(buildPromptConfig('editor', 'body', bodyMessage), createMetadata('feedback submit', { ...flags, category, title }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { inputBody } = await inquirer.prompt([{
|
|
113
|
+
type: 'editor',
|
|
114
|
+
name: 'inputBody',
|
|
115
|
+
message: bodyMessage,
|
|
116
|
+
validate: (input) => {
|
|
117
|
+
if (!input.trim()) {
|
|
118
|
+
return 'Description is required';
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
}]);
|
|
123
|
+
body = inputBody;
|
|
124
|
+
}
|
|
125
|
+
// Build diagnostics section
|
|
126
|
+
const diagnostics = this.collectDiagnostics();
|
|
127
|
+
// Build full issue body with diagnostics
|
|
128
|
+
const fullBody = `${body}\n\n---\n\n## Diagnostics\n\n${diagnostics}`;
|
|
129
|
+
// Get label for category
|
|
130
|
+
const label = CATEGORY_LABELS[category] || 'feedback';
|
|
131
|
+
// Create the GitHub issue
|
|
132
|
+
if (!jsonMode) {
|
|
133
|
+
this.log(styles.muted('\nCreating issue on GitHub...\n'));
|
|
134
|
+
}
|
|
135
|
+
// Escape title for shell
|
|
136
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
137
|
+
let usedLabel = label;
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
// Try with label first
|
|
141
|
+
result = execSync(`gh issue create --repo "${FEEDBACK_REPO}" --title "${escapedTitle}" --body-file - --label "${label}"`, {
|
|
142
|
+
encoding: 'utf-8',
|
|
143
|
+
input: fullBody,
|
|
144
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
145
|
+
}).trim();
|
|
146
|
+
}
|
|
147
|
+
catch (labelError) {
|
|
148
|
+
// If label doesn't exist, try without label
|
|
149
|
+
const labelErrorMessage = labelError instanceof Error ? labelError.message : '';
|
|
150
|
+
if (labelErrorMessage.includes('not found') && labelErrorMessage.includes('label')) {
|
|
151
|
+
try {
|
|
152
|
+
usedLabel = null;
|
|
153
|
+
result = execSync(`gh issue create --repo "${FEEDBACK_REPO}" --title "${escapedTitle}" --body-file -`, {
|
|
154
|
+
encoding: 'utf-8',
|
|
155
|
+
input: fullBody,
|
|
156
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
157
|
+
}).trim();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error creating issue';
|
|
161
|
+
return handleError('ISSUE_CREATE_FAILED', `Failed to create issue: ${errorMessage}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const errorMessage = labelError instanceof Error ? labelError.message : 'Unknown error creating issue';
|
|
166
|
+
return handleError('ISSUE_CREATE_FAILED', `Failed to create issue: ${errorMessage}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Parse issue URL from result
|
|
170
|
+
const issueUrl = result;
|
|
171
|
+
const issueMatch = issueUrl.match(/\/issues\/(\d+)/);
|
|
172
|
+
const issueNumber = issueMatch ? parseInt(issueMatch[1], 10) : undefined;
|
|
173
|
+
if (jsonMode) {
|
|
174
|
+
outputSuccessAsJson({
|
|
175
|
+
issueUrl,
|
|
176
|
+
issueNumber,
|
|
177
|
+
title: title,
|
|
178
|
+
category: category,
|
|
179
|
+
label: usedLabel,
|
|
180
|
+
repository: FEEDBACK_REPO,
|
|
181
|
+
}, createMetadata('feedback submit', flags));
|
|
182
|
+
}
|
|
183
|
+
this.log(chalk.green(' ✓ Issue created successfully!\n'));
|
|
184
|
+
this.log(`${styles.header('Issue URL:')} ${chalk.cyan(issueUrl)}`);
|
|
185
|
+
if (issueNumber) {
|
|
186
|
+
this.log(`${styles.header('Issue #:')} ${issueNumber}`);
|
|
187
|
+
}
|
|
188
|
+
this.log(`${styles.header('Category:')} ${category}`);
|
|
189
|
+
if (usedLabel) {
|
|
190
|
+
this.log(`${styles.header('Label:')} ${usedLabel}\n`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.log(styles.muted(`(Label '${label}' not found in repository)\n`));
|
|
194
|
+
}
|
|
195
|
+
this.log(styles.muted('Thank you for your feedback!'));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Collect diagnostic information to include in the issue
|
|
199
|
+
*/
|
|
200
|
+
collectDiagnostics() {
|
|
201
|
+
const lines = [];
|
|
202
|
+
// CLI version
|
|
203
|
+
lines.push(`- **CLI Version:** ${this.config.version}`);
|
|
204
|
+
// Node version
|
|
205
|
+
lines.push(`- **Node Version:** ${process.version}`);
|
|
206
|
+
// OS info
|
|
207
|
+
lines.push(`- **OS:** ${process.platform}`);
|
|
208
|
+
lines.push(`- **OS Version:** ${os.release()}`);
|
|
209
|
+
// Try to get workspace name if available
|
|
210
|
+
try {
|
|
211
|
+
const cwd = process.cwd();
|
|
212
|
+
const workspaceName = cwd.split('/').pop() || 'unknown';
|
|
213
|
+
lines.push(`- **Working Directory:** ${workspaceName}`);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Ignore errors getting workspace name
|
|
217
|
+
}
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class FeedbackView extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
static args: {
|
|
9
|
+
number: import("@oclif/core/interfaces").Arg<number, {
|
|
10
|
+
max?: number;
|
|
11
|
+
min?: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command, Args } from '@oclif/core';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { styles } from '../../lib/styles.js';
|
|
5
|
+
import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
|
|
6
|
+
import { isMachineOutput, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
7
|
+
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
8
|
+
// Target repository for feedback issues
|
|
9
|
+
const FEEDBACK_REPO = 'chrismcdermut/proletariat';
|
|
10
|
+
export default class FeedbackView extends Command {
|
|
11
|
+
static description = 'View details of a specific feedback issue';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %> 123',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> 123 --json',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...machineOutputFlags,
|
|
18
|
+
};
|
|
19
|
+
static args = {
|
|
20
|
+
number: Args.integer({
|
|
21
|
+
description: 'Issue number to view',
|
|
22
|
+
required: true,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { args, flags } = await this.parse(FeedbackView);
|
|
27
|
+
const jsonMode = isMachineOutput(flags);
|
|
28
|
+
const issueNumber = args.number;
|
|
29
|
+
// Helper to handle errors in JSON mode
|
|
30
|
+
const handleError = (code, message) => {
|
|
31
|
+
if (jsonMode) {
|
|
32
|
+
outputErrorAsJson(code, message, createMetadata('feedback view', flags));
|
|
33
|
+
}
|
|
34
|
+
this.error(message);
|
|
35
|
+
};
|
|
36
|
+
// Check if gh CLI is installed
|
|
37
|
+
if (!isGHInstalled()) {
|
|
38
|
+
return handleError('GH_NOT_INSTALLED', 'GitHub CLI (gh) is not installed. Install it with: brew install gh');
|
|
39
|
+
}
|
|
40
|
+
// Check if gh is authenticated
|
|
41
|
+
if (!isGHAuthenticated()) {
|
|
42
|
+
return handleError('GH_NOT_AUTHENTICATED', 'GitHub CLI is not authenticated. Run: gh auth login');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
// Get issue details
|
|
46
|
+
const result = execSync(`gh issue view ${issueNumber} --repo "${FEEDBACK_REPO}" --json number,title,body,state,labels,createdAt,updatedAt,author,comments`, {
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
}).trim();
|
|
50
|
+
const issue = JSON.parse(result);
|
|
51
|
+
if (jsonMode) {
|
|
52
|
+
outputSuccessAsJson({
|
|
53
|
+
number: issue.number,
|
|
54
|
+
title: issue.title,
|
|
55
|
+
body: issue.body,
|
|
56
|
+
state: issue.state,
|
|
57
|
+
labels: issue.labels.map(l => l.name),
|
|
58
|
+
createdAt: issue.createdAt,
|
|
59
|
+
updatedAt: issue.updatedAt,
|
|
60
|
+
author: issue.author.login,
|
|
61
|
+
comments: issue.comments.map(c => ({
|
|
62
|
+
author: c.author.login,
|
|
63
|
+
body: c.body,
|
|
64
|
+
createdAt: c.createdAt,
|
|
65
|
+
})),
|
|
66
|
+
url: `https://github.com/${FEEDBACK_REPO}/issues/${issue.number}`,
|
|
67
|
+
repository: FEEDBACK_REPO,
|
|
68
|
+
}, createMetadata('feedback view', flags));
|
|
69
|
+
}
|
|
70
|
+
// Display issue in human-friendly format
|
|
71
|
+
const stateIcon = issue.state === 'OPEN' ? chalk.green('OPEN') : chalk.red('CLOSED');
|
|
72
|
+
const labels = issue.labels.map(l => chalk.cyan(`[${l.name}]`)).join(' ');
|
|
73
|
+
const createdDate = new Date(issue.createdAt).toLocaleString();
|
|
74
|
+
const updatedDate = new Date(issue.updatedAt).toLocaleString();
|
|
75
|
+
this.log(`\n${styles.header('Issue #')}${chalk.bold(String(issue.number))} ${stateIcon}\n`);
|
|
76
|
+
this.log(`${styles.header('Title:')} ${issue.title}`);
|
|
77
|
+
this.log(`${styles.header('Author:')} ${issue.author.login}`);
|
|
78
|
+
this.log(`${styles.header('Labels:')} ${labels || styles.muted('none')}`);
|
|
79
|
+
this.log(`${styles.header('Created:')} ${createdDate}`);
|
|
80
|
+
this.log(`${styles.header('Updated:')} ${updatedDate}`);
|
|
81
|
+
this.log(`${styles.header('URL:')} ${chalk.cyan(`https://github.com/${FEEDBACK_REPO}/issues/${issue.number}`)}`);
|
|
82
|
+
if (issue.body) {
|
|
83
|
+
this.log(`\n${styles.header('Description:')}`);
|
|
84
|
+
this.log(styles.muted('─'.repeat(50)));
|
|
85
|
+
this.log(issue.body);
|
|
86
|
+
this.log(styles.muted('─'.repeat(50)));
|
|
87
|
+
}
|
|
88
|
+
if (issue.comments && issue.comments.length > 0) {
|
|
89
|
+
this.log(`\n${styles.header('Comments:')} (${issue.comments.length})`);
|
|
90
|
+
this.log(styles.muted('─'.repeat(50)));
|
|
91
|
+
for (const comment of issue.comments) {
|
|
92
|
+
const commentDate = new Date(comment.createdAt).toLocaleString();
|
|
93
|
+
this.log(`\n${chalk.bold(comment.author.login)} ${styles.muted(`on ${commentDate}`)}`);
|
|
94
|
+
this.log(comment.body);
|
|
95
|
+
}
|
|
96
|
+
this.log(styles.muted('─'.repeat(50)));
|
|
97
|
+
}
|
|
98
|
+
this.log('');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error viewing issue';
|
|
102
|
+
// Check if it's a "not found" error
|
|
103
|
+
if (errorMessage.includes('not found') || errorMessage.includes('Could not resolve')) {
|
|
104
|
+
return handleError('ISSUE_NOT_FOUND', `Issue #${issueNumber} not found in ${FEEDBACK_REPO}`);
|
|
105
|
+
}
|
|
106
|
+
return handleError('ISSUE_VIEW_FAILED', `Failed to view issue: ${errorMessage}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -32,6 +32,7 @@ export default class GH extends Command {
|
|
|
32
32
|
{ name: 'Check status', value: 'status', command: 'prlt gh status --json' },
|
|
33
33
|
{ name: 'Login to GitHub', value: 'login', command: 'prlt gh login --json' },
|
|
34
34
|
{ name: 'Show GH_TOKEN setup', value: 'token', command: 'prlt gh token --json' },
|
|
35
|
+
{ name: 'Create GitHub repository', value: 'repo-create', command: 'prlt repo create --json' },
|
|
35
36
|
],
|
|
36
37
|
skipAutoCommand: true, // Use our custom commands above
|
|
37
38
|
});
|
|
@@ -48,6 +49,9 @@ export default class GH extends Command {
|
|
|
48
49
|
case 'token':
|
|
49
50
|
await this.config.runCommand('gh token');
|
|
50
51
|
break;
|
|
52
|
+
case 'repo-create':
|
|
53
|
+
await this.config.runCommand('repo create');
|
|
54
|
+
break;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
interface CreateRepoResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
url?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get the user's GitHub organizations.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getGHOrganizations(): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Create a GitHub repository using `gh` CLI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createGHRepo(options: {
|
|
16
|
+
name: string;
|
|
17
|
+
visibility: 'public' | 'private';
|
|
18
|
+
org?: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
push?: boolean;
|
|
21
|
+
}): CreateRepoResult;
|
|
22
|
+
export default class Create extends PMOCommand {
|
|
23
|
+
static description: string;
|
|
24
|
+
static examples: string[];
|
|
25
|
+
static flags: {
|
|
26
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
27
|
+
visibility: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
|
+
org: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
push: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
30
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
31
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
32
|
+
};
|
|
33
|
+
protected getPMOOptions(): {
|
|
34
|
+
promptIfMultiple: boolean;
|
|
35
|
+
};
|
|
36
|
+
execute(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
5
|
+
import { colors, format } from '../../lib/colors.js';
|
|
6
|
+
import { findHQRoot, isInGitRepo } from '../../lib/repos/index.js';
|
|
7
|
+
import { hasGitHubRemote } from '../../lib/repos/git.js';
|
|
8
|
+
import { isGHInstalled, isGHAuthenticated, getGHUsername, } from '../../lib/pr/index.js';
|
|
9
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, outputPromptAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
10
|
+
import { addRepositoriesToDatabase } from '../../lib/database/index.js';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// GitHub Helpers
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Get the user's GitHub organizations.
|
|
16
|
+
*/
|
|
17
|
+
export function getGHOrganizations() {
|
|
18
|
+
try {
|
|
19
|
+
const result = execSync('gh api user/orgs --jq ".[].login"', {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
22
|
+
}).trim();
|
|
23
|
+
return result ? result.split('\n').filter(Boolean) : [];
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a GitHub repository using `gh` CLI.
|
|
31
|
+
*/
|
|
32
|
+
export function createGHRepo(options) {
|
|
33
|
+
const { name, visibility, org, cwd, push = true } = options;
|
|
34
|
+
const repoName = org ? `${org}/${name}` : name;
|
|
35
|
+
const args = [
|
|
36
|
+
'repo', 'create', repoName,
|
|
37
|
+
`--${visibility}`,
|
|
38
|
+
'--source=.',
|
|
39
|
+
'--remote=origin',
|
|
40
|
+
];
|
|
41
|
+
if (push) {
|
|
42
|
+
args.push('--push');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = spawnSync('gh', args, {
|
|
46
|
+
cwd,
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: result.stderr || 'Failed to create repository',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Parse the repo URL from stdout
|
|
57
|
+
const url = result.stdout.trim();
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
url,
|
|
61
|
+
name: repoName,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Command
|
|
73
|
+
// =============================================================================
|
|
74
|
+
export default class Create extends PMOCommand {
|
|
75
|
+
static description = 'Create a GitHub repository and set up remote';
|
|
76
|
+
static examples = [
|
|
77
|
+
'<%= config.bin %> <%= command.id %>',
|
|
78
|
+
'<%= config.bin %> <%= command.id %> --name my-repo --visibility private',
|
|
79
|
+
'<%= config.bin %> <%= command.id %> --name my-repo --org my-company',
|
|
80
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
81
|
+
];
|
|
82
|
+
static flags = {
|
|
83
|
+
...pmoBaseFlags,
|
|
84
|
+
name: Flags.string({
|
|
85
|
+
char: 'n',
|
|
86
|
+
description: 'Repository name (defaults to current directory name)',
|
|
87
|
+
}),
|
|
88
|
+
visibility: Flags.string({
|
|
89
|
+
char: 'v',
|
|
90
|
+
description: 'Repository visibility',
|
|
91
|
+
options: ['public', 'private'],
|
|
92
|
+
default: 'private',
|
|
93
|
+
}),
|
|
94
|
+
org: Flags.string({
|
|
95
|
+
char: 'o',
|
|
96
|
+
description: 'GitHub organization (creates personal repo if not specified)',
|
|
97
|
+
}),
|
|
98
|
+
push: Flags.boolean({
|
|
99
|
+
description: 'Push initial commit after creating repo',
|
|
100
|
+
default: true,
|
|
101
|
+
allowNo: true,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
getPMOOptions() {
|
|
105
|
+
return { promptIfMultiple: false };
|
|
106
|
+
}
|
|
107
|
+
async execute() {
|
|
108
|
+
const { flags } = await this.parse(Create);
|
|
109
|
+
const jsonMode = shouldOutputJson(flags);
|
|
110
|
+
const metadata = createMetadata('repo create', flags);
|
|
111
|
+
const handleError = (code, message) => {
|
|
112
|
+
if (jsonMode) {
|
|
113
|
+
outputErrorAsJson(code, message, metadata);
|
|
114
|
+
this.exit(1);
|
|
115
|
+
}
|
|
116
|
+
this.error(message);
|
|
117
|
+
};
|
|
118
|
+
// Check prerequisites
|
|
119
|
+
if (!isGHInstalled()) {
|
|
120
|
+
return handleError('GH_NOT_INSTALLED', 'GitHub CLI (gh) is not installed. Install with: brew install gh');
|
|
121
|
+
}
|
|
122
|
+
if (!isGHAuthenticated()) {
|
|
123
|
+
return handleError('GH_NOT_AUTHENTICATED', 'Not authenticated with GitHub. Run: gh auth login');
|
|
124
|
+
}
|
|
125
|
+
// Must be in a git repo
|
|
126
|
+
if (!isInGitRepo()) {
|
|
127
|
+
return handleError('NOT_GIT_REPO', 'Not in a git repository. Initialize one with: git init');
|
|
128
|
+
}
|
|
129
|
+
// Check if remote already exists
|
|
130
|
+
if (hasGitHubRemote()) {
|
|
131
|
+
return handleError('REMOTE_EXISTS', 'This repository already has a GitHub remote configured.');
|
|
132
|
+
}
|
|
133
|
+
// Gather parameters
|
|
134
|
+
let repoName = flags.name;
|
|
135
|
+
let visibility = flags.visibility;
|
|
136
|
+
let org = flags.org;
|
|
137
|
+
// Get default repo name from directory
|
|
138
|
+
if (!repoName) {
|
|
139
|
+
repoName = path.basename(process.cwd());
|
|
140
|
+
}
|
|
141
|
+
// Prompt for repo name
|
|
142
|
+
const nameMessage = 'Repository name:';
|
|
143
|
+
if (jsonMode) {
|
|
144
|
+
if (!flags.name) {
|
|
145
|
+
outputPromptAsJson(buildPromptConfig('input', 'repoName', nameMessage, undefined, repoName), metadata);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const nameResult = await this.prompt([{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'repoName',
|
|
153
|
+
message: nameMessage,
|
|
154
|
+
default: repoName,
|
|
155
|
+
validate: (input) => {
|
|
156
|
+
const value = String(input || '');
|
|
157
|
+
if (!value.trim())
|
|
158
|
+
return 'Repository name is required';
|
|
159
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(value)) {
|
|
160
|
+
return 'Repository name can only contain letters, numbers, dots, hyphens, and underscores';
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
}], null);
|
|
165
|
+
repoName = nameResult.repoName;
|
|
166
|
+
}
|
|
167
|
+
// Prompt for visibility
|
|
168
|
+
const visibilityChoices = [
|
|
169
|
+
{ name: 'Private', value: 'private' },
|
|
170
|
+
{ name: 'Public', value: 'public' },
|
|
171
|
+
];
|
|
172
|
+
const visibilityMessage = 'Repository visibility:';
|
|
173
|
+
if (jsonMode) {
|
|
174
|
+
if (!flags.visibility) {
|
|
175
|
+
outputPromptAsJson(buildPromptConfig('list', 'visibility', visibilityMessage, visibilityChoices, visibility), metadata);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const visibilityResult = await this.prompt([{
|
|
181
|
+
type: 'list',
|
|
182
|
+
name: 'visibility',
|
|
183
|
+
message: visibilityMessage,
|
|
184
|
+
choices: visibilityChoices,
|
|
185
|
+
default: visibility,
|
|
186
|
+
}], null);
|
|
187
|
+
visibility = visibilityResult.visibility;
|
|
188
|
+
}
|
|
189
|
+
// Prompt for org if orgs are available
|
|
190
|
+
const orgs = getGHOrganizations();
|
|
191
|
+
const username = getGHUsername();
|
|
192
|
+
if (orgs.length > 0 && !flags.org) {
|
|
193
|
+
const ownerChoices = [
|
|
194
|
+
{ name: `${username || 'Personal'} (personal account)`, value: '' },
|
|
195
|
+
...orgs.map(o => ({ name: o, value: o })),
|
|
196
|
+
];
|
|
197
|
+
const orgMessage = 'Create repository under:';
|
|
198
|
+
if (jsonMode) {
|
|
199
|
+
outputPromptAsJson(buildPromptConfig('list', 'org', orgMessage, ownerChoices), metadata);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const orgResult = await this.prompt([{
|
|
203
|
+
type: 'list',
|
|
204
|
+
name: 'org',
|
|
205
|
+
message: orgMessage,
|
|
206
|
+
choices: ownerChoices,
|
|
207
|
+
}], null);
|
|
208
|
+
org = orgResult.org || undefined;
|
|
209
|
+
}
|
|
210
|
+
// Confirm creation (interactive only)
|
|
211
|
+
if (!jsonMode) {
|
|
212
|
+
this.log('');
|
|
213
|
+
this.log(colors.primary('Creating GitHub repository:'));
|
|
214
|
+
this.log(` Name: ${org ? `${org}/${repoName}` : repoName}`);
|
|
215
|
+
this.log(` Visibility: ${visibility}`);
|
|
216
|
+
this.log(` Push initial commit: ${flags.push ? 'Yes' : 'No'}`);
|
|
217
|
+
this.log('');
|
|
218
|
+
const confirmChoices = [
|
|
219
|
+
{ name: 'Yes', value: true },
|
|
220
|
+
{ name: 'No', value: false },
|
|
221
|
+
];
|
|
222
|
+
const confirmResult = await this.prompt([{
|
|
223
|
+
type: 'list',
|
|
224
|
+
name: 'confirm',
|
|
225
|
+
message: 'Proceed with creation?',
|
|
226
|
+
choices: confirmChoices,
|
|
227
|
+
}], null);
|
|
228
|
+
if (!confirmResult.confirm) {
|
|
229
|
+
this.log(colors.textMuted('Operation cancelled.'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Create the repository
|
|
234
|
+
if (!jsonMode) {
|
|
235
|
+
this.log(colors.primary('Creating GitHub repository...'));
|
|
236
|
+
}
|
|
237
|
+
const result = createGHRepo({
|
|
238
|
+
name: repoName,
|
|
239
|
+
visibility,
|
|
240
|
+
org,
|
|
241
|
+
push: flags.push,
|
|
242
|
+
});
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
return handleError('CREATE_FAILED', `Failed to create repository: ${result.error}`);
|
|
245
|
+
}
|
|
246
|
+
// Update prlt database if in HQ
|
|
247
|
+
const hqPath = findHQRoot();
|
|
248
|
+
if (hqPath) {
|
|
249
|
+
try {
|
|
250
|
+
// Use the actual local folder name for the DB path, not repoName
|
|
251
|
+
// (repoName may differ if user overrides it via --name)
|
|
252
|
+
const localFolderName = path.basename(process.cwd());
|
|
253
|
+
addRepositoriesToDatabase(hqPath, [{
|
|
254
|
+
name: repoName,
|
|
255
|
+
path: `repos/${localFolderName}`,
|
|
256
|
+
source_url: result.url,
|
|
257
|
+
}]);
|
|
258
|
+
if (!jsonMode) {
|
|
259
|
+
this.log(colors.textMuted('Updated prlt workspace database.'));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Ignore database update errors - repo was still created
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Success output
|
|
267
|
+
if (jsonMode) {
|
|
268
|
+
outputSuccessAsJson({
|
|
269
|
+
created: true,
|
|
270
|
+
name: result.name,
|
|
271
|
+
url: result.url,
|
|
272
|
+
visibility,
|
|
273
|
+
org: org || null,
|
|
274
|
+
}, metadata);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.log('');
|
|
278
|
+
this.log(format.success(`Repository created: ${result.url}`));
|
|
279
|
+
this.log('');
|
|
280
|
+
this.log(colors.textMuted('Your local repository is now connected to GitHub.'));
|
|
281
|
+
this.log(colors.textMuted('You can push changes with: git push'));
|
|
282
|
+
}
|
|
283
|
+
}
|