create-shape-app 0.1.4 → 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.');
@@ -70,6 +70,18 @@ export async function fetchTemplateRelease(options = {}) {
70
70
  tarballUrl: buildTagTarballUrl(owner, repo, latestTag),
71
71
  };
72
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
+ }
73
85
  }
74
86
  }
75
87
  throw new Error(await buildReleaseLookupError(response, templateRef));
@@ -151,6 +163,20 @@ async function fetchTagNames(options) {
151
163
  return typeof name === 'string' && name.trim() ? [name] : [];
152
164
  });
153
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
+ }
154
180
  function isSupportedTag(tag) {
155
181
  try {
156
182
  assertTagIsSupported(tag);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shape-app",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold Shape Builder Kit projects from pinned release tags.",
5
5
  "license": "MIT",
6
6
  "repository": {