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 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 readline from 'node:readline/promises';
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.print('Package manager:');
145
- for (const [index, candidate] of PACKAGE_MANAGERS.entries()) {
146
- const defaultLabel = candidate === detectedPackageManager ? ' (default)' : '';
147
- runtime.print(` ${index + 1}) ${candidate}${defaultLabel}`);
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 = readline.createInterface({
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.');
@@ -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: buildHeaders(githubToken),
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shape-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold Shape Builder Kit projects from pinned release tags.",
5
5
  "license": "MIT",
6
6
  "repository": {