Skip to content

Commit cca79a5

Browse files
feat: offer to create or use a different repository in setup (#411)
## PR Checklist - [x] Addresses an existing open issue: fixes #183 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/template-typescript-node-package/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Uses a `while (true)` to continuously check if the `repository` exists. If it doesn't, the user can either: * Bail out (`"bail"`) * Create a new repository (`"create"`) * Try again with a different repository (`"different"`) Moves the `octokit` creation logic up in the file so that it can be used for repository checking. This is necessary because the `owner` might be private organization/user.
1 parent 6879381 commit cca79a5

File tree

2 files changed

+133
-62
lines changed

2 files changed

+133
-62
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ module.exports = {
112112
"simple-import-sort/imports": "error",
113113

114114
// These on-by-default rules don't work well for this repo and we like them off.
115+
"no-constant-condition": "off",
115116
"no-inner-declarations": "off",
116117

117118
// Stylistic concerns that don't interfere with Prettier

script/setup.js

Lines changed: 132 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,41 @@ let exitCode = 0;
1515
let skipRestore = true;
1616
const s = prompts.spinner();
1717

18+
function handleCancel() {
19+
prompts.cancel("Operation cancelled. Exiting setup - maybe another time? 👋");
20+
process.exit(0);
21+
}
22+
1823
function handlePromptCancel(value) {
1924
if (prompts.isCancel(value)) {
20-
prompts.cancel(
21-
"Operation cancelled. Exiting setup - maybe another time? 👋"
22-
);
23-
process.exit(0);
25+
handleCancel();
26+
}
27+
}
28+
29+
function skipSpinnerBlock(blockText) {
30+
s.start(chalk.gray("➖ " + blockText));
31+
s.stop(chalk.gray("➖ " + blockText));
32+
}
33+
34+
function successSpinnerBlock(blockText) {
35+
s.start(chalk.green("✅ " + blockText));
36+
s.stop(chalk.green("✅ " + blockText));
37+
}
38+
39+
async function withSpinner(
40+
callback,
41+
{ startText, successText, stopText, errorText }
42+
) {
43+
s.start(startText);
44+
45+
try {
46+
await callback();
47+
48+
s.stop(chalk.green("✅ " + successText));
49+
} catch (error) {
50+
s.stop(chalk.red("❌ " + stopText));
51+
52+
throw new Error(errorText, { cause: error });
2453
}
2554
}
2655

@@ -50,6 +79,33 @@ try {
5079

5180
skipRestore = values["skip-restore"];
5281

82+
const skipApi = values["skip-api"];
83+
const skipUninstalls = values["skip-uninstalls"];
84+
85+
/** @type {Octokit} */
86+
let octokit;
87+
88+
if (skipApi) {
89+
skipSpinnerBlock(`Skipping checking GitHub authentication.`);
90+
} else {
91+
successSpinnerBlock(`Checking GitHub authentication.`);
92+
93+
await withSpinner(
94+
async () => {
95+
await $`gh auth status`;
96+
const auth = (await $`gh auth token`).stdout.trim();
97+
98+
octokit = new Octokit({ auth });
99+
},
100+
{
101+
startText: `Fetching gh auth status...`,
102+
successText: `Fetched gh auth status.`,
103+
stopText: `Error fetching gh auth status.`,
104+
errorText: `Could not fetch github auth token. `,
105+
}
106+
);
107+
}
108+
53109
async function getDefaultSettings() {
54110
let gitRemoteFetch;
55111
try {
@@ -108,43 +164,91 @@ try {
108164
return value;
109165
}
110166

111-
const repository = await getPrefillOrPromptedValue(
112-
"repository",
113-
"What will the kebab-case name of the repository be?",
114-
defaultRepository
167+
const owner = await getPrefillOrPromptedValue(
168+
"owner",
169+
"What owner or user will the repository be under?",
170+
defaultOwner
171+
);
172+
173+
const repository = await ensureRepositoryExists(
174+
await getPrefillOrPromptedValue(
175+
"repository",
176+
"What will the kebab-case name of the repository be?",
177+
defaultRepository
178+
)
115179
);
116180

181+
async function ensureRepositoryExists(repository) {
182+
if (skipApi) {
183+
return repository;
184+
}
185+
186+
// We'll continuously pester the user for a repository
187+
// until they bail, create a new one, or it exists.
188+
while (true) {
189+
// Because the Octokit SDK throws on 404s (😡),
190+
// we try/catch to check whether the repo exists.
191+
try {
192+
await octokit.rest.repos.get({
193+
owner,
194+
repo: repository,
195+
});
196+
return repository;
197+
} catch (error) {
198+
if (error.status !== 404) {
199+
throw error;
200+
}
201+
}
202+
203+
const selection = await prompts.select({
204+
message: `Repository ${repository} doesn't seem to exist under ${owner}. What would you like to do?`,
205+
options: [
206+
{ label: "Bail out and maybe try again later", value: "bail" },
207+
{ label: "Create a new repository", value: "create" },
208+
{
209+
label: "Try again with a different repository",
210+
value: "different",
211+
},
212+
],
213+
});
214+
215+
handlePromptCancel(selection);
216+
217+
switch (selection) {
218+
case "bail":
219+
handleCancel();
220+
break;
221+
222+
case "create":
223+
await octokit.rest.repos.createUsingTemplate({
224+
name: repository,
225+
owner,
226+
template_owner: "JoshuaKGoldberg",
227+
template_repo: "template-typescript-node-package",
228+
});
229+
break;
230+
231+
case "different":
232+
repository = await prompts.text({
233+
message: `What would you like to call the repository?`,
234+
});
235+
break;
236+
}
237+
}
238+
}
239+
117240
const title = await getPrefillOrPromptedValue(
118241
"title",
119242
"What will the Title Case title of the repository be?",
120243
titleCase(repository).replaceAll("-", " ")
121244
);
122245

123-
const owner = await getPrefillOrPromptedValue(
124-
"owner",
125-
"What owner or user will the repository be under?",
126-
defaultOwner
127-
);
128-
129246
const description = await getPrefillOrPromptedValue(
130247
"description",
131248
"How would you describe the new package?",
132249
"A very lovely package. Hooray!"
133250
);
134251

135-
const skipApi = values["skip-api"];
136-
const skipUninstalls = values["skip-uninstalls"];
137-
138-
const successSpinnerBlock = (blockText) => {
139-
s.start(chalk.green("✅ " + blockText));
140-
s.stop(chalk.green("✅ " + blockText));
141-
};
142-
143-
const skipSpinnerBlock = (blockText) => {
144-
s.start(chalk.gray("➖ " + blockText));
145-
s.stop(chalk.gray("➖ " + blockText));
146-
};
147-
148252
successSpinnerBlock("Started hydrating package metadata locally.");
149253

150254
async function readFileAsJSON(filePath) {
@@ -158,23 +262,6 @@ try {
158262
}
159263
}
160264

161-
const withSpinner = async (
162-
callback,
163-
{ startText, successText, stopText, errorText }
164-
) => {
165-
s.start(startText);
166-
167-
try {
168-
await callback();
169-
170-
s.stop(chalk.green("✅ " + successText));
171-
} catch (error) {
172-
s.stop(chalk.red("❌ " + stopText));
173-
174-
throw new Error(errorText, { cause: error });
175-
}
176-
};
177-
178265
await withSpinner(
179266
async () => {
180267
let user;
@@ -410,28 +497,11 @@ try {
410497
}
411498
);
412499

413-
if (skipApi) {
500+
if (!octokit) {
414501
skipSpinnerBlock(`Skipping API hydration.`);
415502
} else {
416503
successSpinnerBlock(`Starting API hydration.`);
417504

418-
let octokit;
419-
420-
await withSpinner(
421-
async () => {
422-
await $`gh auth status`;
423-
const auth = (await $`gh auth token`).stdout.trim();
424-
425-
octokit = new Octokit({ auth });
426-
},
427-
{
428-
startText: `Fetching gh auth status...`,
429-
successText: `Fetched gh auth status.`,
430-
stopText: `Error fetching gh auth status.`,
431-
errorText: `Could not fetch github auth token. `,
432-
}
433-
);
434-
435505
await withSpinner(
436506
async () => {
437507
const existingLabels = JSON.parse(

0 commit comments

Comments
 (0)