create-shape-app 0.1.3 → 0.1.5
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/index.d.ts +7 -0
- package/dist/index.js +98 -26
- package/dist/template/release.js +106 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageManager } from './cli/args.js';
|
|
1
2
|
import { type PostScaffoldSetupOptions, type PostScaffoldSetupResult } from './scaffold/post-setup.js';
|
|
2
3
|
import { type MaterializedTemplate } from './template/materialize.js';
|
|
3
4
|
import { type TemplateRelease } from './template/release.js';
|
|
@@ -10,6 +11,7 @@ interface CliRuntime {
|
|
|
10
11
|
print: (message: string) => void;
|
|
11
12
|
printError: (message: string) => void;
|
|
12
13
|
prompt: (message: string) => Promise<string>;
|
|
14
|
+
selectPackageManager: (options: PackageManagerSelectOptions) => Promise<PackageManager | undefined>;
|
|
13
15
|
confirm: (message: string) => Promise<boolean>;
|
|
14
16
|
resolveTemplateRelease: (templateRef?: string) => Promise<TemplateRelease>;
|
|
15
17
|
materializeTemplate: (release: TemplateRelease) => Promise<MaterializedTemplate>;
|
|
@@ -17,5 +19,10 @@ interface CliRuntime {
|
|
|
17
19
|
copyTemplateToDirectory: (templateRoot: string, targetDirectory: string) => Promise<void>;
|
|
18
20
|
runPostScaffoldSetup: (options: PostScaffoldSetupOptions) => Promise<PostScaffoldSetupResult>;
|
|
19
21
|
}
|
|
22
|
+
interface PackageManagerSelectOptions {
|
|
23
|
+
message: string;
|
|
24
|
+
choices: readonly PackageManager[];
|
|
25
|
+
defaultValue: PackageManager;
|
|
26
|
+
}
|
|
20
27
|
export declare function runCLI(argv: string[], runtimeOverrides?: Partial<CliRuntime>): Promise<number>;
|
|
21
28
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { clearScreenDown, cursorTo, emitKeypressEvents, moveCursor } from 'node:readline';
|
|
5
6
|
import { parseArgs } from './cli/args.js';
|
|
6
7
|
import { CliUsageError } from './cli/errors.js';
|
|
7
8
|
import { HELP_TEXT } from './cli/help.js';
|
|
@@ -13,9 +14,9 @@ const require = createRequire(import.meta.url);
|
|
|
13
14
|
const packageJson = require('../package.json');
|
|
14
15
|
export const CLI_VERSION = packageJson.version ?? '0.0.0';
|
|
15
16
|
const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
16
|
-
const PACKAGE_MANAGER_SET = new Set(PACKAGE_MANAGERS);
|
|
17
17
|
const DEFAULT_PROJECT_NAME_PROMPT = 'Project name: ';
|
|
18
18
|
const DEFAULT_CONFIRM_PROMPT = 'Continue? (Y/n): ';
|
|
19
|
+
const PACKAGE_MANAGER_SELECT_MESSAGE = 'Package manager:';
|
|
19
20
|
export async function runCLI(argv, runtimeOverrides = {}) {
|
|
20
21
|
const runtime = createRuntime(runtimeOverrides);
|
|
21
22
|
try {
|
|
@@ -110,6 +111,7 @@ function createRuntime(overrides) {
|
|
|
110
111
|
print: console.log,
|
|
111
112
|
printError: console.error,
|
|
112
113
|
prompt: defaultPrompt,
|
|
114
|
+
selectPackageManager: defaultSelectPackageManager,
|
|
113
115
|
confirm: defaultConfirm,
|
|
114
116
|
resolveTemplateRelease: (templateRef) => fetchTemplateRelease({
|
|
115
117
|
templateRef,
|
|
@@ -141,35 +143,17 @@ async function resolvePackageManager(packageManager, skipPrompts, runtime) {
|
|
|
141
143
|
if (!isInteractive(runtime) || skipPrompts) {
|
|
142
144
|
return detectedPackageManager;
|
|
143
145
|
}
|
|
144
|
-
runtime.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
while (true) {
|
|
150
|
-
const answer = (await runtime.prompt(`Select package manager (1-${PACKAGE_MANAGERS.length}) [${detectedPackageManager}]: `))
|
|
151
|
-
.trim()
|
|
152
|
-
.toLowerCase();
|
|
153
|
-
if (!answer) {
|
|
154
|
-
return detectedPackageManager;
|
|
155
|
-
}
|
|
156
|
-
const selectedByIndex = Number(answer);
|
|
157
|
-
if (Number.isInteger(selectedByIndex) &&
|
|
158
|
-
selectedByIndex >= 1 &&
|
|
159
|
-
selectedByIndex <= PACKAGE_MANAGERS.length) {
|
|
160
|
-
return PACKAGE_MANAGERS[selectedByIndex - 1];
|
|
161
|
-
}
|
|
162
|
-
if (PACKAGE_MANAGER_SET.has(answer)) {
|
|
163
|
-
return answer;
|
|
164
|
-
}
|
|
165
|
-
runtime.printError(`Invalid package manager: ${answer}. Enter 1-${PACKAGE_MANAGERS.length} or one of ${PACKAGE_MANAGERS.join(', ')}.`);
|
|
166
|
-
}
|
|
146
|
+
return ((await runtime.selectPackageManager({
|
|
147
|
+
message: PACKAGE_MANAGER_SELECT_MESSAGE,
|
|
148
|
+
choices: PACKAGE_MANAGERS,
|
|
149
|
+
defaultValue: detectedPackageManager,
|
|
150
|
+
})) ?? detectedPackageManager);
|
|
167
151
|
}
|
|
168
152
|
function isInteractive(runtime) {
|
|
169
153
|
return runtime.stdinIsTTY && runtime.stdoutIsTTY;
|
|
170
154
|
}
|
|
171
155
|
async function defaultPrompt(message) {
|
|
172
|
-
const rl =
|
|
156
|
+
const rl = createInterface({
|
|
173
157
|
input: process.stdin,
|
|
174
158
|
output: process.stdout,
|
|
175
159
|
});
|
|
@@ -185,6 +169,94 @@ async function defaultConfirm(message) {
|
|
|
185
169
|
const normalized = answer.trim().toLowerCase();
|
|
186
170
|
return normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
187
171
|
}
|
|
172
|
+
async function defaultSelectPackageManager(options) {
|
|
173
|
+
const { message, choices, defaultValue } = options;
|
|
174
|
+
if (choices.length === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const defaultIndex = choices.indexOf(defaultValue);
|
|
178
|
+
let selectedIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
179
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
180
|
+
return choices[selectedIndex];
|
|
181
|
+
}
|
|
182
|
+
const stdin = process.stdin;
|
|
183
|
+
const stdout = process.stdout;
|
|
184
|
+
const previousRawMode = Boolean(stdin.isRaw);
|
|
185
|
+
let renderedLines = 0;
|
|
186
|
+
const render = () => {
|
|
187
|
+
const lines = [
|
|
188
|
+
message,
|
|
189
|
+
...choices.map((candidate, index) => {
|
|
190
|
+
const indicator = index === selectedIndex ? '>' : ' ';
|
|
191
|
+
const defaultLabel = candidate === defaultValue ? ' (default)' : '';
|
|
192
|
+
return ` ${indicator} ${candidate}${defaultLabel}`;
|
|
193
|
+
}),
|
|
194
|
+
' Use Up/Down arrows and Enter to confirm.',
|
|
195
|
+
];
|
|
196
|
+
if (renderedLines > 0) {
|
|
197
|
+
moveCursor(stdout, 0, -renderedLines);
|
|
198
|
+
cursorTo(stdout, 0);
|
|
199
|
+
clearScreenDown(stdout);
|
|
200
|
+
}
|
|
201
|
+
stdout.write(lines.join('\n'));
|
|
202
|
+
stdout.write('\n');
|
|
203
|
+
renderedLines = lines.length;
|
|
204
|
+
};
|
|
205
|
+
emitKeypressEvents(stdin);
|
|
206
|
+
if (typeof stdin.setRawMode === 'function') {
|
|
207
|
+
stdin.setRawMode(true);
|
|
208
|
+
}
|
|
209
|
+
stdin.resume();
|
|
210
|
+
stdout.write('\x1b[?25l');
|
|
211
|
+
render();
|
|
212
|
+
try {
|
|
213
|
+
const selected = await new Promise((resolve, reject) => {
|
|
214
|
+
const onKeypress = (_value, key) => {
|
|
215
|
+
if (!key) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (key.ctrl && key.name === 'c') {
|
|
219
|
+
stdin.off('keypress', onKeypress);
|
|
220
|
+
reject(new Error('Aborted.'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
224
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
225
|
+
render();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
229
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
230
|
+
render();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
234
|
+
stdin.off('keypress', onKeypress);
|
|
235
|
+
resolve(choices[selectedIndex]);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (key.name === 'escape') {
|
|
239
|
+
stdin.off('keypress', onKeypress);
|
|
240
|
+
resolve(defaultValue);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
stdin.on('keypress', onKeypress);
|
|
244
|
+
});
|
|
245
|
+
if (renderedLines > 0) {
|
|
246
|
+
moveCursor(stdout, 0, -renderedLines);
|
|
247
|
+
cursorTo(stdout, 0);
|
|
248
|
+
clearScreenDown(stdout);
|
|
249
|
+
}
|
|
250
|
+
stdout.write(`Package manager: ${selected}\n`);
|
|
251
|
+
return selected;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
stdout.write('\x1b[?25h');
|
|
255
|
+
if (typeof stdin.setRawMode === 'function') {
|
|
256
|
+
stdin.setRawMode(previousRawMode);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
188
260
|
function assertValidProjectName(projectName) {
|
|
189
261
|
if (projectName === '.' || projectName === '..') {
|
|
190
262
|
throw new CliUsageError('Invalid project name: "." and ".." are not allowed.');
|
package/dist/template/release.js
CHANGED
|
@@ -23,10 +23,11 @@ export async function fetchTemplateRelease(options = {}) {
|
|
|
23
23
|
const endpoint = templateRef
|
|
24
24
|
? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(templateRef)}`
|
|
25
25
|
: `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
|
26
|
+
const headers = buildHeaders(githubToken);
|
|
26
27
|
let response;
|
|
27
28
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
28
29
|
response = await fetchImpl(endpoint, {
|
|
29
|
-
headers
|
|
30
|
+
headers,
|
|
30
31
|
});
|
|
31
32
|
if (response.ok) {
|
|
32
33
|
break;
|
|
@@ -40,6 +41,49 @@ export async function fetchTemplateRelease(options = {}) {
|
|
|
40
41
|
throw new Error('Failed to resolve template release: no response received.');
|
|
41
42
|
}
|
|
42
43
|
if (!response.ok) {
|
|
44
|
+
if (response.status === 404) {
|
|
45
|
+
if (templateRef) {
|
|
46
|
+
const resolvedTag = await resolveTemplateRefTag({
|
|
47
|
+
owner,
|
|
48
|
+
repo,
|
|
49
|
+
templateRef,
|
|
50
|
+
githubToken,
|
|
51
|
+
fetchImpl,
|
|
52
|
+
});
|
|
53
|
+
if (resolvedTag) {
|
|
54
|
+
return {
|
|
55
|
+
tag: resolvedTag,
|
|
56
|
+
tarballUrl: buildTagTarballUrl(owner, repo, resolvedTag),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const latestTag = await resolveLatestSupportedTag({
|
|
62
|
+
owner,
|
|
63
|
+
repo,
|
|
64
|
+
githubToken,
|
|
65
|
+
fetchImpl,
|
|
66
|
+
});
|
|
67
|
+
if (latestTag) {
|
|
68
|
+
return {
|
|
69
|
+
tag: latestTag,
|
|
70
|
+
tarballUrl: buildTagTarballUrl(owner, repo, latestTag),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const defaultBranch = await resolveDefaultBranch({
|
|
74
|
+
owner,
|
|
75
|
+
repo,
|
|
76
|
+
githubToken,
|
|
77
|
+
fetchImpl,
|
|
78
|
+
});
|
|
79
|
+
if (defaultBranch) {
|
|
80
|
+
return {
|
|
81
|
+
tag: defaultBranch,
|
|
82
|
+
tarballUrl: buildTagTarballUrl(owner, repo, defaultBranch),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
43
87
|
throw new Error(await buildReleaseLookupError(response, templateRef));
|
|
44
88
|
}
|
|
45
89
|
const payload = (await response.json());
|
|
@@ -54,6 +98,9 @@ export async function fetchTemplateRelease(options = {}) {
|
|
|
54
98
|
tarballUrl,
|
|
55
99
|
};
|
|
56
100
|
}
|
|
101
|
+
function buildTagTarballUrl(owner, repo, tag) {
|
|
102
|
+
return `https://api.github.com/repos/${owner}/${repo}/tarball/${encodeURIComponent(tag)}`;
|
|
103
|
+
}
|
|
57
104
|
function buildHeaders(githubToken) {
|
|
58
105
|
const headers = {
|
|
59
106
|
Accept: 'application/vnd.github+json',
|
|
@@ -84,6 +131,64 @@ async function buildReleaseLookupError(response, templateRef) {
|
|
|
84
131
|
}
|
|
85
132
|
return `Failed to resolve ${refLabel}: HTTP ${response.status}.`;
|
|
86
133
|
}
|
|
134
|
+
async function resolveLatestSupportedTag(options) {
|
|
135
|
+
const tags = await fetchTagNames(options);
|
|
136
|
+
return tags.find((tag) => isSupportedTag(tag));
|
|
137
|
+
}
|
|
138
|
+
async function resolveTemplateRefTag(options) {
|
|
139
|
+
const tags = await fetchTagNames(options);
|
|
140
|
+
const exactMatch = tags.find((tag) => tag === options.templateRef && isSupportedTag(tag));
|
|
141
|
+
if (exactMatch) {
|
|
142
|
+
return exactMatch;
|
|
143
|
+
}
|
|
144
|
+
const normalizedTemplateRef = normalizeTag(options.templateRef);
|
|
145
|
+
return tags.find((tag) => normalizeTag(tag) === normalizedTemplateRef && isSupportedTag(tag));
|
|
146
|
+
}
|
|
147
|
+
async function fetchTagNames(options) {
|
|
148
|
+
const response = await options.fetchImpl(`https://api.github.com/repos/${options.owner}/${options.repo}/tags?per_page=100`, {
|
|
149
|
+
headers: buildHeaders(options.githubToken),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const payload = (await response.json());
|
|
155
|
+
if (!Array.isArray(payload)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
return payload.flatMap((item) => {
|
|
159
|
+
if (!item || typeof item !== 'object') {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const name = item.name;
|
|
163
|
+
return typeof name === 'string' && name.trim() ? [name] : [];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function resolveDefaultBranch(options) {
|
|
167
|
+
const response = await options.fetchImpl(`https://api.github.com/repos/${options.owner}/${options.repo}`, {
|
|
168
|
+
headers: buildHeaders(options.githubToken),
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const payload = (await response.json());
|
|
174
|
+
const defaultBranch = payload.default_branch;
|
|
175
|
+
if (typeof defaultBranch !== 'string' || !defaultBranch.trim()) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return defaultBranch;
|
|
179
|
+
}
|
|
180
|
+
function isSupportedTag(tag) {
|
|
181
|
+
try {
|
|
182
|
+
assertTagIsSupported(tag);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function normalizeTag(tag) {
|
|
190
|
+
return tag.startsWith('v') ? tag.slice(1) : tag;
|
|
191
|
+
}
|
|
87
192
|
async function readApiMessage(response) {
|
|
88
193
|
try {
|
|
89
194
|
const payload = (await response.clone().json());
|