Skip to content

Commit 441ab36

Browse files
committed
feat: fixed some issues with save and load.
1 parent 8cb78cc commit 441ab36

File tree

8 files changed

+246
-95
lines changed

8 files changed

+246
-95
lines changed

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@
3636
]
3737
},
3838
"moduleNameMapper": {
39-
"^(\\.\\./.*)\\.js$": "$1"
40-
}
39+
"^(\\.\\./.*)\\.js$": "$1",
40+
"^(\\./.*)\\.js$": "$1"
41+
},
42+
"transformIgnorePatterns": [
43+
"/node_modules/(?!inquirer)"
44+
]
4145
},
4246
"dependencies": {
4347
"0g": "^0.4.2",

src/__tests__/cli.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
jest.mock('inquirer', () => ({
2+
__esModule: true,
3+
default: { prompt: jest.fn() },
4+
}));
5+
16
import path from 'path';
27
import fs from 'fs-extra';
38
import { loadCustomTemplates, mergeTemplates } from '../template-loader.js';

src/__tests__/github.test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ jest.mock('execa', () => ({
33
execa: jest.fn(),
44
}));
55

6+
jest.mock('inquirer', () => ({
7+
__esModule: true,
8+
default: { prompt: jest.fn() },
9+
}));
10+
611
import {
712
sanitizeRepoName,
813
checkGhAuth,
@@ -141,28 +146,36 @@ describe('createRepo', () => {
141146
});
142147

143148
it('initializes git and calls gh repo create with expected args and returns repo URL', async () => {
149+
mockExeca
150+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
151+
.mockRejectedValueOnce(new Error('no HEAD'))
152+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
153+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
154+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
155+
144156
const url = await createRepo({
145157
repoName: 'my-app',
146158
projectPath,
147159
username,
148160
});
149161

150162
expect(url).toBe('https://github.com/octocat/my-app.git');
151-
expect(mockExeca).toHaveBeenCalledTimes(4);
163+
expect(mockExeca).toHaveBeenCalledTimes(5);
152164
expect(mockExeca).toHaveBeenNthCalledWith(1, 'git', ['init'], {
153165
stdio: 'inherit',
154166
cwd: projectPath,
155167
});
156-
expect(mockExeca).toHaveBeenNthCalledWith(2, 'git', ['add', '.'], {
168+
expect(mockExeca).toHaveBeenNthCalledWith(2, 'git', ['rev-parse', '--verify', 'HEAD'], expect.any(Object));
169+
expect(mockExeca).toHaveBeenNthCalledWith(3, 'git', ['add', '.'], {
157170
stdio: 'inherit',
158171
cwd: projectPath,
159172
});
160-
expect(mockExeca).toHaveBeenNthCalledWith(3, 'git', ['commit', '-m', 'Initial commit'], {
173+
expect(mockExeca).toHaveBeenNthCalledWith(4, 'git', ['commit', '-m', 'Initial commit'], {
161174
stdio: 'inherit',
162175
cwd: projectPath,
163176
});
164177
expect(mockExeca).toHaveBeenNthCalledWith(
165-
4,
178+
5,
166179
'gh',
167180
[
168181
'repo',
@@ -178,6 +191,13 @@ describe('createRepo', () => {
178191
});
179192

180193
it('passes description when provided', async () => {
194+
mockExeca
195+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
196+
.mockRejectedValueOnce(new Error('no HEAD'))
197+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
198+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
199+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
200+
181201
await createRepo({
182202
repoName: 'my-app',
183203
projectPath,
@@ -186,7 +206,7 @@ describe('createRepo', () => {
186206
});
187207

188208
expect(mockExeca).toHaveBeenNthCalledWith(
189-
4,
209+
5,
190210
'gh',
191211
expect.arrayContaining(['--description=My cool project']),
192212
expect.any(Object),

src/__tests__/save.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ jest.mock('inquirer', () => ({
1717
},
1818
}));
1919

20+
jest.mock('../github.js', () => ({
21+
offerAndCreateGitHubRepo: jest.fn(),
22+
}));
23+
2024
import fs from 'fs-extra';
2125
import { execa } from 'execa';
2226
import inquirer from 'inquirer';
27+
import { offerAndCreateGitHubRepo } from '../github.js';
2328
import { runSave } from '../save.js';
2429

2530
const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists>;
2631
const mockExeca = execa as jest.MockedFunction<typeof execa>;
2732
const mockPrompt = inquirer.prompt as jest.MockedFunction<typeof inquirer.prompt>;
33+
const mockOfferAndCreateGitHubRepo = offerAndCreateGitHubRepo as jest.MockedFunction<typeof offerAndCreateGitHubRepo>;
2834

2935
const cwd = '/tmp/my-repo';
3036

@@ -125,14 +131,15 @@ describe('runSave', () => {
125131
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
126132
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
127133
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
134+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
128135
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
129136
mockPrompt
130137
.mockResolvedValueOnce({ saveChanges: true })
131138
.mockResolvedValueOnce({ message: 'Fix bug in save command' });
132139

133140
await runSave(cwd);
134141

135-
expect(mockExeca).toHaveBeenCalledTimes(4);
142+
expect(mockExeca).toHaveBeenCalledTimes(5);
136143
expect(mockExeca).toHaveBeenNthCalledWith(1, 'git', ['status', '--porcelain'], {
137144
cwd,
138145
encoding: 'utf8',
@@ -146,7 +153,8 @@ describe('runSave', () => {
146153
'-m',
147154
'Fix bug in save command',
148155
], { cwd, stdio: 'inherit' });
149-
expect(mockExeca).toHaveBeenNthCalledWith(4, 'git', ['push'], {
156+
expect(mockExeca).toHaveBeenNthCalledWith(4, 'git', ['remote', 'get-url', 'origin'], expect.any(Object));
157+
expect(mockExeca).toHaveBeenNthCalledWith(5, 'git', ['push'], {
150158
cwd,
151159
stdio: 'inherit',
152160
});
@@ -161,6 +169,7 @@ describe('runSave', () => {
161169
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
162170
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
163171
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
172+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
164173
.mockRejectedValueOnce(Object.assign(new Error('push failed'), { exitCode: 128 }));
165174

166175
mockPrompt
@@ -200,6 +209,7 @@ describe('runSave', () => {
200209
.mockResolvedValueOnce({ stdout: ' M file.ts', stderr: '', exitCode: 0 })
201210
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
202211
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 })
212+
.mockResolvedValueOnce({ stdout: 'https://github.com/user/repo.git', stderr: '', exitCode: 0 })
203213
.mockRejectedValueOnce(new Error('network error'));
204214

205215
mockPrompt
@@ -215,4 +225,48 @@ describe('runSave', () => {
215225
expect.stringContaining('network error'),
216226
);
217227
});
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+
});
218272
});

src/cli.ts

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import fs from 'fs-extra';
77
import path from 'path';
88
import { defaultTemplates } from './templates.js';
99
import { mergeTemplates } from './template-loader.js';
10-
import { checkGhAuth, createRepo, repoExists as ghRepoExists, sanitizeRepoName } from './github.js';
10+
import { offerAndCreateGitHubRepo } from './github.js';
1111
import { runSave } from './save.js';
1212
import { runLoad } from './load.js';
1313

@@ -175,84 +175,7 @@ program
175175
console.log('✅ Dependencies installed.');
176176

177177
// Optional: Create GitHub repository
178-
const { createGitHub } = await inquirer.prompt([
179-
{
180-
type: 'confirm',
181-
name: 'createGitHub',
182-
message: 'Would you like to create a GitHub repository for this project?',
183-
default: false,
184-
},
185-
]);
186-
187-
if (createGitHub) {
188-
const auth = await checkGhAuth();
189-
if (!auth.ok) {
190-
console.log(`\n⚠️ ${auth.message}`);
191-
console.log(' Skipping GitHub repository creation.\n');
192-
} else {
193-
const pkgJsonForGh = await fs.readJson(pkgJsonPath);
194-
const projectName = pkgJsonForGh.name as string;
195-
let repoName = sanitizeRepoName(projectName);
196-
197-
// If repo already exists, ask for alternative name until we get one that doesn't exist or user skips
198-
while (await ghRepoExists(auth.username, repoName)) {
199-
console.log(`\n⚠️ A repository named "${repoName}" already exists on GitHub under your account.\n`);
200-
const { alternativeName } = await inquirer.prompt([
201-
{
202-
type: 'input',
203-
name: 'alternativeName',
204-
message: 'Enter an alternative repository name (or leave empty to skip creating a GitHub repository):',
205-
default: '',
206-
},
207-
]);
208-
if (!alternativeName?.trim()) {
209-
repoName = '';
210-
break;
211-
}
212-
repoName = sanitizeRepoName(alternativeName.trim());
213-
}
214-
215-
if (repoName) {
216-
const repoUrl = `https://github.com/${auth.username}/${repoName}`;
217-
console.log('\n📋 The following will happen:\n');
218-
console.log(` • A new public repository will be created at: ${repoUrl}`);
219-
console.log(` • The repository will be created under your GitHub account (${auth.username}).`);
220-
console.log(` • The repository URL will be added to your package.json.`);
221-
console.log(` • The remote "origin" will be set to this repository (you can push when ready).\n`);
222-
223-
const { confirmCreate } = await inquirer.prompt([
224-
{
225-
type: 'confirm',
226-
name: 'confirmCreate',
227-
message: 'Do you want to proceed with creating this repository?',
228-
default: true,
229-
},
230-
]);
231-
232-
if (!confirmCreate) {
233-
console.log('\n❌ GitHub repository was not created. Your local project is ready at:');
234-
console.log(` ${projectPath}\n`);
235-
} else {
236-
try {
237-
const createdUrl = await createRepo({
238-
repoName,
239-
projectPath,
240-
username: auth.username,
241-
...(pkgJsonForGh.description && { description: String(pkgJsonForGh.description) }),
242-
});
243-
pkgJsonForGh.repository = { type: 'git', url: createdUrl };
244-
await fs.writeJson(pkgJsonPath, pkgJsonForGh, { spaces: 2 });
245-
console.log('\n✅ GitHub repository created successfully!');
246-
console.log(` ${repoUrl}`);
247-
console.log(' Repository URL has been added to your package.json.\n');
248-
} catch (err) {
249-
console.error('\n❌ Failed to create GitHub repository:');
250-
if (err instanceof Error) console.error(` ${err.message}\n`);
251-
}
252-
}
253-
}
254-
}
255-
}
178+
await offerAndCreateGitHubRepo(projectPath);
256179

257180
// Let the user know the project was created successfully
258181
console.log('\n✨ Project created successfully! ✨\n');
@@ -278,6 +201,21 @@ program
278201
}
279202
});
280203

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+
281219
/** Command to list all available templates */
282220
program
283221
.command('list')

0 commit comments

Comments
 (0)