@patternfly/patternfly-cli 1.0.2
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/.github/workflows/build.yml +21 -0
- package/.github/workflows/lint.yml +24 -0
- package/.github/workflows/release.yml +42 -0
- package/.github/workflows/test.yml +24 -0
- package/.releaserc.json +14 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/__mocks__/execa.js +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/github.d.ts +33 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +180 -0
- package/dist/github.js.map +1 -0
- package/dist/load.d.ts +6 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +40 -0
- package/dist/load.js.map +1 -0
- package/dist/save.d.ts +6 -0
- package/dist/save.d.ts.map +1 -0
- package/dist/save.js +97 -0
- package/dist/save.js.map +1 -0
- package/dist/template-loader.d.ts +6 -0
- package/dist/template-loader.d.ts.map +1 -0
- package/dist/template-loader.js +83 -0
- package/dist/template-loader.js.map +1 -0
- package/dist/templates.d.ts +17 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +33 -0
- package/dist/templates.js.map +1 -0
- package/eslint.config.js +19 -0
- package/package.json +80 -0
- package/src/__tests__/cli.test.ts +171 -0
- package/src/__tests__/fixtures/invalid-template-bad-options.json +1 -0
- package/src/__tests__/fixtures/invalid-template-missing-name.json +1 -0
- package/src/__tests__/fixtures/not-array.json +1 -0
- package/src/__tests__/fixtures/valid-templates.json +14 -0
- package/src/__tests__/github.test.ts +214 -0
- package/src/__tests__/load.test.ts +106 -0
- package/src/__tests__/save.test.ts +272 -0
- package/src/cli.ts +307 -0
- package/src/github.ts +193 -0
- package/src/load.ts +38 -0
- package/src/save.ts +103 -0
- package/src/template-loader.ts +84 -0
- package/src/templates.ts +48 -0
- package/tsconfig.json +50 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
jest.mock('fs-extra', () => ({
|
|
2
|
+
__esModule: true,
|
|
3
|
+
default: {
|
|
4
|
+
pathExists: jest.fn(),
|
|
5
|
+
},
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock('execa', () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
execa: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('inquirer', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: {
|
|
16
|
+
prompt: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('../github.js', () => ({
|
|
21
|
+
offerAndCreateGitHubRepo: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import fs from 'fs-extra';
|
|
25
|
+
import { execa } from 'execa';
|
|
26
|
+
import inquirer from 'inquirer';
|
|
27
|
+
import { offerAndCreateGitHubRepo } from '../github.js';
|
|
28
|
+
import { runSave } from '../save.js';
|
|
29
|
+
|
|
30
|
+
const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists>;
|
|
31
|
+
const mockExeca = execa as jest.MockedFunction<typeof execa>;
|
|
32
|
+
const mockPrompt = inquirer.prompt as jest.MockedFunction<typeof inquirer.prompt>;
|
|
33
|
+
const mockOfferAndCreateGitHubRepo = offerAndCreateGitHubRepo as jest.MockedFunction<typeof offerAndCreateGitHubRepo>;
|
|
34
|
+
|
|
35
|
+
const cwd = '/tmp/my-repo';
|
|
36
|
+
|
|
37
|
+
describe('runSave', () => {
|
|
38
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
39
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
consoleErrorSpy.mockRestore();
|
|
47
|
+
consoleLogSpy.mockRestore();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('throws and logs error when .git directory does not exist', async () => {
|
|
51
|
+
mockPathExists.mockResolvedValue(false);
|
|
52
|
+
|
|
53
|
+
await expect(runSave(cwd)).rejects.toThrow('Not a git repository');
|
|
54
|
+
expect(mockPathExists).toHaveBeenCalledWith(`${cwd}/.git`);
|
|
55
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
56
|
+
expect.stringContaining('This directory is not a git repository'),
|
|
57
|
+
);
|
|
58
|
+
expect(mockExeca).not.toHaveBeenCalled();
|
|
59
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('logs "No changes to save" and returns when working tree is clean', async () => {
|
|
63
|
+
mockPathExists.mockResolvedValue(true);
|
|
64
|
+
mockExeca.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
|
|
65
|
+
|
|
66
|
+
await runSave(cwd);
|
|
67
|
+
|
|
68
|
+
expect(mockExeca).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(mockExeca).toHaveBeenCalledWith('git', ['status', '--porcelain'], {
|
|
70
|
+
cwd,
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
});
|
|
73
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
74
|
+
expect.stringContaining('No changes to save'),
|
|
75
|
+
);
|
|
76
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('logs "No changes to save" when status output is only whitespace', async () => {
|
|
80
|
+
mockPathExists.mockResolvedValue(true);
|
|
81
|
+
mockExeca.mockResolvedValue({ stdout: ' \n ', stderr: '', exitCode: 0 });
|
|
82
|
+
|
|
83
|
+
await runSave(cwd);
|
|
84
|
+
|
|
85
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
86
|
+
expect.stringContaining('No changes to save'),
|
|
87
|
+
);
|
|
88
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('prompts to save; when user says no, logs "Nothing has been saved"', async () => {
|
|
92
|
+
mockPathExists.mockResolvedValue(true);
|
|
93
|
+
mockExeca.mockResolvedValue({ stdout: ' M file.ts', stderr: '', exitCode: 0 });
|
|
94
|
+
mockPrompt.mockResolvedValueOnce({ saveChanges: false });
|
|
95
|
+
|
|
96
|
+
await runSave(cwd);
|
|
97
|
+
|
|
98
|
+
expect(mockPrompt).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(mockPrompt).toHaveBeenCalledWith([
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'saveChanges',
|
|
103
|
+
message: expect.stringContaining('Would you like to save them?'),
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
107
|
+
expect.stringContaining('Nothing has been saved'),
|
|
108
|
+
);
|
|
109
|
+
expect(mockExeca).toHaveBeenCalledTimes(1); // only status, no add/commit/push
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('when user says yes but message is empty, logs "No message provided"', async () => {
|
|
113
|
+
mockPathExists.mockResolvedValue(true);
|
|
114
|
+
mockExeca.mockResolvedValue({ stdout: ' M file.ts', stderr: '', exitCode: 0 });
|
|
115
|
+
mockPrompt
|
|
116
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
117
|
+
.mockResolvedValueOnce({ message: ' ' });
|
|
118
|
+
|
|
119
|
+
await runSave(cwd);
|
|
120
|
+
|
|
121
|
+
expect(mockPrompt).toHaveBeenCalledTimes(2);
|
|
122
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('No message provided'),
|
|
124
|
+
);
|
|
125
|
+
expect(mockExeca).toHaveBeenCalledTimes(1); // only status
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('when user says yes and provides message, runs add, commit, push and logs success', async () => {
|
|
129
|
+
mockPathExists.mockResolvedValue(true);
|
|
130
|
+
mockExeca
|
|
131
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
132
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
133
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
134
|
+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
|
|
135
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
|
|
136
|
+
mockPrompt
|
|
137
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
138
|
+
.mockResolvedValueOnce({ message: 'Fix bug in save command' });
|
|
139
|
+
|
|
140
|
+
await runSave(cwd);
|
|
141
|
+
|
|
142
|
+
expect(mockExeca).toHaveBeenCalledTimes(5);
|
|
143
|
+
expect(mockExeca).toHaveBeenNthCalledWith(1, 'git', ['status', '--porcelain'], {
|
|
144
|
+
cwd,
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
});
|
|
147
|
+
expect(mockExeca).toHaveBeenNthCalledWith(2, 'git', ['add', '.'], {
|
|
148
|
+
cwd,
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
});
|
|
151
|
+
expect(mockExeca).toHaveBeenNthCalledWith(3, 'git', [
|
|
152
|
+
'commit',
|
|
153
|
+
'-m',
|
|
154
|
+
'Fix bug in save command',
|
|
155
|
+
], { cwd, stdio: 'inherit' });
|
|
156
|
+
expect(mockExeca).toHaveBeenNthCalledWith(4, 'git', ['remote', 'get-url', 'origin'], expect.any(Object));
|
|
157
|
+
expect(mockExeca).toHaveBeenNthCalledWith(5, 'git', ['push'], {
|
|
158
|
+
cwd,
|
|
159
|
+
stdio: 'inherit',
|
|
160
|
+
});
|
|
161
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
162
|
+
expect.stringContaining('Changes saved and pushed to GitHub successfully'),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('throws and logs push-failure message when push fails with exitCode 128', async () => {
|
|
167
|
+
mockPathExists.mockResolvedValue(true);
|
|
168
|
+
mockExeca
|
|
169
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
170
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
171
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
172
|
+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
|
|
173
|
+
.mockRejectedValueOnce(Object.assign(new Error('push failed'), { exitCode: 128 }));
|
|
174
|
+
|
|
175
|
+
mockPrompt
|
|
176
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
177
|
+
.mockResolvedValueOnce({ message: 'WIP' });
|
|
178
|
+
|
|
179
|
+
await expect(runSave(cwd)).rejects.toMatchObject({ message: 'push failed' });
|
|
180
|
+
|
|
181
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
182
|
+
expect.stringContaining('Push failed'),
|
|
183
|
+
);
|
|
184
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
185
|
+
expect.stringContaining('remote'),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('throws and logs generic failure when add/commit/push fails with other exitCode', async () => {
|
|
190
|
+
mockPathExists.mockResolvedValue(true);
|
|
191
|
+
mockExeca
|
|
192
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
193
|
+
.mockRejectedValueOnce(Object.assign(new Error('add failed'), { exitCode: 1 }));
|
|
194
|
+
|
|
195
|
+
mockPrompt
|
|
196
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
197
|
+
.mockResolvedValueOnce({ message: 'WIP' });
|
|
198
|
+
|
|
199
|
+
await expect(runSave(cwd)).rejects.toMatchObject({ message: 'add failed' });
|
|
200
|
+
|
|
201
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
202
|
+
expect.stringContaining('Save or push failed'),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('throws and logs error when execa throws without exitCode', async () => {
|
|
207
|
+
mockPathExists.mockResolvedValue(true);
|
|
208
|
+
mockExeca
|
|
209
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
210
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
211
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
212
|
+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
|
|
213
|
+
.mockRejectedValueOnce(new Error('network error'));
|
|
214
|
+
|
|
215
|
+
mockPrompt
|
|
216
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
217
|
+
.mockResolvedValueOnce({ message: 'WIP' });
|
|
218
|
+
|
|
219
|
+
await expect(runSave(cwd)).rejects.toThrow('network error');
|
|
220
|
+
|
|
221
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
222
|
+
expect.stringContaining('An error occurred'),
|
|
223
|
+
);
|
|
224
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
225
|
+
expect.stringContaining('network error'),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('when no remote origin, offers to create GitHub repo and throws if user does not create', async () => {
|
|
230
|
+
mockPathExists.mockResolvedValue(true);
|
|
231
|
+
mockExeca
|
|
232
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
233
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
234
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
235
|
+
.mockRejectedValueOnce(new Error('no remote'));
|
|
236
|
+
mockOfferAndCreateGitHubRepo.mockResolvedValue(false);
|
|
237
|
+
|
|
238
|
+
mockPrompt
|
|
239
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
240
|
+
.mockResolvedValueOnce({ message: 'WIP' });
|
|
241
|
+
|
|
242
|
+
await expect(runSave(cwd)).rejects.toThrow('No remote origin');
|
|
243
|
+
expect(mockOfferAndCreateGitHubRepo).toHaveBeenCalledWith(cwd);
|
|
244
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
245
|
+
expect.stringContaining('Set a remote'),
|
|
246
|
+
);
|
|
247
|
+
expect(mockExeca).not.toHaveBeenCalledWith('git', ['push'], expect.any(Object));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('when no remote origin and user creates GitHub repo, pushes successfully', async () => {
|
|
251
|
+
mockPathExists.mockResolvedValue(true);
|
|
252
|
+
mockExeca
|
|
253
|
+
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
|
|
254
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
255
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
|
|
256
|
+
.mockRejectedValueOnce(new Error('no remote'))
|
|
257
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
|
|
258
|
+
mockOfferAndCreateGitHubRepo.mockResolvedValue(true);
|
|
259
|
+
|
|
260
|
+
mockPrompt
|
|
261
|
+
.mockResolvedValueOnce({ saveChanges: true })
|
|
262
|
+
.mockResolvedValueOnce({ message: 'WIP' });
|
|
263
|
+
|
|
264
|
+
await runSave(cwd);
|
|
265
|
+
|
|
266
|
+
expect(mockOfferAndCreateGitHubRepo).toHaveBeenCalledWith(cwd);
|
|
267
|
+
expect(mockExeca).toHaveBeenCalledWith('git', ['push'], { cwd, stdio: 'inherit' });
|
|
268
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
269
|
+
expect.stringContaining('Changes saved and pushed to GitHub successfully'),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { defaultTemplates } from './templates.js';
|
|
9
|
+
import { mergeTemplates } from './template-loader.js';
|
|
10
|
+
import { offerAndCreateGitHubRepo } from './github.js';
|
|
11
|
+
import { runSave } from './save.js';
|
|
12
|
+
import { runLoad } from './load.js';
|
|
13
|
+
|
|
14
|
+
/** Project data provided by the user */
|
|
15
|
+
type ProjectData = {
|
|
16
|
+
/** Project name */
|
|
17
|
+
name: string,
|
|
18
|
+
/** Project version */
|
|
19
|
+
version: string,
|
|
20
|
+
/** Project description */
|
|
21
|
+
description: string,
|
|
22
|
+
/** Project author */
|
|
23
|
+
author: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Command to create a new project */
|
|
27
|
+
program
|
|
28
|
+
.version('1.0.0')
|
|
29
|
+
.command('create')
|
|
30
|
+
.description('Create a new project from a git template')
|
|
31
|
+
.argument('[project-directory]', 'The directory to create the project in')
|
|
32
|
+
.argument('[template-name]', 'The name of the template to use')
|
|
33
|
+
.option('-t, --template-file <path>', 'Path to a JSON file with custom templates (same format as built-in)')
|
|
34
|
+
.option('--ssh', 'Use SSH URL for cloning the template repository')
|
|
35
|
+
.action(async (projectDirectory, templateName, options) => {
|
|
36
|
+
const templatesToUse = mergeTemplates(defaultTemplates, options?.templateFile);
|
|
37
|
+
|
|
38
|
+
// If project directory is not provided, prompt for it
|
|
39
|
+
if (!projectDirectory) {
|
|
40
|
+
const projectDirAnswer = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'input',
|
|
43
|
+
name: 'projectDirectory',
|
|
44
|
+
message: 'Please provide the directory where you want to create the project?',
|
|
45
|
+
default: 'my-app',
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
projectDirectory = projectDirAnswer.projectDirectory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If template name is not provided, show available templates and let user select
|
|
52
|
+
if (!templateName) {
|
|
53
|
+
console.log('\n๐ Available templates:\n');
|
|
54
|
+
templatesToUse.forEach(t => {
|
|
55
|
+
console.log(` ${t.name.padEnd(12)} - ${t.description}`);
|
|
56
|
+
});
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
const templateQuestion = [
|
|
60
|
+
{
|
|
61
|
+
type: 'list',
|
|
62
|
+
name: 'templateName',
|
|
63
|
+
message: 'Select a template:',
|
|
64
|
+
choices: templatesToUse.map(t => ({
|
|
65
|
+
name: `${t.name} - ${t.description}`,
|
|
66
|
+
value: t.name
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const templateAnswer = await inquirer.prompt(templateQuestion);
|
|
72
|
+
templateName = templateAnswer.templateName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Look up the template by name
|
|
76
|
+
const template = templatesToUse.find(t => t.name === templateName);
|
|
77
|
+
if (!template) {
|
|
78
|
+
console.error(`โ Template "${templateName}" not found.\n`);
|
|
79
|
+
console.log('๐ Available templates:\n');
|
|
80
|
+
templatesToUse.forEach(t => {
|
|
81
|
+
console.log(` ${t.name.padEnd(12)} - ${t.description}`);
|
|
82
|
+
});
|
|
83
|
+
console.log('');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If --ssh was not passed, prompt whether to use SSH
|
|
88
|
+
let useSSH = options?.ssh;
|
|
89
|
+
if (useSSH === undefined && template.repoSSH) {
|
|
90
|
+
const sshAnswer = await inquirer.prompt([
|
|
91
|
+
{
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
name: 'useSSH',
|
|
94
|
+
message: 'Use SSH URL for cloning?',
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
useSSH = sshAnswer.useSSH;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const templateRepoUrl = useSSH && template.repoSSH ? template.repoSSH : template.repo;
|
|
102
|
+
|
|
103
|
+
// Define the full path for the new project
|
|
104
|
+
const projectPath = path.resolve(projectDirectory);
|
|
105
|
+
console.log(`Cloning template "${templateName}" from ${templateRepoUrl} into ${projectPath}...`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
|
|
109
|
+
// Clone the repository
|
|
110
|
+
const cloneArgs = ['clone'];
|
|
111
|
+
if (template.options && Array.isArray(template.options)) {
|
|
112
|
+
cloneArgs.push(...template.options);
|
|
113
|
+
}
|
|
114
|
+
cloneArgs.push(templateRepoUrl, projectPath);
|
|
115
|
+
await execa('git', cloneArgs, { stdio: 'inherit' });
|
|
116
|
+
console.log('โ
Template cloned successfully.');
|
|
117
|
+
|
|
118
|
+
// Remove the .git folder from the *new* project
|
|
119
|
+
await fs.remove(path.join(projectPath, '.git'));
|
|
120
|
+
console.log('๐งน Cleaned up template .git directory.');
|
|
121
|
+
|
|
122
|
+
// Ask user for customization details
|
|
123
|
+
const questions = [
|
|
124
|
+
{
|
|
125
|
+
type: 'input',
|
|
126
|
+
name: 'name',
|
|
127
|
+
message: 'What is the project name?',
|
|
128
|
+
default: path.basename(projectPath),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'version',
|
|
133
|
+
message: 'What version number would you like to use?',
|
|
134
|
+
default: '1.0.0',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'input',
|
|
138
|
+
name: 'description',
|
|
139
|
+
message: 'What is the project description?',
|
|
140
|
+
default: '',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'input',
|
|
144
|
+
name: 'author',
|
|
145
|
+
message: 'Who is the author of the project?',
|
|
146
|
+
default: '',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const answers: ProjectData = await inquirer.prompt(questions);
|
|
151
|
+
|
|
152
|
+
// Update the package.json in the new project
|
|
153
|
+
const pkgJsonPath = path.join(projectPath, 'package.json');
|
|
154
|
+
|
|
155
|
+
if (await fs.pathExists(pkgJsonPath)) {
|
|
156
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
157
|
+
|
|
158
|
+
// Overwrite fields with user's answers
|
|
159
|
+
pkgJson.name = answers.name;
|
|
160
|
+
pkgJson.version = answers.version;
|
|
161
|
+
pkgJson.description = answers.description;
|
|
162
|
+
pkgJson.author = answers.author;
|
|
163
|
+
|
|
164
|
+
// Write the updated package.json back
|
|
165
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
166
|
+
console.log('๐ Customized package.json.');
|
|
167
|
+
} else {
|
|
168
|
+
console.log('โน๏ธ No package.json found in template, skipping customization.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const packageManager = template.packageManager || "npm";
|
|
172
|
+
// Install dependencies
|
|
173
|
+
console.log('๐ฆ Installing dependencies... (This may take a moment)');
|
|
174
|
+
await execa(packageManager, ['install'], { cwd: projectPath, stdio: 'inherit' });
|
|
175
|
+
console.log('โ
Dependencies installed.');
|
|
176
|
+
|
|
177
|
+
// Optional: Create GitHub repository
|
|
178
|
+
await offerAndCreateGitHubRepo(projectPath);
|
|
179
|
+
|
|
180
|
+
// Let the user know the project was created successfully
|
|
181
|
+
console.log('\nโจ Project created successfully! โจ\n');
|
|
182
|
+
console.log(`To get started:`);
|
|
183
|
+
console.log(` cd ${projectDirectory}`);
|
|
184
|
+
console.log(' Happy coding! ๐');
|
|
185
|
+
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('โ An error occurred:');
|
|
188
|
+
if (error instanceof Error) {
|
|
189
|
+
console.error(error.message);
|
|
190
|
+
} else if (error && typeof error === 'object' && 'stderr' in error) {
|
|
191
|
+
console.error((error as { stderr?: string }).stderr || String(error));
|
|
192
|
+
} else {
|
|
193
|
+
console.error(String(error));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Clean up the created directory if an error occurred
|
|
197
|
+
if (await fs.pathExists(projectPath)) {
|
|
198
|
+
await fs.remove(projectPath);
|
|
199
|
+
console.log('๐งน Cleaned up failed project directory.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** Command to initialize a project and optionally create a GitHub repository */
|
|
205
|
+
program
|
|
206
|
+
.command('init')
|
|
207
|
+
.description('Initialize the current directory (or path) as a git repo and optionally create a GitHub repository')
|
|
208
|
+
.argument('[path]', 'Path to the project directory (defaults to current directory)')
|
|
209
|
+
.action(async (dirPath) => {
|
|
210
|
+
const cwd = dirPath ? path.resolve(dirPath) : process.cwd();
|
|
211
|
+
const gitDir = path.join(cwd, '.git');
|
|
212
|
+
if (!(await fs.pathExists(gitDir))) {
|
|
213
|
+
await execa('git', ['init'], { stdio: 'inherit', cwd });
|
|
214
|
+
console.log('โ
Git repository initialized.\n');
|
|
215
|
+
}
|
|
216
|
+
await offerAndCreateGitHubRepo(cwd);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/** Command to list all available templates */
|
|
220
|
+
program
|
|
221
|
+
.command('list')
|
|
222
|
+
.description('List all available templates')
|
|
223
|
+
.option('--verbose', 'List all available templates with verbose information')
|
|
224
|
+
.option('-t, --template-file <path>', 'Include templates from a JSON file (same format as built-in)')
|
|
225
|
+
.action((options) => {
|
|
226
|
+
const templatesToUse = mergeTemplates(defaultTemplates, options?.templateFile);
|
|
227
|
+
console.log('\n๐ Available templates:\n');
|
|
228
|
+
templatesToUse.forEach(template => {
|
|
229
|
+
console.log(` ${template.name.padEnd(20)} - ${template.description}`)
|
|
230
|
+
if (options.verbose) {
|
|
231
|
+
console.log(` Repo URL: ${template.repo}`);
|
|
232
|
+
if (template.options && Array.isArray(template.options)) {
|
|
233
|
+
console.log(` Checkout Options: ${template.options.join(', ')}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
console.log('');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/** Command to run PatternFly codemods on a directory */
|
|
241
|
+
program
|
|
242
|
+
.command('update')
|
|
243
|
+
.description('Run PatternFly codemods on a directory to transform code to the latest PatternFly patterns')
|
|
244
|
+
.argument('[path]', 'The path to the source directory to run codemods on (defaults to "src")')
|
|
245
|
+
.option('--fix', 'Automatically apply fixes to files instead of just showing what would be changed')
|
|
246
|
+
.action(async (srcPath, options) => {
|
|
247
|
+
const targetPath = srcPath || 'src';
|
|
248
|
+
const resolvedPath = path.resolve(targetPath);
|
|
249
|
+
const commands = ['@patternfly/pf-codemods', '@patternfly/class-name-updater'];
|
|
250
|
+
|
|
251
|
+
console.log(`Running PatternFly updates on ${resolvedPath}...`);
|
|
252
|
+
|
|
253
|
+
for (const command of commands) {
|
|
254
|
+
try {
|
|
255
|
+
console.log(`\n๐ฆ Running ${command}...`);
|
|
256
|
+
const args = [command];
|
|
257
|
+
if (options.fix) {
|
|
258
|
+
args.push('--fix');
|
|
259
|
+
}
|
|
260
|
+
args.push(resolvedPath);
|
|
261
|
+
await execa('npx', args, { stdio: 'inherit' });
|
|
262
|
+
console.log(`โ
${command} completed successfully.`);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`โ An error occurred while running ${command}:`);
|
|
265
|
+
if (error instanceof Error) {
|
|
266
|
+
console.error(error.message);
|
|
267
|
+
} else if (error && typeof error === 'object' && 'stderr' in error) {
|
|
268
|
+
console.error((error as { stderr?: string }).stderr || String(error));
|
|
269
|
+
} else {
|
|
270
|
+
console.error(String(error));
|
|
271
|
+
}
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('\nโจ All updates completed successfully! โจ');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/** Command to save changes: check for changes, prompt to commit, and push */
|
|
280
|
+
program
|
|
281
|
+
.command('save')
|
|
282
|
+
.description('Check for changes, optionally commit them with a message, and push to the current branch')
|
|
283
|
+
.argument('[path]', 'Path to the repository (defaults to current directory)')
|
|
284
|
+
.action(async (repoPath) => {
|
|
285
|
+
const cwd = repoPath ? path.resolve(repoPath) : process.cwd();
|
|
286
|
+
try {
|
|
287
|
+
await runSave(cwd);
|
|
288
|
+
} catch {
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/** Command to load latest updates from the remote */
|
|
294
|
+
program
|
|
295
|
+
.command('load')
|
|
296
|
+
.description('Pull the latest updates from GitHub')
|
|
297
|
+
.argument('[path]', 'Path to the repository (defaults to current directory)')
|
|
298
|
+
.action(async (repoPath) => {
|
|
299
|
+
const cwd = repoPath ? path.resolve(repoPath) : process.cwd();
|
|
300
|
+
try {
|
|
301
|
+
await runLoad(cwd);
|
|
302
|
+
} catch {
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
program.parse(process.argv);
|