@cbnventures/nova 0.12.0 → 0.13.0

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.
Files changed (183) hide show
  1. package/build/eslint.config.d.ts +32 -1
  2. package/build/eslint.config.d.ts.map +1 -1
  3. package/build/eslint.config.js +9 -1
  4. package/build/eslint.config.js.map +1 -1
  5. package/build/package.json +63 -59
  6. package/build/src/api/node-releases.d.ts +7 -0
  7. package/build/src/api/node-releases.d.ts.map +1 -0
  8. package/build/src/api/node-releases.js +67 -0
  9. package/build/src/api/node-releases.js.map +1 -0
  10. package/build/src/api/spdx-licenses.d.ts +7 -0
  11. package/build/src/api/spdx-licenses.d.ts.map +1 -0
  12. package/build/src/api/spdx-licenses.js +43 -0
  13. package/build/src/api/spdx-licenses.js.map +1 -0
  14. package/build/src/cli/index.js +60 -18
  15. package/build/src/cli/index.js.map +1 -1
  16. package/build/src/cli/recipe/pin-versions.d.ts +7 -0
  17. package/build/src/cli/recipe/pin-versions.d.ts.map +1 -0
  18. package/build/src/cli/recipe/pin-versions.js +145 -0
  19. package/build/src/cli/recipe/pin-versions.js.map +1 -0
  20. package/build/src/cli/recipe/sync-lts-engines.d.ts +6 -0
  21. package/build/src/cli/recipe/sync-lts-engines.d.ts.map +1 -0
  22. package/build/src/cli/recipe/sync-lts-engines.js +118 -0
  23. package/build/src/cli/recipe/sync-lts-engines.js.map +1 -0
  24. package/build/src/cli/recipe/sync-packages.d.ts +18 -0
  25. package/build/src/cli/recipe/sync-packages.d.ts.map +1 -0
  26. package/build/src/cli/recipe/sync-packages.js +1212 -0
  27. package/build/src/cli/recipe/sync-packages.js.map +1 -0
  28. package/build/src/cli/utility/changelog.d.ts +11 -0
  29. package/build/src/cli/utility/changelog.d.ts.map +1 -0
  30. package/build/src/cli/utility/changelog.js +670 -0
  31. package/build/src/cli/utility/changelog.js.map +1 -0
  32. package/build/src/cli/utility/initialize.d.ts +9 -5
  33. package/build/src/cli/utility/initialize.d.ts.map +1 -1
  34. package/build/src/cli/utility/initialize.js +478 -210
  35. package/build/src/cli/utility/initialize.js.map +1 -1
  36. package/build/src/cli/utility/type-check.d.ts +9 -0
  37. package/build/src/cli/utility/type-check.d.ts.map +1 -0
  38. package/build/src/cli/utility/type-check.js +59 -0
  39. package/build/src/cli/utility/type-check.js.map +1 -0
  40. package/build/src/cli/utility/version.d.ts +2 -2
  41. package/build/src/cli/utility/version.d.ts.map +1 -1
  42. package/build/src/cli/utility/version.js +107 -68
  43. package/build/src/cli/utility/version.js.map +1 -1
  44. package/build/src/lib/item.d.ts +23 -5
  45. package/build/src/lib/item.d.ts.map +1 -1
  46. package/build/src/lib/item.js +329 -4
  47. package/build/src/lib/item.js.map +1 -1
  48. package/build/src/lib/nova-config.d.ts +4 -4
  49. package/build/src/lib/nova-config.d.ts.map +1 -1
  50. package/build/src/lib/nova-config.js +76 -88
  51. package/build/src/lib/nova-config.js.map +1 -1
  52. package/build/src/lib/regex.d.ts +9 -1
  53. package/build/src/lib/regex.d.ts.map +1 -1
  54. package/build/src/lib/regex.js +9 -1
  55. package/build/src/lib/regex.js.map +1 -1
  56. package/build/src/lib/schema.d.ts +18 -0
  57. package/build/src/lib/schema.d.ts.map +1 -0
  58. package/build/src/lib/schema.js +13 -0
  59. package/build/src/lib/schema.js.map +1 -0
  60. package/build/src/lib/utility.d.ts +9 -1
  61. package/build/src/lib/utility.d.ts.map +1 -1
  62. package/build/src/lib/utility.js +219 -40
  63. package/build/src/lib/utility.js.map +1 -1
  64. package/build/src/presets/eslint/dx-code-style.d.mts.map +1 -1
  65. package/build/src/presets/eslint/dx-code-style.mjs +0 -20
  66. package/build/src/presets/eslint/dx-code-style.mjs.map +1 -1
  67. package/build/src/presets/eslint/lang-mdx.d.mts.map +1 -1
  68. package/build/src/presets/eslint/lang-mdx.mjs +0 -21
  69. package/build/src/presets/eslint/lang-mdx.mjs.map +1 -1
  70. package/build/src/presets/tsconfig/dx-strict.json +2 -1
  71. package/build/src/rules/eslint/index.d.ts +4 -0
  72. package/build/src/rules/eslint/index.d.ts.map +1 -1
  73. package/build/src/rules/eslint/index.js +4 -0
  74. package/build/src/rules/eslint/index.js.map +1 -1
  75. package/build/src/rules/eslint/no-logger-dev.d.ts +3 -1
  76. package/build/src/rules/eslint/no-logger-dev.d.ts.map +1 -1
  77. package/build/src/rules/eslint/no-logger-dev.js +4 -4
  78. package/build/src/rules/eslint/no-logger-dev.js.map +1 -1
  79. package/build/src/rules/eslint/no-raw-text-in-code.d.ts +6 -0
  80. package/build/src/rules/eslint/no-raw-text-in-code.d.ts.map +1 -0
  81. package/build/src/rules/eslint/no-raw-text-in-code.js +34 -0
  82. package/build/src/rules/eslint/no-raw-text-in-code.js.map +1 -0
  83. package/build/src/rules/eslint/no-regex-literal-flags.d.ts +6 -0
  84. package/build/src/rules/eslint/no-regex-literal-flags.d.ts.map +1 -0
  85. package/build/src/rules/eslint/no-regex-literal-flags.js +30 -0
  86. package/build/src/rules/eslint/no-regex-literal-flags.js.map +1 -0
  87. package/build/src/rules/eslint/no-regex-literals.d.ts +9 -0
  88. package/build/src/rules/eslint/no-regex-literals.d.ts.map +1 -0
  89. package/build/src/rules/eslint/no-regex-literals.js +55 -0
  90. package/build/src/rules/eslint/no-regex-literals.js.map +1 -0
  91. package/build/src/rules/eslint/switch-case-blocks.d.ts +6 -0
  92. package/build/src/rules/eslint/switch-case-blocks.d.ts.map +1 -0
  93. package/build/src/rules/eslint/switch-case-blocks.js +36 -0
  94. package/build/src/rules/eslint/switch-case-blocks.js.map +1 -0
  95. package/build/src/tests/api/node-releases.test.d.ts +2 -0
  96. package/build/src/tests/api/node-releases.test.d.ts.map +1 -0
  97. package/build/src/tests/api/node-releases.test.js +193 -0
  98. package/build/src/tests/api/node-releases.test.js.map +1 -0
  99. package/build/src/tests/api/spdx-licenses.test.d.ts +2 -0
  100. package/build/src/tests/api/spdx-licenses.test.d.ts.map +1 -0
  101. package/build/src/tests/api/spdx-licenses.test.js +91 -0
  102. package/build/src/tests/api/spdx-licenses.test.js.map +1 -0
  103. package/build/src/tests/cli/recipe/pin-versions.test.d.ts +2 -0
  104. package/build/src/tests/cli/recipe/pin-versions.test.d.ts.map +1 -0
  105. package/build/src/tests/cli/recipe/pin-versions.test.js +197 -0
  106. package/build/src/tests/cli/recipe/pin-versions.test.js.map +1 -0
  107. package/build/src/tests/cli/recipe/sync-lts-engines.test.d.ts +2 -0
  108. package/build/src/tests/cli/recipe/sync-lts-engines.test.d.ts.map +1 -0
  109. package/build/src/tests/cli/recipe/sync-lts-engines.test.js +131 -0
  110. package/build/src/tests/cli/recipe/sync-lts-engines.test.js.map +1 -0
  111. package/build/src/tests/lib/item.test.d.ts +2 -0
  112. package/build/src/tests/lib/item.test.d.ts.map +1 -0
  113. package/build/src/tests/lib/item.test.js +142 -0
  114. package/build/src/tests/lib/item.test.js.map +1 -0
  115. package/build/src/tests/lib/nova-config.test.d.ts +2 -0
  116. package/build/src/tests/lib/nova-config.test.d.ts.map +1 -0
  117. package/build/src/tests/lib/nova-config.test.js +489 -0
  118. package/build/src/tests/lib/nova-config.test.js.map +1 -0
  119. package/build/src/tests/lib/regex.test.d.ts +2 -0
  120. package/build/src/tests/lib/regex.test.d.ts.map +1 -0
  121. package/build/src/tests/lib/regex.test.js +342 -0
  122. package/build/src/tests/lib/regex.test.js.map +1 -0
  123. package/build/src/tests/lib/schema.test.d.ts +2 -0
  124. package/build/src/tests/lib/schema.test.d.ts.map +1 -0
  125. package/build/src/tests/lib/schema.test.js +260 -0
  126. package/build/src/tests/lib/schema.test.js.map +1 -0
  127. package/build/src/tests/lib/utility.test.js +704 -44
  128. package/build/src/tests/lib/utility.test.js.map +1 -1
  129. package/build/src/tests/rules/eslint/no-logger-dev.test.d.ts +2 -0
  130. package/build/src/tests/rules/eslint/no-logger-dev.test.d.ts.map +1 -0
  131. package/build/src/tests/rules/eslint/no-logger-dev.test.js +55 -0
  132. package/build/src/tests/rules/eslint/no-logger-dev.test.js.map +1 -0
  133. package/build/src/tests/rules/eslint/no-raw-text-in-code.test.d.ts +2 -0
  134. package/build/src/tests/rules/eslint/no-raw-text-in-code.test.d.ts.map +1 -0
  135. package/build/src/tests/rules/eslint/no-raw-text-in-code.test.js +47 -0
  136. package/build/src/tests/rules/eslint/no-raw-text-in-code.test.js.map +1 -0
  137. package/build/src/tests/rules/eslint/no-regex-literal-flags.test.d.ts +2 -0
  138. package/build/src/tests/rules/eslint/no-regex-literal-flags.test.d.ts.map +1 -0
  139. package/build/src/tests/rules/eslint/no-regex-literal-flags.test.js +47 -0
  140. package/build/src/tests/rules/eslint/no-regex-literal-flags.test.js.map +1 -0
  141. package/build/src/tests/rules/eslint/no-regex-literals.test.d.ts +2 -0
  142. package/build/src/tests/rules/eslint/no-regex-literals.test.d.ts.map +1 -0
  143. package/build/src/tests/rules/eslint/no-regex-literals.test.js +49 -0
  144. package/build/src/tests/rules/eslint/no-regex-literals.test.js.map +1 -0
  145. package/build/src/tests/rules/eslint/switch-case-blocks.test.d.ts +2 -0
  146. package/build/src/tests/rules/eslint/switch-case-blocks.test.d.ts.map +1 -0
  147. package/build/src/tests/rules/eslint/switch-case-blocks.test.js +43 -0
  148. package/build/src/tests/rules/eslint/switch-case-blocks.test.js.map +1 -0
  149. package/build/src/tests/toolkit/cli-header.test.d.ts +2 -0
  150. package/build/src/tests/toolkit/cli-header.test.d.ts.map +1 -0
  151. package/build/src/tests/toolkit/cli-header.test.js +143 -0
  152. package/build/src/tests/toolkit/cli-header.test.js.map +1 -0
  153. package/build/src/tests/toolkit/logger.test.d.ts +2 -0
  154. package/build/src/tests/toolkit/logger.test.d.ts.map +1 -0
  155. package/build/src/tests/toolkit/logger.test.js +96 -0
  156. package/build/src/tests/toolkit/logger.test.js.map +1 -0
  157. package/build/src/tests/toolkit/markdown-table.test.d.ts +2 -0
  158. package/build/src/tests/toolkit/markdown-table.test.d.ts.map +1 -0
  159. package/build/src/tests/toolkit/markdown-table.test.js +138 -0
  160. package/build/src/tests/toolkit/markdown-table.test.js.map +1 -0
  161. package/build/src/toolkit/cli-header.d.ts +1 -0
  162. package/build/src/toolkit/cli-header.d.ts.map +1 -1
  163. package/build/src/toolkit/cli-header.js +24 -13
  164. package/build/src/toolkit/cli-header.js.map +1 -1
  165. package/build/src/toolkit/index.d.ts +1 -1
  166. package/build/src/toolkit/index.d.ts.map +1 -1
  167. package/build/src/toolkit/index.js +1 -1
  168. package/build/src/toolkit/index.js.map +1 -1
  169. package/build/src/toolkit/logger.d.ts.map +1 -1
  170. package/build/src/toolkit/logger.js +25 -10
  171. package/build/src/toolkit/logger.js.map +1 -1
  172. package/build/src/toolkit/markdown-table.d.ts.map +1 -1
  173. package/build/src/toolkit/markdown-table.js +3 -3
  174. package/build/src/toolkit/markdown-table.js.map +1 -1
  175. package/package.json +63 -59
  176. package/build/src/cli/recipe/sync-metadata.d.ts +0 -5
  177. package/build/src/cli/recipe/sync-metadata.d.ts.map +0 -1
  178. package/build/src/cli/recipe/sync-metadata.js +0 -7
  179. package/build/src/cli/recipe/sync-metadata.js.map +0 -1
  180. package/build/src/cli/recipe/sync-versions.d.ts +0 -5
  181. package/build/src/cli/recipe/sync-versions.d.ts.map +0 -1
  182. package/build/src/cli/recipe/sync-versions.js +0 -7
  183. package/build/src/cli/recipe/sync-versions.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import * as path from 'path';
1
+ import { relative, sep } from 'path';
2
2
  import chalk from 'chalk';
3
3
  import prompts from 'prompts';
4
- import { itemAllowedPoliciesByRole } from '../../lib/item.js';
4
+ import { itemAllowedPoliciesByRole, itemAllowedSyncProperties } from '../../lib/item.js';
5
5
  import { NovaConfig } from '../../lib/nova-config.js';
6
6
  import { PATTERN_EMAIL_SIMPLE, PATTERN_SLUG_SCOPED, PATTERN_SLUG_SIMPLE } from '../../lib/regex.js';
7
7
  import { discoverPathsWithFile } from '../../lib/utility.js';
@@ -10,17 +10,25 @@ export class CLIUtilityInitialize {
10
10
  static async run(options) {
11
11
  const currentDirectory = process.cwd();
12
12
  const isProjectRoot = await CLIUtilityInitialize.checkPath(currentDirectory);
13
- if (!isProjectRoot) {
13
+ if (isProjectRoot !== true) {
14
14
  process.exitCode = 1;
15
15
  return;
16
16
  }
17
- if (options.dryRun === true) {
17
+ const isDryRun = options.dryRun === true;
18
+ const isReplaceFile = options.replaceFile === true;
19
+ if (isDryRun === true) {
18
20
  Logger.customize({
19
21
  name: 'CLIUtilityInitialize.run',
20
22
  purpose: 'options',
21
- padBottom: 1,
22
23
  }).warn('Dry run enabled. File changes will not be made in this session.');
23
24
  }
25
+ if (isReplaceFile === true) {
26
+ const replaceFileNotice = (isDryRun) ? 'This option has no effect during a dry run session.' : 'Backup file will not be created.';
27
+ Logger.customize({
28
+ name: 'CLIUtilityInitialize.run',
29
+ purpose: 'options',
30
+ }).warn(`Replace file enabled. ${replaceFileNotice}`);
31
+ }
24
32
  const novaConfig = new NovaConfig();
25
33
  const workingFile = await novaConfig.load();
26
34
  const promptFlowResult = await CLIUtilityInitialize.promptFlow(workingFile);
@@ -28,22 +36,18 @@ export class CLIUtilityInitialize {
28
36
  Logger.customize({
29
37
  name: 'CLIUtilityInitialize.run',
30
38
  purpose: 'promptFlow',
31
- padTop: 1,
32
- padBottom: 1,
33
39
  }).debug('Prompt flow exited without saving.');
34
40
  return;
35
41
  }
36
42
  novaConfig.set(workingFile);
37
- if (options.dryRun === true) {
43
+ if (isDryRun === true) {
38
44
  Logger.customize({
39
45
  name: 'CLIUtilityInitialize.run',
40
46
  purpose: 'promptFlow',
41
- padTop: 1,
42
- padBottom: 1,
43
47
  }).debug('Dry run enabled. Skipping save operation.');
44
48
  return;
45
49
  }
46
- await novaConfig.save();
50
+ await novaConfig.save(isReplaceFile);
47
51
  }
48
52
  static async promptFlow(config) {
49
53
  const category = {
@@ -57,9 +61,14 @@ export class CLIUtilityInitialize {
57
61
  description: 'Manage entities, their roles, and contact information.',
58
62
  handler: CLIUtilityInitialize.promptEntities,
59
63
  },
64
+ emails: {
65
+ label: 'Emails',
66
+ description: 'Configure project emails (bugs, etc.).',
67
+ handler: CLIUtilityInitialize.promptEmails,
68
+ },
60
69
  urls: {
61
70
  label: 'URLs',
62
- description: 'Set URLs for docs, repo, support, and funding sources.',
71
+ description: 'Configure project URLs (homepage, repository, fund sources, etc.).',
63
72
  handler: CLIUtilityInitialize.promptUrls,
64
73
  },
65
74
  workspaces: {
@@ -109,89 +118,88 @@ export class CLIUtilityInitialize {
109
118
  }
110
119
  static async promptProject(config) {
111
120
  const existingProject = config.project;
112
- const existingProjectName = existingProject?.name;
113
- const existingProjectDescription = existingProject?.description;
114
- const existingProjectKeywords = existingProject?.keywords;
121
+ const existingProjectName = (existingProject !== undefined) ? existingProject.name : undefined;
122
+ const existingProjectDescription = (existingProject !== undefined) ? existingProject.description : undefined;
123
+ const existingProjectKeywords = (existingProject !== undefined) ? existingProject.keywords : undefined;
115
124
  const project = (existingProject !== undefined) ? { ...existingProject } : {};
116
125
  const projectName = (existingProjectName !== undefined) ? { ...existingProjectName } : {};
117
126
  const projectDescription = (existingProjectDescription !== undefined) ? { ...existingProjectDescription } : {};
127
+ const projectKeywords = (existingProjectKeywords !== undefined) ? [...existingProjectKeywords] : [];
118
128
  const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
119
129
  {
120
130
  type: 'text',
121
131
  name: 'projectNameTitle',
122
132
  message: 'Project title (display name)',
123
133
  initial: projectName.title ?? '',
134
+ validate: (value) => CLIUtilityInitialize.normalizeText(value, Infinity).result,
124
135
  },
125
136
  {
126
137
  type: 'text',
127
138
  name: 'projectNameSlug',
128
139
  message: 'Project slug (package name)',
129
140
  initial: projectName.slug ?? '',
130
- validate: (value) => {
131
- const trimmed = value.trim();
132
- if (trimmed === '' || new RegExp(PATTERN_SLUG_SIMPLE, 'i').test(trimmed)) {
133
- return true;
134
- }
135
- return 'Use letters, numbers, hyphens, and underscores only.';
136
- },
141
+ validate: (value) => CLIUtilityInitialize.normalizeProjectSlug(value).result,
137
142
  },
138
143
  {
139
144
  type: 'text',
140
145
  name: 'projectDescriptionShort',
141
146
  message: 'Short description',
142
147
  initial: projectDescription.short ?? '',
148
+ validate: (value) => CLIUtilityInitialize.normalizeText(value, Infinity).result,
143
149
  },
144
150
  {
145
151
  type: 'text',
146
152
  name: 'projectDescriptionLong',
147
153
  message: 'Long description',
148
154
  initial: projectDescription.long ?? '',
155
+ validate: (value) => CLIUtilityInitialize.normalizeText(value, Infinity).result,
149
156
  },
150
157
  {
151
158
  type: 'text',
152
159
  name: 'projectKeywords',
153
160
  message: 'Keywords (comma separated)',
154
- initial: (Array.isArray(existingProjectKeywords)) ? existingProjectKeywords.join(', ') : '',
161
+ initial: (projectKeywords.length > 0) ? projectKeywords.join(', ') : '',
162
+ validate: (value) => CLIUtilityInitialize.normalizeTextArray(value, 50).result,
155
163
  },
156
164
  ]);
157
165
  if (questionsOutput.cancelled) {
158
166
  return 'back';
159
167
  }
160
168
  const questionsOutputResult = questionsOutput.result;
161
- const projectNameTitleInput = (questionsOutputResult.projectNameTitle ?? '').trim();
162
- const projectNameSlugInput = (questionsOutputResult.projectNameSlug ?? '').trim();
163
- const projectDescriptionShortInput = (questionsOutputResult.projectDescriptionShort ?? '').trim();
164
- const projectDescriptionLongInput = (questionsOutputResult.projectDescriptionLong ?? '').trim();
165
- const projectKeywordsInput = (questionsOutputResult.projectKeywords ?? '').trim();
166
- if (projectNameTitleInput === '') {
167
- Reflect.deleteProperty(projectName, 'title');
169
+ const projectNameTitleInput = CLIUtilityInitialize.normalizeText(questionsOutputResult.projectNameTitle, Infinity).sanitized;
170
+ const projectNameSlugInput = CLIUtilityInitialize.normalizeProjectSlug(questionsOutputResult.projectNameSlug).sanitized;
171
+ const projectDescriptionShortInput = CLIUtilityInitialize.normalizeText(questionsOutputResult.projectDescriptionShort, Infinity).sanitized;
172
+ const projectDescriptionLongInput = CLIUtilityInitialize.normalizeText(questionsOutputResult.projectDescriptionLong, Infinity).sanitized;
173
+ const projectKeywordsInput = CLIUtilityInitialize.normalizeTextArray(questionsOutputResult.projectKeywords, 50).sanitized;
174
+ if (projectNameTitleInput !== undefined) {
175
+ projectName.title = projectNameTitleInput;
168
176
  }
169
177
  else {
170
- projectName.title = projectNameTitleInput;
178
+ Reflect.deleteProperty(projectName, 'title');
171
179
  }
172
- if (projectNameSlugInput === '') {
173
- Reflect.deleteProperty(projectName, 'slug');
180
+ if (projectNameSlugInput !== undefined) {
181
+ projectName.slug = projectNameSlugInput;
174
182
  }
175
183
  else {
176
- projectName.slug = projectNameSlugInput;
184
+ Reflect.deleteProperty(projectName, 'slug');
177
185
  }
178
- if (projectDescriptionShortInput === '') {
179
- Reflect.deleteProperty(projectDescription, 'short');
186
+ if (Object.keys(projectName).length > 0) {
187
+ project.name = projectName;
180
188
  }
181
189
  else {
182
- projectDescription.short = projectDescriptionShortInput;
190
+ Reflect.deleteProperty(project, 'name');
183
191
  }
184
- if (projectDescriptionLongInput === '') {
185
- Reflect.deleteProperty(projectDescription, 'long');
192
+ if (projectDescriptionShortInput !== undefined) {
193
+ projectDescription.short = projectDescriptionShortInput;
186
194
  }
187
195
  else {
188
- projectDescription.long = projectDescriptionLongInput;
196
+ Reflect.deleteProperty(projectDescription, 'short');
189
197
  }
190
- if (Object.keys(projectName).length > 0) {
191
- project.name = projectName;
198
+ if (projectDescriptionLongInput !== undefined) {
199
+ projectDescription.long = projectDescriptionLongInput;
192
200
  }
193
201
  else {
194
- Reflect.deleteProperty(project, 'name');
202
+ Reflect.deleteProperty(projectDescription, 'long');
195
203
  }
196
204
  if (Object.keys(projectDescription).length > 0) {
197
205
  project.description = projectDescription;
@@ -199,20 +207,11 @@ export class CLIUtilityInitialize {
199
207
  else {
200
208
  Reflect.deleteProperty(project, 'description');
201
209
  }
202
- if (projectKeywordsInput === '') {
203
- Reflect.deleteProperty(project, 'keywords');
210
+ if (projectKeywordsInput !== undefined && projectKeywordsInput.length > 0) {
211
+ project.keywords = projectKeywordsInput;
204
212
  }
205
213
  else {
206
- const projectKeywordsList = projectKeywordsInput
207
- .split(',')
208
- .map((projectKeywordInput) => projectKeywordInput.trim())
209
- .filter((projectKeywordInput) => projectKeywordInput !== '');
210
- if (projectKeywordsList.length > 0) {
211
- project.keywords = projectKeywordsList;
212
- }
213
- else {
214
- Reflect.deleteProperty(project, 'keywords');
215
- }
214
+ Reflect.deleteProperty(project, 'keywords');
216
215
  }
217
216
  if (Object.keys(project).length > 0) {
218
217
  Object.assign(config, { project });
@@ -220,8 +219,8 @@ export class CLIUtilityInitialize {
220
219
  else {
221
220
  Reflect.deleteProperty(config, 'project');
222
221
  }
223
- const previousSlug = existingProjectName?.slug ?? '';
224
- const currentSlug = config.project?.name?.slug ?? '';
222
+ const previousSlug = (existingProjectName !== undefined) ? existingProjectName.slug ?? '' : '';
223
+ const currentSlug = (config.project !== undefined && config.project.name !== undefined) ? config.project.name.slug ?? '' : '';
225
224
  const slugChanged = previousSlug !== currentSlug;
226
225
  if (slugChanged && config.workspaces !== undefined) {
227
226
  const rolesToSync = ['project', 'docs', 'config', 'app', 'tool'];
@@ -411,7 +410,7 @@ export class CLIUtilityInitialize {
411
410
  const entityEmail = (typeof entityToRemove.email === 'string') ? entityToRemove.email.trim() : '';
412
411
  const entityLabel = entityName || entityEmail || `Entity ${entityIndex + 1}`;
413
412
  const shouldRemove = await CLIUtilityInitialize.promptEntitiesDeleteForm(entityLabel);
414
- if (!shouldRemove) {
413
+ if (shouldRemove !== true) {
415
414
  continue;
416
415
  }
417
416
  entities.splice(entityIndex, 1);
@@ -427,11 +426,11 @@ export class CLIUtilityInitialize {
427
426
  }
428
427
  static async promptEntitiesForm(entity, mode) {
429
428
  const validRoles = ['author', 'contributor', 'supporter'];
430
- const existingName = (typeof entity?.name === 'string') ? entity.name : '';
431
- const existingEmail = (typeof entity?.email === 'string') ? entity.email : '';
432
- const existingUrl = (typeof entity?.url === 'string') ? entity.url : '';
429
+ const existingName = (entity !== undefined && typeof entity.name === 'string') ? entity.name : '';
430
+ const existingEmail = (entity !== undefined && typeof entity.email === 'string') ? entity.email : '';
431
+ const existingUrl = (entity !== undefined && typeof entity.url === 'string') ? entity.url : '';
433
432
  let existingRoles = [];
434
- if (Array.isArray(entity?.roles)) {
433
+ if (entity !== undefined && Array.isArray(entity.roles)) {
435
434
  existingRoles = entity.roles.filter((role) => validRoles.includes(role));
436
435
  }
437
436
  const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
@@ -440,38 +439,21 @@ export class CLIUtilityInitialize {
440
439
  name: 'entityName',
441
440
  message: 'Entity name',
442
441
  initial: existingName,
442
+ validate: (value) => CLIUtilityInitialize.normalizeText(value, Infinity).result,
443
443
  },
444
444
  {
445
445
  type: 'text',
446
446
  name: 'entityEmail',
447
447
  message: 'Entity email address',
448
448
  initial: existingEmail,
449
- validate: (value) => {
450
- const trimmed = value.trim();
451
- if (trimmed === '') {
452
- return true;
453
- }
454
- if (PATTERN_EMAIL_SIMPLE.test(trimmed)) {
455
- return true;
456
- }
457
- return 'Enter a valid email address or leave blank.';
458
- },
449
+ validate: (value) => CLIUtilityInitialize.normalizeEmail(value).result,
459
450
  },
460
451
  {
461
452
  type: 'text',
462
453
  name: 'entityUrl',
463
454
  message: 'Entity website',
464
455
  initial: existingUrl,
465
- validate: (value) => {
466
- const trimmed = value.trim();
467
- if (trimmed === '') {
468
- return true;
469
- }
470
- if (CLIUtilityInitialize.isAllowedHttpUrl(trimmed)) {
471
- return true;
472
- }
473
- return 'Enter a valid URL (http:// or https://) or leave blank.';
474
- },
456
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
475
457
  },
476
458
  {
477
459
  type: 'multiselect',
@@ -490,18 +472,18 @@ export class CLIUtilityInitialize {
490
472
  };
491
473
  }
492
474
  const questionsOutputResult = questionsOutput.result;
493
- const entityNameInput = (typeof questionsOutputResult.entityName === 'string') ? questionsOutputResult.entityName.trim() : '';
494
- const entityEmailInput = (typeof questionsOutputResult.entityEmail === 'string') ? questionsOutputResult.entityEmail.trim() : '';
495
- const entityUrlInput = (typeof questionsOutputResult.entityUrl === 'string') ? questionsOutputResult.entityUrl.trim() : '';
475
+ const entityNameInput = CLIUtilityInitialize.normalizeText(questionsOutputResult.entityName, Infinity).sanitized;
476
+ const entityEmailInput = CLIUtilityInitialize.normalizeEmail(questionsOutputResult.entityEmail).sanitized;
477
+ const entityUrlInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.entityUrl, 'generic').sanitized;
496
478
  const entityRolesInput = Array.isArray(questionsOutputResult.entityRoles) ? [...questionsOutputResult.entityRoles] : [];
497
479
  const resolvedEntity = {};
498
- if (entityNameInput !== '') {
480
+ if (entityNameInput !== undefined) {
499
481
  resolvedEntity.name = entityNameInput;
500
482
  }
501
- if (entityEmailInput !== '') {
483
+ if (entityEmailInput !== undefined) {
502
484
  resolvedEntity.email = entityEmailInput;
503
485
  }
504
- if (entityUrlInput !== '') {
486
+ if (entityUrlInput !== undefined) {
505
487
  resolvedEntity.url = entityUrlInput;
506
488
  }
507
489
  if (entityRolesInput.length > 0) {
@@ -530,112 +512,180 @@ export class CLIUtilityInitialize {
530
512
  const confirmOutputResult = confirmOutput.result;
531
513
  return confirmOutputResult.confirm;
532
514
  }
515
+ static async promptEmails(config) {
516
+ const existingEmails = config.emails;
517
+ const emails = (existingEmails !== undefined) ? { ...existingEmails } : {};
518
+ const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
519
+ {
520
+ type: 'text',
521
+ name: 'emailsBugs',
522
+ message: 'Issue tracker email',
523
+ initial: emails.bugs ?? '',
524
+ validate: (value) => CLIUtilityInitialize.normalizeEmail(value).result,
525
+ },
526
+ ]);
527
+ if (questionsOutput.cancelled) {
528
+ return 'back';
529
+ }
530
+ const questionsOutputResult = questionsOutput.result;
531
+ const emailsBugsInput = CLIUtilityInitialize.normalizeEmail(questionsOutputResult.emailsBugs).sanitized;
532
+ if (emailsBugsInput !== undefined) {
533
+ emails.bugs = emailsBugsInput;
534
+ }
535
+ else {
536
+ Reflect.deleteProperty(emails, 'bugs');
537
+ }
538
+ if (Object.keys(emails).length > 0) {
539
+ Object.assign(config, { emails });
540
+ }
541
+ else {
542
+ Reflect.deleteProperty(config, 'emails');
543
+ }
544
+ Logger.customize({
545
+ name: 'CLIUtilityInitialize.promptEmails',
546
+ purpose: 'updated',
547
+ padTop: 1,
548
+ padBottom: 1,
549
+ }).info('Email references updated.');
550
+ return 'back';
551
+ }
533
552
  static async promptUrls(config) {
534
553
  const existingUrls = config.urls;
535
554
  const urls = (existingUrls !== undefined) ? { ...existingUrls } : {};
536
- const validatedUrls = {};
537
- const validate = (key, input) => {
538
- const field = (key === 'repository') ? 'repository' : undefined;
539
- const sanitizedUrl = CLIUtilityInitialize.sanitizeHttpUrl(input, field);
540
- if (sanitizedUrl !== undefined) {
541
- validatedUrls[key] = sanitizedUrl;
542
- }
543
- };
544
555
  const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
545
556
  {
546
557
  type: 'text',
547
558
  name: 'urlsHomepage',
548
559
  message: 'Homepage URL',
549
560
  initial: urls.homepage ?? '',
550
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
561
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
551
562
  },
552
563
  {
553
564
  type: 'text',
554
565
  name: 'urlsRepository',
555
566
  message: 'Repository URL',
556
567
  initial: urls.repository ?? '',
557
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value, 'repository'),
568
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'repository').result,
558
569
  },
559
570
  {
560
571
  type: 'text',
561
572
  name: 'urlsBugs',
562
573
  message: 'Issue tracker URL',
563
574
  initial: urls.bugs ?? '',
564
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
575
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
565
576
  },
566
577
  {
567
578
  type: 'text',
568
579
  name: 'urlsLicense',
569
580
  message: 'License URL',
570
581
  initial: urls.license ?? '',
571
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
582
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
572
583
  },
573
584
  {
574
585
  type: 'text',
575
586
  name: 'urlsLogo',
576
587
  message: 'Logo URL',
577
588
  initial: urls.logo ?? '',
578
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
589
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
579
590
  },
580
591
  {
581
592
  type: 'text',
582
593
  name: 'urlsDocumentation',
583
594
  message: 'Documentation URL',
584
595
  initial: urls.documentation ?? '',
585
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
596
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
586
597
  },
587
598
  {
588
599
  type: 'text',
589
600
  name: 'urlsGithub',
590
601
  message: 'GitHub URL',
591
602
  initial: urls.github ?? '',
592
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
603
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
593
604
  },
594
605
  {
595
606
  type: 'text',
596
607
  name: 'urlsNpm',
597
608
  message: 'npm package URL',
598
609
  initial: urls.npm ?? '',
599
- validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
610
+ validate: (value) => CLIUtilityInitialize.normalizeUrl(value, 'generic').result,
600
611
  },
601
612
  {
602
613
  type: 'text',
603
614
  name: 'urlsFundSources',
604
615
  message: 'Funding URLs (comma separated)',
605
616
  initial: (Array.isArray(urls.fundSources)) ? urls.fundSources.join(', ') : '',
606
- validate: CLIUtilityInitialize.validateFundSources,
617
+ validate: (value) => CLIUtilityInitialize.normalizeUrlArray(value, 'generic').result,
607
618
  },
608
619
  ]);
609
620
  if (questionsOutput.cancelled) {
610
621
  return 'back';
611
622
  }
612
623
  const questionsOutputResult = questionsOutput.result;
613
- validate('homepage', questionsOutputResult.urlsHomepage);
614
- validate('repository', questionsOutputResult.urlsRepository);
615
- validate('bugs', questionsOutputResult.urlsBugs);
616
- validate('license', questionsOutputResult.urlsLicense);
617
- validate('logo', questionsOutputResult.urlsLogo);
618
- validate('documentation', questionsOutputResult.urlsDocumentation);
619
- validate('github', questionsOutputResult.urlsGithub);
620
- validate('npm', questionsOutputResult.urlsNpm);
621
- const fundSourcesParts = questionsOutputResult.urlsFundSources
622
- .split(',')
623
- .map((fundSourceInput) => fundSourceInput.trim())
624
- .filter((fundSourceInput) => fundSourceInput !== '');
625
- if (fundSourcesParts.length > 0) {
626
- const fundSourcesList = [];
627
- for (const fundSourcesPart of fundSourcesParts) {
628
- const sanitizedUrl = CLIUtilityInitialize.sanitizeHttpUrl(fundSourcesPart, 'generic');
629
- if (sanitizedUrl !== undefined) {
630
- fundSourcesList.push(sanitizedUrl);
631
- }
632
- }
633
- if (fundSourcesList.length > 0) {
634
- validatedUrls.fundSources = fundSourcesList;
635
- }
624
+ const urlsHomepageInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsHomepage, 'generic').sanitized;
625
+ const urlsRepositoryInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsRepository, 'repository').sanitized;
626
+ const urlsBugsInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsBugs, 'generic').sanitized;
627
+ const urlsLicenseInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsLicense, 'generic').sanitized;
628
+ const urlsLogoInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsLogo, 'generic').sanitized;
629
+ const urlsDocumentationInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsDocumentation, 'generic').sanitized;
630
+ const urlsGithubInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsGithub, 'generic').sanitized;
631
+ const urlsNpmInput = CLIUtilityInitialize.normalizeUrl(questionsOutputResult.urlsNpm, 'generic').sanitized;
632
+ const urlsFundSourcesInput = CLIUtilityInitialize.normalizeUrlArray(questionsOutputResult.urlsFundSources, 'generic').sanitized;
633
+ if (urlsHomepageInput !== undefined) {
634
+ urls.homepage = urlsHomepageInput;
635
+ }
636
+ else {
637
+ Reflect.deleteProperty(urls, 'homepage');
638
+ }
639
+ if (urlsRepositoryInput !== undefined) {
640
+ urls.repository = urlsRepositoryInput;
641
+ }
642
+ else {
643
+ Reflect.deleteProperty(urls, 'repository');
636
644
  }
637
- if (Object.keys(validatedUrls).length > 0) {
638
- Object.assign(config, { urls: validatedUrls });
645
+ if (urlsBugsInput !== undefined) {
646
+ urls.bugs = urlsBugsInput;
647
+ }
648
+ else {
649
+ Reflect.deleteProperty(urls, 'bugs');
650
+ }
651
+ if (urlsLicenseInput !== undefined) {
652
+ urls.license = urlsLicenseInput;
653
+ }
654
+ else {
655
+ Reflect.deleteProperty(urls, 'license');
656
+ }
657
+ if (urlsLogoInput !== undefined) {
658
+ urls.logo = urlsLogoInput;
659
+ }
660
+ else {
661
+ Reflect.deleteProperty(urls, 'logo');
662
+ }
663
+ if (urlsDocumentationInput !== undefined) {
664
+ urls.documentation = urlsDocumentationInput;
665
+ }
666
+ else {
667
+ Reflect.deleteProperty(urls, 'documentation');
668
+ }
669
+ if (urlsGithubInput !== undefined) {
670
+ urls.github = urlsGithubInput;
671
+ }
672
+ else {
673
+ Reflect.deleteProperty(urls, 'github');
674
+ }
675
+ if (urlsNpmInput !== undefined) {
676
+ urls.npm = urlsNpmInput;
677
+ }
678
+ else {
679
+ Reflect.deleteProperty(urls, 'npm');
680
+ }
681
+ if (urlsFundSourcesInput !== undefined) {
682
+ urls.fundSources = urlsFundSourcesInput;
683
+ }
684
+ else {
685
+ Reflect.deleteProperty(urls, 'fundSources');
686
+ }
687
+ if (Object.keys(urls).length > 0) {
688
+ Object.assign(config, { urls });
639
689
  }
640
690
  else {
641
691
  Reflect.deleteProperty(config, 'urls');
@@ -652,11 +702,11 @@ export class CLIUtilityInitialize {
652
702
  const workspaces = (config.workspaces) ? { ...(config.workspaces) } : {};
653
703
  const rawWorkspacePaths = await discoverPathsWithFile('package.json', 'forward');
654
704
  const workspacePaths = rawWorkspacePaths.map((rawWorkspacePath) => {
655
- const relativePath = path.relative(process.cwd(), rawWorkspacePath);
705
+ const relativePath = relative(process.cwd(), rawWorkspacePath);
656
706
  if (relativePath === '') {
657
707
  return './';
658
708
  }
659
- return `./${relativePath.split(path.sep).join('/')}`;
709
+ return `./${relativePath.split(sep).join('/')}`;
660
710
  });
661
711
  Logger.customize({
662
712
  name: 'CLIUtilityInitialize.promptWorkspaces',
@@ -666,13 +716,13 @@ export class CLIUtilityInitialize {
666
716
  const choices = workspacePaths.map((workspacePath) => {
667
717
  const workspace = workspaces[workspacePath];
668
718
  const summaryParts = [];
669
- if (workspace.name) {
719
+ if (workspace !== undefined && workspace.name !== undefined) {
670
720
  summaryParts.push(workspace.name);
671
721
  }
672
- if (workspace.role) {
722
+ if (workspace !== undefined && workspace.role !== undefined) {
673
723
  summaryParts.push(workspace.role);
674
724
  }
675
- if (workspace.policy) {
725
+ if (workspace !== undefined && workspace.policy !== undefined) {
676
726
  summaryParts.push(workspace.policy);
677
727
  }
678
728
  return {
@@ -704,12 +754,12 @@ export class CLIUtilityInitialize {
704
754
  const formResult = await CLIUtilityInitialize.promptWorkspacesForm({
705
755
  workspacePath,
706
756
  existingWorkspace: workspaces[workspacePath],
707
- projectSlug: config.project?.name?.slug,
757
+ projectSlug: (config.project !== undefined && config.project.name !== undefined) ? config.project.name.slug : undefined,
708
758
  });
709
759
  if (formResult.action === 'back') {
710
760
  continue;
711
761
  }
712
- workspaces[workspacePath] = formResult.workspace;
762
+ Reflect.set(workspaces, workspacePath, formResult.workspace);
713
763
  Object.assign(config, { workspaces });
714
764
  Logger.customize({
715
765
  name: 'CLIUtilityInitialize.promptWorkspaces',
@@ -751,6 +801,11 @@ export class CLIUtilityInitialize {
751
801
  description: 'Internal CLI or build tools (e.g., codegen, bundler)',
752
802
  value: 'tool',
753
803
  },
804
+ {
805
+ title: 'Template',
806
+ description: 'Ready-to-copy scaffold bundles consumed by generators (e.g., starter files)',
807
+ value: 'template',
808
+ },
754
809
  ];
755
810
  const policy = {
756
811
  freezable: {
@@ -778,42 +833,13 @@ export class CLIUtilityInitialize {
778
833
  type: 'text',
779
834
  name: 'workspaceName',
780
835
  message: 'Workspace package name',
781
- initial: options.existingWorkspace.name,
782
- validate: (value) => {
783
- const trimmed = value.trim();
784
- if (trimmed === '') {
785
- return 'Enter a package name.';
786
- }
787
- switch (role) {
788
- case 'config':
789
- case 'app':
790
- case 'tool':
791
- const expectedPrefix = `${base}-`;
792
- if (!trimmed.startsWith(expectedPrefix)) {
793
- return `Begin with "${expectedPrefix}" and add a descriptor slug.`;
794
- }
795
- const descriptor = trimmed.slice(expectedPrefix.length);
796
- if (descriptor.length === 0) {
797
- return 'Add a descriptor after the prefix.';
798
- }
799
- if (!PATTERN_SLUG_SIMPLE.test(descriptor)) {
800
- return 'Descriptor must match the slug pattern (lowercase letters, numbers, hyphens, underscores).';
801
- }
802
- return true;
803
- case 'package':
804
- default:
805
- if (PATTERN_SLUG_SIMPLE.test(trimmed) || PATTERN_SLUG_SCOPED.test(trimmed)) {
806
- return true;
807
- }
808
- return 'Enter an unscoped slug or a scoped package name (e.g. @scope/name).';
809
- }
810
- },
836
+ initial: (options.existingWorkspace !== undefined) ? options.existingWorkspace.name ?? '' : '',
837
+ validate: (value) => CLIUtilityInitialize.normalizeWorkspaceName(value, role, base).result,
811
838
  });
812
839
  if (namePrompt.cancelled) {
813
840
  return undefined;
814
841
  }
815
- const name = namePrompt.result.workspaceName.trim();
816
- return (name === '') ? undefined : name;
842
+ return CLIUtilityInitialize.normalizeWorkspaceName(namePrompt.result.workspaceName, role, base).sanitized;
817
843
  };
818
844
  const rolePrompt = await CLIUtilityInitialize.promptWithCancel({
819
845
  type: 'select',
@@ -824,7 +850,7 @@ export class CLIUtilityInitialize {
824
850
  description: role.description,
825
851
  value: role.value,
826
852
  })),
827
- initial: Math.max(0, allowedRoles.findIndex((role) => role.value === options.existingWorkspace.role)),
853
+ initial: Math.max(0, allowedRoles.findIndex((role) => options.existingWorkspace !== undefined && role.value === options.existingWorkspace.role)),
828
854
  });
829
855
  if (rolePrompt.cancelled) {
830
856
  return {
@@ -842,7 +868,7 @@ export class CLIUtilityInitialize {
842
868
  description: policy[allowedPolicy].description,
843
869
  value: allowedPolicy,
844
870
  })),
845
- initial: Math.max(0, allowedPolicies.findIndex((policy) => policy === options.existingWorkspace.policy)),
871
+ initial: Math.max(0, allowedPolicies.findIndex((policy) => options.existingWorkspace !== undefined && policy === options.existingWorkspace.policy)),
846
872
  });
847
873
  if (policyPrompt.cancelled) {
848
874
  return {
@@ -856,12 +882,61 @@ export class CLIUtilityInitialize {
856
882
  action: 'back',
857
883
  };
858
884
  }
885
+ let syncProperties;
886
+ if (selectedPolicy === 'distributable') {
887
+ const syncPropertiesPrompt = await CLIUtilityInitialize.promptWithCancel({
888
+ type: 'multiselect',
889
+ name: 'workspaceSyncProperties',
890
+ message: 'Select metadata properties to sync',
891
+ choices: itemAllowedSyncProperties.map((property) => ({
892
+ title: property,
893
+ value: property,
894
+ selected: (options.existingWorkspace !== undefined && options.existingWorkspace.syncProperties !== undefined) ? options.existingWorkspace.syncProperties.includes(property) : false,
895
+ })),
896
+ });
897
+ if (syncPropertiesPrompt.cancelled) {
898
+ return {
899
+ action: 'back',
900
+ };
901
+ }
902
+ const selectedSyncProperties = syncPropertiesPrompt.result.workspaceSyncProperties;
903
+ if (selectedSyncProperties.length > 0) {
904
+ syncProperties = selectedSyncProperties;
905
+ }
906
+ }
907
+ const pinVersionsPrompt = await CLIUtilityInitialize.promptWithCancel({
908
+ type: 'confirm',
909
+ name: 'workspacePinVersions',
910
+ message: 'Pin dependency versions?',
911
+ initial: options.existingWorkspace !== undefined && options.existingWorkspace.pinVersions === true,
912
+ });
913
+ if (pinVersionsPrompt.cancelled) {
914
+ return {
915
+ action: 'back',
916
+ };
917
+ }
918
+ const selectedPinVersions = pinVersionsPrompt.result.workspacePinVersions;
919
+ const syncLtsEnginesPrompt = await CLIUtilityInitialize.promptWithCancel({
920
+ type: 'confirm',
921
+ name: 'workspaceSyncLtsEngines',
922
+ message: 'Sync Node.js LTS engine constraint?',
923
+ initial: options.existingWorkspace !== undefined && options.existingWorkspace.syncLtsEngines === true,
924
+ });
925
+ if (syncLtsEnginesPrompt.cancelled) {
926
+ return {
927
+ action: 'back',
928
+ };
929
+ }
930
+ const selectedSyncLtsEngines = syncLtsEnginesPrompt.result.workspaceSyncLtsEngines;
859
931
  return {
860
932
  action: 'apply',
861
933
  workspace: {
862
934
  name: resolvedName,
863
935
  role: selectedRole,
864
936
  policy: selectedPolicy,
937
+ ...(syncProperties !== undefined) ? { syncProperties } : {},
938
+ ...(selectedPinVersions === true) ? { pinVersions: selectedPinVersions } : {},
939
+ ...(selectedSyncLtsEngines === true) ? { syncLtsEngines: selectedSyncLtsEngines } : {},
865
940
  },
866
941
  };
867
942
  }
@@ -893,7 +968,6 @@ export class CLIUtilityInitialize {
893
968
  Logger.customize({
894
969
  name: 'CLIUtilityInitialize.checkPath',
895
970
  purpose: 'lessThanOne',
896
- padBottom: 1,
897
971
  }).error([
898
972
  'No "package.json" files were found. Re-run this command inside the project root directory.',
899
973
  `Current directory is "${currentDirectory}"`,
@@ -904,7 +978,6 @@ export class CLIUtilityInitialize {
904
978
  Logger.customize({
905
979
  name: 'CLIUtilityInitialize.checkPath',
906
980
  purpose: 'greaterThanOne',
907
- padBottom: 1,
908
981
  }).error([
909
982
  'Multiple "package.json" files were found. Re-run this command inside the project root directory.',
910
983
  `Current directory is "${currentDirectory}"`,
@@ -915,7 +988,6 @@ export class CLIUtilityInitialize {
915
988
  Logger.customize({
916
989
  name: 'CLIUtilityInitialize.checkPath',
917
990
  purpose: 'notProjectRootDir',
918
- padBottom: 1,
919
991
  }).error([
920
992
  'Must be run inside the project root directory.',
921
993
  `Current directory is "${currentDirectory}"`,
@@ -924,60 +996,256 @@ export class CLIUtilityInitialize {
924
996
  }
925
997
  return true;
926
998
  }
927
- static validateHttpUrl(value, field) {
928
- const trimmed = value.trim();
929
- if (trimmed === '') {
930
- return true;
999
+ static normalizeEmail(value) {
1000
+ if (typeof value !== 'string') {
1001
+ return {
1002
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1003
+ sanitized: undefined,
1004
+ };
1005
+ }
1006
+ const trimmedValue = value.trim();
1007
+ if (trimmedValue === '') {
1008
+ return {
1009
+ result: true,
1010
+ sanitized: undefined,
1011
+ };
931
1012
  }
932
- if (CLIUtilityInitialize.isAllowedHttpUrl(trimmed, field)) {
933
- return true;
1013
+ if (!PATTERN_EMAIL_SIMPLE.test(trimmedValue)) {
1014
+ return {
1015
+ result: 'Enter a valid email address or leave blank.',
1016
+ sanitized: undefined,
1017
+ };
934
1018
  }
935
- return 'Enter a valid URL (http:// or https://) or leave blank.';
1019
+ return {
1020
+ result: true,
1021
+ sanitized: trimmedValue,
1022
+ };
936
1023
  }
937
- static validateFundSources(value) {
938
- const trimmed = value.trim();
939
- if (trimmed === '') {
940
- return true;
1024
+ static normalizeProjectSlug(value) {
1025
+ if (typeof value !== 'string') {
1026
+ return {
1027
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1028
+ sanitized: undefined,
1029
+ };
1030
+ }
1031
+ const trimmedValue = value.trim();
1032
+ if (trimmedValue === '') {
1033
+ return {
1034
+ result: true,
1035
+ sanitized: undefined,
1036
+ };
1037
+ }
1038
+ if (trimmedValue.length > 214
1039
+ || !new RegExp(PATTERN_SLUG_SIMPLE, 'i').test(trimmedValue)) {
1040
+ return {
1041
+ result: 'Use only letters, numbers, hyphens, or underscores, and keep it at 214 characters or fewer.',
1042
+ sanitized: undefined,
1043
+ };
1044
+ }
1045
+ return {
1046
+ result: true,
1047
+ sanitized: trimmedValue,
1048
+ };
1049
+ }
1050
+ static normalizeText(value, maxLength) {
1051
+ if (typeof value !== 'string') {
1052
+ return {
1053
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1054
+ sanitized: undefined,
1055
+ };
1056
+ }
1057
+ const trimmedValue = value.trim();
1058
+ if (trimmedValue === '') {
1059
+ return {
1060
+ result: true,
1061
+ sanitized: undefined,
1062
+ };
1063
+ }
1064
+ if (trimmedValue.length > maxLength) {
1065
+ return {
1066
+ result: `Input a value under ${maxLength} character(s) or leave blank.`,
1067
+ sanitized: undefined,
1068
+ };
1069
+ }
1070
+ return {
1071
+ result: true,
1072
+ sanitized: trimmedValue,
1073
+ };
1074
+ }
1075
+ static normalizeTextArray(value, maxLengthPerItem) {
1076
+ if (typeof value !== 'string') {
1077
+ return {
1078
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1079
+ sanitized: undefined,
1080
+ };
1081
+ }
1082
+ const trimmedValue = value.trim();
1083
+ if (trimmedValue === '') {
1084
+ return {
1085
+ result: true,
1086
+ sanitized: undefined,
1087
+ };
941
1088
  }
942
- const parts = trimmed
1089
+ const items = trimmedValue
943
1090
  .split(',')
944
- .map((part) => part.trim())
945
- .filter((part) => part !== '');
946
- for (const part of parts) {
947
- if (!CLIUtilityInitialize.isAllowedHttpUrl(part, 'generic')) {
948
- return 'Enter comma separated URLs (http:// or https://) or leave blank.';
1091
+ .map((item) => item.trim())
1092
+ .filter((item) => item !== '');
1093
+ for (let i = 0; i < items.length; i += 1) {
1094
+ const { result, sanitized } = CLIUtilityInitialize.normalizeText(items[i], maxLengthPerItem);
1095
+ if (result !== true) {
1096
+ return {
1097
+ result: `Invalid entry "${items[i]}": Input a value under ${maxLengthPerItem} character(s) or remove entry.`,
1098
+ sanitized: undefined,
1099
+ };
1100
+ }
1101
+ if (sanitized !== undefined) {
1102
+ items[i] = sanitized;
949
1103
  }
950
1104
  }
951
- return true;
1105
+ return {
1106
+ result: true,
1107
+ sanitized: items,
1108
+ };
952
1109
  }
953
- static sanitizeHttpUrl(value, field) {
1110
+ static normalizeUrl(value, protocol) {
954
1111
  if (typeof value !== 'string') {
955
- return undefined;
1112
+ return {
1113
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1114
+ sanitized: undefined,
1115
+ };
956
1116
  }
957
- const trimmed = value.trim();
958
- if (trimmed === '') {
959
- return undefined;
1117
+ const trimmedValue = value.trim();
1118
+ if (trimmedValue === '') {
1119
+ return {
1120
+ result: true,
1121
+ sanitized: undefined,
1122
+ };
960
1123
  }
1124
+ const rules = {
1125
+ generic: {
1126
+ allowed: ['http:', 'https:'],
1127
+ message: 'Enter a valid generic URL (e.g., https://) or leave blank.',
1128
+ },
1129
+ repository: {
1130
+ allowed: ['git:', 'git+https:', 'git+ssh:', 'git+http:', 'http:', 'https:'],
1131
+ message: 'Enter a valid repository URL (e.g., git+https://) or leave blank.',
1132
+ },
1133
+ };
1134
+ const allowed = (protocol === 'repository') ? rules.repository.allowed : rules.generic.allowed;
1135
+ const errorMessage = (protocol === 'repository') ? rules.repository.message : rules.generic.message;
961
1136
  try {
962
- const parsedUrl = new URL(trimmed);
963
- if (CLIUtilityInitialize.isAllowedHttpUrl(parsedUrl.toString(), field)) {
964
- return parsedUrl.toString();
1137
+ const url = new URL(trimmedValue);
1138
+ if (allowed.includes(url.protocol)) {
1139
+ return {
1140
+ result: true,
1141
+ sanitized: url.toString(),
1142
+ };
965
1143
  }
966
1144
  }
967
1145
  catch {
968
1146
  }
969
- return undefined;
1147
+ return {
1148
+ result: errorMessage,
1149
+ sanitized: undefined,
1150
+ };
970
1151
  }
971
- static isAllowedHttpUrl(value, field) {
972
- try {
973
- const url = new URL(value);
974
- const genericProtocols = ['http:', 'https:'];
975
- const repositoryProtocols = ['git:', 'git+https:', 'git+ssh:', 'git+http:', 'https:', 'http:'];
976
- const allowedProtocols = (field === 'repository') ? repositoryProtocols : genericProtocols;
977
- return allowedProtocols.includes(url.protocol);
1152
+ static normalizeUrlArray(value, protocol) {
1153
+ if (typeof value !== 'string') {
1154
+ return {
1155
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1156
+ sanitized: undefined,
1157
+ };
978
1158
  }
979
- catch {
980
- return false;
1159
+ const trimmedValue = value.trim();
1160
+ if (trimmedValue === '') {
1161
+ return {
1162
+ result: true,
1163
+ sanitized: undefined,
1164
+ };
1165
+ }
1166
+ const items = trimmedValue
1167
+ .split(',')
1168
+ .map((item) => item.trim())
1169
+ .filter((item) => item !== '');
1170
+ for (let i = 0; i < items.length; i += 1) {
1171
+ const { result, sanitized } = CLIUtilityInitialize.normalizeUrl(items[i], protocol);
1172
+ if (result !== true) {
1173
+ const errorMessages = {
1174
+ generic: 'Enter a valid generic URL (e.g., https://) or remove entry.',
1175
+ repository: 'Enter a valid repository URL (e.g., git+https://) or remove entry.',
1176
+ };
1177
+ const errorMessage = (protocol === 'repository') ? errorMessages.repository : errorMessages.generic;
1178
+ return {
1179
+ result: `Invalid URL "${items[i]}": ${errorMessage}`,
1180
+ sanitized: undefined,
1181
+ };
1182
+ }
1183
+ if (sanitized !== undefined) {
1184
+ items[i] = sanitized;
1185
+ }
1186
+ }
1187
+ return {
1188
+ result: true,
1189
+ sanitized: items,
1190
+ };
1191
+ }
1192
+ static normalizeWorkspaceName(value, role, base) {
1193
+ if (typeof value !== 'string') {
1194
+ return {
1195
+ result: `Unexpected type error. Expect type to be "string", got "${typeof value}".`,
1196
+ sanitized: undefined,
1197
+ };
1198
+ }
1199
+ const trimmedValue = value.trim();
1200
+ if (trimmedValue === '') {
1201
+ return {
1202
+ result: 'Enter a package name.',
1203
+ sanitized: undefined,
1204
+ };
1205
+ }
1206
+ switch (role) {
1207
+ case 'config':
1208
+ case 'app':
1209
+ case 'tool': {
1210
+ const expectedPrefix = `${base}-`;
1211
+ if (!trimmedValue.startsWith(expectedPrefix)) {
1212
+ return {
1213
+ result: `Begin with "${expectedPrefix}" and add a descriptor slug.`,
1214
+ sanitized: undefined,
1215
+ };
1216
+ }
1217
+ const descriptor = trimmedValue.slice(expectedPrefix.length);
1218
+ if (descriptor.length === 0) {
1219
+ return {
1220
+ result: 'Add a descriptor after the prefix.',
1221
+ sanitized: undefined,
1222
+ };
1223
+ }
1224
+ if (!PATTERN_SLUG_SIMPLE.test(descriptor)) {
1225
+ return {
1226
+ result: 'Descriptor must match the slug pattern (lowercase letters, numbers, hyphens, underscores).',
1227
+ sanitized: undefined,
1228
+ };
1229
+ }
1230
+ return {
1231
+ result: true,
1232
+ sanitized: trimmedValue,
1233
+ };
1234
+ }
1235
+ case 'template':
1236
+ case 'package':
1237
+ default: {
1238
+ if (PATTERN_SLUG_SIMPLE.test(trimmedValue) || PATTERN_SLUG_SCOPED.test(trimmedValue)) {
1239
+ return {
1240
+ result: true,
1241
+ sanitized: trimmedValue,
1242
+ };
1243
+ }
1244
+ return {
1245
+ result: 'Enter an unscoped slug or a scoped package name (e.g. @scope/name).',
1246
+ sanitized: undefined,
1247
+ };
1248
+ }
981
1249
  }
982
1250
  }
983
1251
  }