ecopages 0.2.0-rc.7 → 0.2.0-rc.8

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.
@@ -0,0 +1,56 @@
1
+ import { repositoryName, withSubpath } from './git-template-source-shared.js';
2
+
3
+ export function normalizeGithubParts(parts, source) {
4
+ if (parts.length < 2) {
5
+ return null;
6
+ }
7
+
8
+ const [owner, repositoryPart, marker, ref, ...subpath] = parts;
9
+ const repository = `${owner}/${repositoryName(repositoryPart)}`;
10
+ if (marker === 'tree' || marker === 'blob') {
11
+ if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
12
+ return `github:${withSubpath(repository, subpath)}#${ref}`;
13
+ }
14
+ return `github:${repository}`;
15
+ }
16
+
17
+ export function normalizeGitlabParts(parts) {
18
+ const markerIndex = parts.indexOf('-');
19
+ const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
20
+ if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
21
+ const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
22
+ const ref = parts[treeIndex + 1];
23
+ const subpath = parts.slice(treeIndex + 2);
24
+ return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
25
+ }
26
+ if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
27
+ return null;
28
+ }
29
+
30
+ export function normalizeBitbucketParts(parts) {
31
+ if (parts.length < 2) {
32
+ return null;
33
+ }
34
+
35
+ const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
36
+ const repository = `${workspace}/${repositoryName(repositoryPart)}`;
37
+ if (marker === 'src' && ref) {
38
+ return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
39
+ }
40
+ return `bitbucket:${repository}`;
41
+ }
42
+
43
+ export function normalizeSourcehutParts(parts) {
44
+ if (parts.length < 2) {
45
+ return null;
46
+ }
47
+
48
+ const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
49
+ if (treeIndex > 1 && parts[treeIndex + 1]) {
50
+ const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
51
+ const ref = parts[treeIndex + 1];
52
+ const subpath = parts.slice(treeIndex + 2);
53
+ return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
54
+ }
55
+ return `sourcehut:${parts.join('/')}`;
56
+ }
@@ -0,0 +1,7 @@
1
+ export function repositoryName(value) {
2
+ return value.replace(/\.git$/u, '');
3
+ }
4
+
5
+ export function withSubpath(repository, subpath) {
6
+ return subpath.length > 0 ? `${repository}/${subpath.join('/')}` : repository;
7
+ }
@@ -1,3 +1,10 @@
1
+ import {
2
+ normalizeBitbucketParts,
3
+ normalizeGithubParts,
4
+ normalizeGitlabParts,
5
+ normalizeSourcehutParts,
6
+ } from './git-template-source-providers.js';
7
+
1
8
  const GIT_PROVIDER_PREFIX = /^(github|gitlab|bitbucket|sourcehut):/u;
2
9
 
3
10
  const HOST_PROVIDER = {
@@ -7,12 +14,21 @@ const HOST_PROVIDER = {
7
14
  'git.sr.ht': 'sourcehut',
8
15
  };
9
16
 
10
- function repositoryName(value) {
11
- return value.replace(/\.git$/u, '');
12
- }
17
+ const PROVIDER_NORMALIZERS = {
18
+ github: normalizeGithubParts,
19
+ gitlab: normalizeGitlabParts,
20
+ bitbucket: normalizeBitbucketParts,
21
+ sourcehut: normalizeSourcehutParts,
22
+ };
23
+
24
+ function normalizeProviderUrl(hostProvider, parts, source) {
25
+ const normalize = PROVIDER_NORMALIZERS[hostProvider];
26
+ const normalized = normalize?.(parts, source);
27
+ if (normalized) {
28
+ return normalized;
29
+ }
13
30
 
14
- function withSubpath(repository, subpath) {
15
- return subpath.length > 0 ? `${repository}/${subpath.join('/')}` : repository;
31
+ throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
16
32
  }
17
33
 
18
34
  /**
@@ -42,47 +58,5 @@ export function normalizeGitSource(source) {
42
58
  .split('/')
43
59
  .filter(Boolean);
44
60
 
45
- if (hostProvider === 'github' && parts.length >= 2) {
46
- const [owner, repositoryPart, marker, ref, ...subpath] = parts;
47
- const repository = `${owner}/${repositoryName(repositoryPart)}`;
48
- if (marker === 'tree' || marker === 'blob') {
49
- if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
50
- return `github:${withSubpath(repository, subpath)}#${ref}`;
51
- }
52
- return `github:${repository}`;
53
- }
54
-
55
- if (hostProvider === 'gitlab') {
56
- const markerIndex = parts.indexOf('-');
57
- const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
58
- if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
59
- const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
60
- const ref = parts[treeIndex + 1];
61
- const subpath = parts.slice(treeIndex + 2);
62
- return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
63
- }
64
- if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
65
- }
66
-
67
- if (hostProvider === 'bitbucket' && parts.length >= 2) {
68
- const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
69
- const repository = `${workspace}/${repositoryName(repositoryPart)}`;
70
- if (marker === 'src' && ref) {
71
- return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
72
- }
73
- return `bitbucket:${repository}`;
74
- }
75
-
76
- if (hostProvider === 'sourcehut' && parts.length >= 2) {
77
- const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
78
- if (treeIndex > 1 && parts[treeIndex + 1]) {
79
- const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
80
- const ref = parts[treeIndex + 1];
81
- const subpath = parts.slice(treeIndex + 2);
82
- return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
83
- }
84
- return `sourcehut:${parts.join('/')}`;
85
- }
86
-
87
- throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
61
+ return normalizeProviderUrl(hostProvider, parts, source);
88
62
  }
package/bin/init.js CHANGED
@@ -220,52 +220,114 @@ function isCancelled(value) {
220
220
  return false;
221
221
  }
222
222
 
223
+ async function promptProjectDirectory() {
224
+ const answer = await prompts.text({
225
+ message: 'Project directory:',
226
+ placeholder: 'my-app',
227
+ validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
228
+ });
229
+ if (isCancelled(answer)) return null;
230
+ return answer.trim();
231
+ }
232
+
233
+ async function promptTemplateSelection() {
234
+ const answer = await prompts.select({
235
+ message: 'Select a template:',
236
+ options: [
237
+ ...officialTemplates.map((template) => ({
238
+ label: template.displayName,
239
+ value: template.id,
240
+ hint: template.description,
241
+ })),
242
+ {
243
+ label: 'Community template',
244
+ value: '__community__',
245
+ hint: 'Use a Git repository or provider source.',
246
+ },
247
+ ],
248
+ });
249
+ if (isCancelled(answer)) return null;
250
+ return answer;
251
+ }
252
+
253
+ async function promptCommunityTemplateSource() {
254
+ const sourceAnswer = await prompts.text({
255
+ message: 'Template source:',
256
+ placeholder: 'github:owner/repository#v1.0.0',
257
+ validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
258
+ });
259
+ if (isCancelled(sourceAnswer)) return null;
260
+ return sourceAnswer.trim();
261
+ }
262
+
263
+ async function resolveInteractiveTemplateChoice(templateId, source) {
264
+ if (templateId || source) {
265
+ return { templateId, source };
266
+ }
267
+
268
+ const answer = await promptTemplateSelection();
269
+ if (answer === null) {
270
+ return null;
271
+ }
272
+ if (answer === '__community__') {
273
+ const communitySource = await promptCommunityTemplateSource();
274
+ if (communitySource === null) {
275
+ return null;
276
+ }
277
+ return { templateId, source: communitySource };
278
+ }
279
+ return { templateId: answer, source };
280
+ }
281
+
282
+ async function promptInstallChoice(packageManager, parsed) {
283
+ if (parsed.start) {
284
+ return true;
285
+ }
286
+ if (parsed.install !== undefined) {
287
+ return parsed.install;
288
+ }
289
+ if (!parsed.interactive) {
290
+ return undefined;
291
+ }
292
+
293
+ const answer = await prompts.confirm({
294
+ message: `Install dependencies with ${packageManager}?`,
295
+ initialValue: true,
296
+ });
297
+ if (isCancelled(answer)) return null;
298
+ return answer;
299
+ }
300
+
301
+ async function promptStartChoice(parsed, install) {
302
+ if (parsed.start !== undefined) {
303
+ return parsed.start;
304
+ }
305
+ if (!parsed.interactive || !install) {
306
+ return undefined;
307
+ }
308
+
309
+ const answer = await prompts.confirm({ message: 'Start the development server now?', initialValue: false });
310
+ if (isCancelled(answer)) return null;
311
+ return answer;
312
+ }
313
+
223
314
  async function collectInitAnswers(parsed) {
224
315
  const packageManager = detectPackageManager();
225
316
  let dir = parsed.dir;
226
317
  let templateId = parsed.template;
227
318
  let source = parsed.from;
228
319
 
229
- if (parsed.interactive) {
230
- if (!dir) {
231
- const answer = await prompts.text({
232
- message: 'Project directory:',
233
- placeholder: 'my-app',
234
- validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
235
- });
236
- if (isCancelled(answer)) return null;
237
- dir = answer.trim();
238
- }
320
+ if (parsed.interactive && !dir) {
321
+ const promptedDir = await promptProjectDirectory();
322
+ if (promptedDir === null) return null;
323
+ dir = promptedDir;
324
+ }
239
325
 
240
- if (!templateId && !source) {
241
- const answer = await prompts.select({
242
- message: 'Select a template:',
243
- options: [
244
- ...officialTemplates.map((template) => ({
245
- label: template.displayName,
246
- value: template.id,
247
- hint: template.description,
248
- })),
249
- {
250
- label: 'Community template',
251
- value: '__community__',
252
- hint: 'Use a Git repository or provider source.',
253
- },
254
- ],
255
- });
256
- if (isCancelled(answer)) return null;
257
- if (answer === '__community__') {
258
- const sourceAnswer = await prompts.text({
259
- message: 'Template source:',
260
- placeholder: 'github:owner/repository#v1.0.0',
261
- validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
262
- });
263
- if (isCancelled(sourceAnswer)) return null;
264
- source = sourceAnswer.trim();
265
- } else {
266
- templateId = answer;
267
- }
268
- }
326
+ if (parsed.interactive) {
327
+ const templateChoice = await resolveInteractiveTemplateChoice(templateId, source);
328
+ if (templateChoice === null) return null;
329
+ templateId = templateChoice.templateId;
330
+ source = templateChoice.source;
269
331
  }
270
332
 
271
333
  if (!templateId && !source) templateId = defaultOfficialTemplate.id;
@@ -273,24 +335,13 @@ async function collectInitAnswers(parsed) {
273
335
  prompts.log.step(`Resolved template source: ${getTemplateSource(templateId, source)}`);
274
336
  }
275
337
 
276
- let install = parsed.start ? true : parsed.install;
277
- if (install === undefined && parsed.interactive) {
278
- const answer = await prompts.confirm({
279
- message: `Install dependencies with ${packageManager}?`,
280
- initialValue: true,
281
- });
282
- if (isCancelled(answer)) return null;
283
- install = answer;
284
- }
285
- install ??= false;
338
+ const installAnswer = await promptInstallChoice(packageManager, parsed);
339
+ if (installAnswer === null) return null;
340
+ const install = installAnswer ?? false;
286
341
 
287
- let start = parsed.start;
288
- if (start === undefined && parsed.interactive && install) {
289
- const answer = await prompts.confirm({ message: 'Start the development server now?', initialValue: false });
290
- if (isCancelled(answer)) return null;
291
- start = answer;
292
- }
293
- start ??= false;
342
+ const startAnswer = await promptStartChoice(parsed, install);
343
+ if (startAnswer === null) return null;
344
+ const start = startAnswer ?? false;
294
345
 
295
346
  return { dir, templateId, source, packageManager, install, start };
296
347
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecopages",
3
- "version": "0.2.0-rc.7",
3
+ "version": "0.2.0-rc.8",
4
4
  "description": "CLI utilities for Ecopages",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^1.7.0",
39
- "@ecopages/core": "0.2.0-rc.7",
39
+ "@ecopages/core": "0.2.0-rc.8",
40
40
  "@ecopages/logger": "^0.2.3",
41
41
  "giget": "^2.0.0",
42
42
  "tsx": "^4.22.3"