@cbnventures/nova 0.11.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 (134) hide show
  1. package/build/eslint.config.d.ts +3 -0
  2. package/build/eslint.config.d.ts.map +1 -0
  3. package/build/eslint.config.js +8 -0
  4. package/build/eslint.config.js.map +1 -0
  5. package/build/package.json +131 -0
  6. package/build/src/cli/index.d.ts +3 -0
  7. package/build/src/cli/index.d.ts.map +1 -0
  8. package/build/src/cli/index.js +258 -0
  9. package/build/src/cli/index.js.map +1 -0
  10. package/build/src/cli/recipe/sync-metadata.d.ts +5 -0
  11. package/build/src/cli/recipe/sync-metadata.d.ts.map +1 -0
  12. package/build/src/cli/recipe/sync-metadata.js +6 -0
  13. package/build/src/cli/recipe/sync-metadata.js.map +1 -0
  14. package/build/src/cli/recipe/sync-versions.d.ts +5 -0
  15. package/build/src/cli/recipe/sync-versions.d.ts.map +1 -0
  16. package/build/src/cli/recipe/sync-versions.js +6 -0
  17. package/build/src/cli/recipe/sync-versions.js.map +1 -0
  18. package/build/src/cli/utility/initialize.d.ts +19 -0
  19. package/build/src/cli/utility/initialize.d.ts.map +1 -0
  20. package/build/src/cli/utility/initialize.js +962 -0
  21. package/build/src/cli/utility/initialize.js.map +1 -0
  22. package/build/src/cli/utility/version.d.ts +11 -0
  23. package/build/src/cli/utility/version.d.ts.map +1 -0
  24. package/build/src/cli/utility/version.js +324 -0
  25. package/build/src/cli/utility/version.js.map +1 -0
  26. package/build/src/lib/item.d.ts +7 -0
  27. package/build/src/lib/item.d.ts.map +1 -0
  28. package/build/src/lib/item.js +55 -0
  29. package/build/src/lib/item.js.map +1 -0
  30. package/build/src/lib/nova-config.d.ts +21 -0
  31. package/build/src/lib/nova-config.d.ts.map +1 -0
  32. package/build/src/lib/nova-config.js +327 -0
  33. package/build/src/lib/nova-config.js.map +1 -0
  34. package/build/src/lib/regex.d.ts +19 -0
  35. package/build/src/lib/regex.d.ts.map +1 -0
  36. package/build/src/lib/regex.js +19 -0
  37. package/build/src/lib/regex.js.map +1 -0
  38. package/build/src/lib/utility.d.ts +11 -0
  39. package/build/src/lib/utility.d.ts.map +1 -0
  40. package/build/src/lib/utility.js +278 -0
  41. package/build/src/lib/utility.js.map +1 -0
  42. package/build/src/presets/eslint/dx-code-style.d.mts +4 -0
  43. package/build/src/presets/eslint/dx-code-style.d.mts.map +1 -0
  44. package/build/src/presets/eslint/dx-code-style.mjs +231 -0
  45. package/build/src/presets/eslint/dx-code-style.mjs.map +1 -0
  46. package/build/src/presets/eslint/dx-ignore.d.mts +4 -0
  47. package/build/src/presets/eslint/dx-ignore.d.mts.map +1 -0
  48. package/build/src/presets/eslint/dx-ignore.mjs +21 -0
  49. package/build/src/presets/eslint/dx-ignore.mjs.map +1 -0
  50. package/build/src/presets/eslint/env-browser.d.mts +4 -0
  51. package/build/src/presets/eslint/env-browser.d.mts.map +1 -0
  52. package/build/src/presets/eslint/env-browser.mjs +3 -0
  53. package/build/src/presets/eslint/env-browser.mjs.map +1 -0
  54. package/build/src/presets/eslint/env-edge.d.mts +4 -0
  55. package/build/src/presets/eslint/env-edge.d.mts.map +1 -0
  56. package/build/src/presets/eslint/env-edge.mjs +3 -0
  57. package/build/src/presets/eslint/env-edge.mjs.map +1 -0
  58. package/build/src/presets/eslint/env-node.d.mts +4 -0
  59. package/build/src/presets/eslint/env-node.d.mts.map +1 -0
  60. package/build/src/presets/eslint/env-node.mjs +68 -0
  61. package/build/src/presets/eslint/env-node.mjs.map +1 -0
  62. package/build/src/presets/eslint/env-service-worker.d.mts +4 -0
  63. package/build/src/presets/eslint/env-service-worker.d.mts.map +1 -0
  64. package/build/src/presets/eslint/env-service-worker.mjs +3 -0
  65. package/build/src/presets/eslint/env-service-worker.mjs.map +1 -0
  66. package/build/src/presets/eslint/env-web-worker.d.mts +4 -0
  67. package/build/src/presets/eslint/env-web-worker.d.mts.map +1 -0
  68. package/build/src/presets/eslint/env-web-worker.mjs +3 -0
  69. package/build/src/presets/eslint/env-web-worker.mjs.map +1 -0
  70. package/build/src/presets/eslint/fw-docusaurus.d.mts +4 -0
  71. package/build/src/presets/eslint/fw-docusaurus.d.mts.map +1 -0
  72. package/build/src/presets/eslint/fw-docusaurus.mjs +10 -0
  73. package/build/src/presets/eslint/fw-docusaurus.mjs.map +1 -0
  74. package/build/src/presets/eslint/fw-expressjs.d.mts +4 -0
  75. package/build/src/presets/eslint/fw-expressjs.d.mts.map +1 -0
  76. package/build/src/presets/eslint/fw-expressjs.mjs +8 -0
  77. package/build/src/presets/eslint/fw-expressjs.mjs.map +1 -0
  78. package/build/src/presets/eslint/fw-nextjs.d.mts +4 -0
  79. package/build/src/presets/eslint/fw-nextjs.d.mts.map +1 -0
  80. package/build/src/presets/eslint/fw-nextjs.mjs +8 -0
  81. package/build/src/presets/eslint/fw-nextjs.mjs.map +1 -0
  82. package/build/src/presets/eslint/index.d.mts +16 -0
  83. package/build/src/presets/eslint/index.d.mts.map +1 -0
  84. package/build/src/presets/eslint/index.mjs +16 -0
  85. package/build/src/presets/eslint/index.mjs.map +1 -0
  86. package/build/src/presets/eslint/lang-javascript.d.mts +4 -0
  87. package/build/src/presets/eslint/lang-javascript.d.mts.map +1 -0
  88. package/build/src/presets/eslint/lang-javascript.mjs +3 -0
  89. package/build/src/presets/eslint/lang-javascript.mjs.map +1 -0
  90. package/build/src/presets/eslint/lang-mdx.d.mts +4 -0
  91. package/build/src/presets/eslint/lang-mdx.d.mts.map +1 -0
  92. package/build/src/presets/eslint/lang-mdx.mjs +45 -0
  93. package/build/src/presets/eslint/lang-mdx.mjs.map +1 -0
  94. package/build/src/presets/eslint/lang-typescript.d.mts +4 -0
  95. package/build/src/presets/eslint/lang-typescript.d.mts.map +1 -0
  96. package/build/src/presets/eslint/lang-typescript.mjs +88 -0
  97. package/build/src/presets/eslint/lang-typescript.mjs.map +1 -0
  98. package/build/src/presets/eslint/platform-cloudflare-workers.d.mts +4 -0
  99. package/build/src/presets/eslint/platform-cloudflare-workers.d.mts.map +1 -0
  100. package/build/src/presets/eslint/platform-cloudflare-workers.mjs +8 -0
  101. package/build/src/presets/eslint/platform-cloudflare-workers.mjs.map +1 -0
  102. package/build/src/presets/eslint/tool-vite.d.mts +4 -0
  103. package/build/src/presets/eslint/tool-vite.d.mts.map +1 -0
  104. package/build/src/presets/eslint/tool-vite.mjs +8 -0
  105. package/build/src/presets/eslint/tool-vite.mjs.map +1 -0
  106. package/build/src/presets/tsconfig/dx-essentials.json +24 -0
  107. package/build/src/presets/tsconfig/dx-strict.json +33 -0
  108. package/build/src/presets/tsconfig/env-browser.json +12 -0
  109. package/build/src/presets/tsconfig/env-edge.json +12 -0
  110. package/build/src/presets/tsconfig/env-node.json +10 -0
  111. package/build/src/presets/tsconfig/env-service-worker.json +12 -0
  112. package/build/src/presets/tsconfig/env-web-worker.json +12 -0
  113. package/build/src/presets/tsconfig/fw-docusaurus.json +15 -0
  114. package/build/src/presets/tsconfig/fw-expressjs.json +10 -0
  115. package/build/src/presets/tsconfig/fw-nextjs.json +14 -0
  116. package/build/src/presets/tsconfig/platform-cloudflare-workers.json +14 -0
  117. package/build/src/presets/tsconfig/tool-vite.json +14 -0
  118. package/build/src/toolkit/cli-header.d.ts +10 -0
  119. package/build/src/toolkit/cli-header.d.ts.map +1 -0
  120. package/build/src/toolkit/cli-header.js +164 -0
  121. package/build/src/toolkit/cli-header.js.map +1 -0
  122. package/build/src/toolkit/index.d.ts +4 -0
  123. package/build/src/toolkit/index.d.ts.map +1 -0
  124. package/build/src/toolkit/index.js +4 -0
  125. package/build/src/toolkit/index.js.map +1 -0
  126. package/build/src/toolkit/logger.d.ts +13 -0
  127. package/build/src/toolkit/logger.d.ts.map +1 -0
  128. package/build/src/toolkit/logger.js +99 -0
  129. package/build/src/toolkit/logger.js.map +1 -0
  130. package/build/src/toolkit/markdown-table.d.ts +14 -0
  131. package/build/src/toolkit/markdown-table.d.ts.map +1 -0
  132. package/build/src/toolkit/markdown-table.js +69 -0
  133. package/build/src/toolkit/markdown-table.js.map +1 -0
  134. package/package.json +131 -0
@@ -0,0 +1,962 @@
1
+ import * as path from 'path';
2
+ import chalk from 'chalk';
3
+ import prompts from 'prompts';
4
+ import { itemAllowedPoliciesByRole } from '../../lib/item.js';
5
+ import { NovaConfig } from '../../lib/nova-config.js';
6
+ import { PATTERN_EMAIL_SIMPLE, PATTERN_SLUG_SCOPED, PATTERN_SLUG_SIMPLE } from '../../lib/regex.js';
7
+ import { discoverPathsWithFile } from '../../lib/utility.js';
8
+ import { Logger } from '../../toolkit/index.js';
9
+ export class CLIUtilityInitialize {
10
+ static async run(options) {
11
+ const currentDirectory = process.cwd();
12
+ const isProjectRoot = await CLIUtilityInitialize.checkPath(currentDirectory);
13
+ if (!isProjectRoot) {
14
+ process.exitCode = 1;
15
+ return;
16
+ }
17
+ if (options.dryRun === true) {
18
+ Logger.customize({
19
+ name: 'CLIUtilityInitialize.run::options',
20
+ padBottom: 1,
21
+ }).warn('Dry run enabled. File changes will not be made in this session.');
22
+ }
23
+ const novaConfig = new NovaConfig();
24
+ const workingFile = await novaConfig.load();
25
+ const promptFlowResult = await CLIUtilityInitialize.promptFlow(workingFile);
26
+ if (promptFlowResult === 'cancel') {
27
+ Logger.customize({
28
+ name: 'CLIUtilityInitialize.run::promptFlow',
29
+ padTop: 1,
30
+ padBottom: 1,
31
+ }).debug('Prompt flow exited without saving.');
32
+ return;
33
+ }
34
+ novaConfig.set(workingFile);
35
+ if (options.dryRun === true) {
36
+ Logger.customize({
37
+ name: 'CLIUtilityInitialize.run::promptFlow',
38
+ padTop: 1,
39
+ padBottom: 1,
40
+ }).debug('Dry run enabled. Skipping save operation.');
41
+ return;
42
+ }
43
+ await novaConfig.save();
44
+ }
45
+ static async promptFlow(config) {
46
+ const category = {
47
+ project: {
48
+ label: 'Project',
49
+ description: 'Configure project metadata (name, description, keywords).',
50
+ handler: CLIUtilityInitialize.promptProject,
51
+ },
52
+ entities: {
53
+ label: 'Entities',
54
+ description: 'Manage entities, their roles, and contact information.',
55
+ handler: CLIUtilityInitialize.promptEntities,
56
+ },
57
+ urls: {
58
+ label: 'URLs',
59
+ description: 'Set URLs for docs, repo, support, and funding sources.',
60
+ handler: CLIUtilityInitialize.promptUrls,
61
+ },
62
+ workspaces: {
63
+ label: 'Workspaces',
64
+ description: 'Review workspace packages, assigning roles and policies.',
65
+ handler: CLIUtilityInitialize.promptWorkspaces,
66
+ },
67
+ };
68
+ while (true) {
69
+ const categoryKeys = Object.keys(category);
70
+ const choices = categoryKeys.map((categoryKey) => ({
71
+ title: category[categoryKey].label,
72
+ description: category[categoryKey].description,
73
+ value: categoryKey,
74
+ }));
75
+ choices.push({
76
+ title: 'Save & Exit',
77
+ description: 'Persist the "nova.config.json" file and exit.',
78
+ value: 'save',
79
+ });
80
+ choices.push({
81
+ title: 'Cancel',
82
+ description: 'Exit without persisting any changes.',
83
+ value: 'cancel',
84
+ });
85
+ const menuOutput = await CLIUtilityInitialize.promptWithCancel({
86
+ type: 'select',
87
+ name: 'action',
88
+ message: 'Select a Nova configuration category to edit.',
89
+ choices,
90
+ });
91
+ if (menuOutput.cancelled) {
92
+ return 'cancel';
93
+ }
94
+ const menuOutputResult = menuOutput.result;
95
+ if (menuOutputResult.action === undefined
96
+ || menuOutputResult.action === 'cancel') {
97
+ return 'cancel';
98
+ }
99
+ if (menuOutputResult.action === 'save') {
100
+ return 'save';
101
+ }
102
+ const categoryKey = menuOutputResult.action;
103
+ const categoryHandler = category[categoryKey].handler;
104
+ await categoryHandler(config);
105
+ }
106
+ }
107
+ static async promptProject(config) {
108
+ const existingProject = config.project;
109
+ const existingProjectName = existingProject?.name;
110
+ const existingProjectDescription = existingProject?.description;
111
+ const existingProjectKeywords = existingProject?.keywords;
112
+ const project = (existingProject !== undefined) ? { ...existingProject } : {};
113
+ const projectName = (existingProjectName !== undefined) ? { ...existingProjectName } : {};
114
+ const projectDescription = (existingProjectDescription !== undefined) ? { ...existingProjectDescription } : {};
115
+ const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
116
+ {
117
+ type: 'text',
118
+ name: 'projectNameTitle',
119
+ message: 'Project title (display name)',
120
+ initial: projectName.title ?? '',
121
+ },
122
+ {
123
+ type: 'text',
124
+ name: 'projectNameSlug',
125
+ message: 'Project slug (package name)',
126
+ initial: projectName.slug ?? '',
127
+ validate: (value) => {
128
+ const trimmed = value.trim();
129
+ if (trimmed === '' || new RegExp(PATTERN_SLUG_SIMPLE, 'i').test(trimmed)) {
130
+ return true;
131
+ }
132
+ return 'Use letters, numbers, hyphens, and underscores only.';
133
+ },
134
+ },
135
+ {
136
+ type: 'text',
137
+ name: 'projectDescriptionShort',
138
+ message: 'Short description',
139
+ initial: projectDescription.short ?? '',
140
+ },
141
+ {
142
+ type: 'text',
143
+ name: 'projectDescriptionLong',
144
+ message: 'Long description',
145
+ initial: projectDescription.long ?? '',
146
+ },
147
+ {
148
+ type: 'text',
149
+ name: 'projectKeywords',
150
+ message: 'Keywords (comma separated)',
151
+ initial: (Array.isArray(existingProjectKeywords)) ? existingProjectKeywords.join(', ') : '',
152
+ },
153
+ ]);
154
+ if (questionsOutput.cancelled) {
155
+ return 'back';
156
+ }
157
+ const questionsOutputResult = questionsOutput.result;
158
+ const projectNameTitleInput = (questionsOutputResult.projectNameTitle ?? '').trim();
159
+ const projectNameSlugInput = (questionsOutputResult.projectNameSlug ?? '').trim();
160
+ const projectDescriptionShortInput = (questionsOutputResult.projectDescriptionShort ?? '').trim();
161
+ const projectDescriptionLongInput = (questionsOutputResult.projectDescriptionLong ?? '').trim();
162
+ const projectKeywordsInput = (questionsOutputResult.projectKeywords ?? '').trim();
163
+ if (projectNameTitleInput === '') {
164
+ Reflect.deleteProperty(projectName, 'title');
165
+ }
166
+ else {
167
+ projectName.title = projectNameTitleInput;
168
+ }
169
+ if (projectNameSlugInput === '') {
170
+ Reflect.deleteProperty(projectName, 'slug');
171
+ }
172
+ else {
173
+ projectName.slug = projectNameSlugInput;
174
+ }
175
+ if (projectDescriptionShortInput === '') {
176
+ Reflect.deleteProperty(projectDescription, 'short');
177
+ }
178
+ else {
179
+ projectDescription.short = projectDescriptionShortInput;
180
+ }
181
+ if (projectDescriptionLongInput === '') {
182
+ Reflect.deleteProperty(projectDescription, 'long');
183
+ }
184
+ else {
185
+ projectDescription.long = projectDescriptionLongInput;
186
+ }
187
+ if (Object.keys(projectName).length > 0) {
188
+ project.name = projectName;
189
+ }
190
+ else {
191
+ Reflect.deleteProperty(project, 'name');
192
+ }
193
+ if (Object.keys(projectDescription).length > 0) {
194
+ project.description = projectDescription;
195
+ }
196
+ else {
197
+ Reflect.deleteProperty(project, 'description');
198
+ }
199
+ if (projectKeywordsInput === '') {
200
+ Reflect.deleteProperty(project, 'keywords');
201
+ }
202
+ else {
203
+ const projectKeywordsList = projectKeywordsInput
204
+ .split(',')
205
+ .map((projectKeywordInput) => projectKeywordInput.trim())
206
+ .filter((projectKeywordInput) => projectKeywordInput !== '');
207
+ if (projectKeywordsList.length > 0) {
208
+ project.keywords = projectKeywordsList;
209
+ }
210
+ else {
211
+ Reflect.deleteProperty(project, 'keywords');
212
+ }
213
+ }
214
+ if (Object.keys(project).length > 0) {
215
+ Object.assign(config, { project });
216
+ }
217
+ else {
218
+ Reflect.deleteProperty(config, 'project');
219
+ }
220
+ const previousSlug = existingProjectName?.slug ?? '';
221
+ const currentSlug = config.project?.name?.slug ?? '';
222
+ const slugChanged = previousSlug !== currentSlug;
223
+ if (slugChanged && config.workspaces !== undefined) {
224
+ const rolesToSync = ['project', 'docs', 'config', 'app', 'tool'];
225
+ const slugPrefix = new RegExp(`^${previousSlug}-`);
226
+ Logger.customize({
227
+ name: 'CLIUtilityInitialize.promptProject::updated',
228
+ padTop: 1,
229
+ }).info(`Project slug updated from "${previousSlug || '(unset)'}" to "${currentSlug || '(unset)'}".`);
230
+ for (const workspace of Object.values(config.workspaces)) {
231
+ if (!rolesToSync.includes(workspace.role)) {
232
+ continue;
233
+ }
234
+ const { name } = workspace;
235
+ if (previousSlug === '' && currentSlug !== '') {
236
+ workspace.name = `${currentSlug}-${name}`;
237
+ }
238
+ else if (previousSlug !== '' && currentSlug === '') {
239
+ workspace.name = name.replace(slugPrefix, '');
240
+ }
241
+ else {
242
+ workspace.name = name.replace(slugPrefix, `${currentSlug}-`);
243
+ }
244
+ Logger.customize({ name: 'CLIUtilityInitialize.promptProject::updated' }).info(`Workspace name updated from "${name}" to "${workspace.name}".`);
245
+ }
246
+ }
247
+ Logger.customize({
248
+ name: 'CLIUtilityInitialize.promptProject::updated',
249
+ padTop: (slugChanged && config.workspaces !== undefined) ? 0 : 1,
250
+ padBottom: 1,
251
+ }).info('Project details updated.');
252
+ return 'back';
253
+ }
254
+ static async promptEntities(config) {
255
+ const entities = [];
256
+ if (Array.isArray(config.entities)) {
257
+ for (const configEntity of config.entities) {
258
+ const clonedEntity = { ...configEntity };
259
+ if (Array.isArray(configEntity.roles)) {
260
+ clonedEntity.roles = [...configEntity.roles];
261
+ }
262
+ else {
263
+ Reflect.deleteProperty(clonedEntity, 'roles');
264
+ }
265
+ entities.push(clonedEntity);
266
+ }
267
+ }
268
+ const sync = () => {
269
+ if (entities.length > 0) {
270
+ const normalizedEntities = entities.map((entity) => {
271
+ const normalizedEntity = { ...entity };
272
+ if (Array.isArray(entity.roles) && entity.roles.length > 0) {
273
+ normalizedEntity.roles = [...entity.roles];
274
+ }
275
+ else {
276
+ Reflect.deleteProperty(normalizedEntity, 'roles');
277
+ }
278
+ return normalizedEntity;
279
+ });
280
+ Object.assign(config, { entities: normalizedEntities });
281
+ }
282
+ else {
283
+ Reflect.deleteProperty(config, 'entities');
284
+ }
285
+ };
286
+ while (true) {
287
+ const choices = [];
288
+ entities.forEach((entity, index) => {
289
+ const entityName = (entity.name !== undefined) ? entity.name.trim() : '';
290
+ const entityEmail = (entity.email !== undefined) ? entity.email.trim() : '';
291
+ const entityRoles = (Array.isArray(entity.roles)) ? entity.roles.filter((role) => role.trim() !== '') : [];
292
+ const label = entityName || entityEmail || `Entity ${index + 1}`;
293
+ const descriptionParts = [];
294
+ if (entityEmail !== '') {
295
+ descriptionParts.push(entityEmail);
296
+ }
297
+ if (entityRoles.length > 0) {
298
+ const normalizedRoles = entityRoles
299
+ .map((entityRole) => entityRole.trim())
300
+ .filter((entityRole) => entityRole.length > 0)
301
+ .reduce((unique, entityRole) => {
302
+ if (!unique.includes(entityRole)) {
303
+ unique.push(entityRole);
304
+ }
305
+ return unique;
306
+ }, []);
307
+ if (normalizedRoles.length > 0) {
308
+ descriptionParts.push(normalizedRoles.join(', '));
309
+ }
310
+ }
311
+ const description = descriptionParts.join(' · ');
312
+ choices.push({
313
+ title: `${chalk.yellow.bold('[EDIT]')} ${label}`,
314
+ description: (description !== '') ? description : 'Update this entity.',
315
+ value: {
316
+ kind: 'edit',
317
+ index,
318
+ },
319
+ });
320
+ choices.push({
321
+ title: `${chalk.red.bold('[REMOVE]')} ${label}`,
322
+ description: 'Delete this entity.',
323
+ value: {
324
+ kind: 'remove',
325
+ index,
326
+ },
327
+ });
328
+ });
329
+ choices.push({
330
+ title: 'Add new entity',
331
+ description: 'Create a new entity.',
332
+ value: {
333
+ kind: 'add',
334
+ },
335
+ });
336
+ choices.push({
337
+ title: 'Back',
338
+ description: 'Return to the category selection.',
339
+ value: {
340
+ kind: 'back',
341
+ },
342
+ });
343
+ const menuOutput = await CLIUtilityInitialize.promptWithCancel({
344
+ type: 'select',
345
+ name: 'action',
346
+ message: (entities.length > 0) ? 'Select an entity to manage.' : 'No entities found. Choose an option.',
347
+ choices,
348
+ });
349
+ if (menuOutput.cancelled) {
350
+ return 'back';
351
+ }
352
+ const menuOutputResult = menuOutput.result;
353
+ if (menuOutputResult.action === undefined
354
+ || menuOutputResult.action.kind === 'back') {
355
+ sync();
356
+ return 'back';
357
+ }
358
+ if (menuOutputResult.action.kind === 'add') {
359
+ const result = await CLIUtilityInitialize.promptEntitiesForm(undefined, 'create');
360
+ if (result.action === 'back') {
361
+ continue;
362
+ }
363
+ entities.push(result.entity);
364
+ sync();
365
+ Logger.customize({
366
+ name: 'CLIUtilityInitialize.promptEntities::add',
367
+ padTop: 1,
368
+ padBottom: 1,
369
+ }).info('Added new entity.');
370
+ continue;
371
+ }
372
+ if (menuOutputResult.action.kind === 'edit') {
373
+ const entityIndex = menuOutputResult.action.index;
374
+ if (entityIndex < 0 || entityIndex >= entities.length) {
375
+ continue;
376
+ }
377
+ const entityToEdit = entities[entityIndex];
378
+ const entityResult = await CLIUtilityInitialize.promptEntitiesForm(entityToEdit, 'update');
379
+ if (entityResult.action === 'back') {
380
+ continue;
381
+ }
382
+ entities[entityIndex] = entityResult.entity;
383
+ sync();
384
+ Logger.customize({
385
+ name: 'CLIUtilityInitialize.promptEntities::edit',
386
+ padTop: 1,
387
+ padBottom: 1,
388
+ }).info('Updated entity.');
389
+ continue;
390
+ }
391
+ if (menuOutputResult.action.kind === 'remove') {
392
+ const entityIndex = menuOutputResult.action.index;
393
+ if (entityIndex < 0 || entityIndex >= entities.length) {
394
+ continue;
395
+ }
396
+ const entityToRemove = entities[entityIndex];
397
+ if (entityToRemove === undefined) {
398
+ continue;
399
+ }
400
+ const entityName = (typeof entityToRemove.name === 'string') ? entityToRemove.name.trim() : '';
401
+ const entityEmail = (typeof entityToRemove.email === 'string') ? entityToRemove.email.trim() : '';
402
+ const entityLabel = entityName || entityEmail || `Entity ${entityIndex + 1}`;
403
+ const shouldRemove = await CLIUtilityInitialize.promptEntitiesDeleteForm(entityLabel);
404
+ if (!shouldRemove) {
405
+ continue;
406
+ }
407
+ entities.splice(entityIndex, 1);
408
+ sync();
409
+ Logger.customize({
410
+ name: 'CLIUtilityInitialize.promptEntities::remove',
411
+ padTop: 1,
412
+ padBottom: 1,
413
+ }).info('Removed entity.');
414
+ }
415
+ }
416
+ }
417
+ static async promptEntitiesForm(entity, mode) {
418
+ const validRoles = ['author', 'contributor', 'supporter'];
419
+ const existingName = (typeof entity?.name === 'string') ? entity.name : '';
420
+ const existingEmail = (typeof entity?.email === 'string') ? entity.email : '';
421
+ const existingUrl = (typeof entity?.url === 'string') ? entity.url : '';
422
+ let existingRoles = [];
423
+ if (Array.isArray(entity?.roles)) {
424
+ existingRoles = entity.roles.filter((role) => validRoles.includes(role));
425
+ }
426
+ const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
427
+ {
428
+ type: 'text',
429
+ name: 'entityName',
430
+ message: 'Entity name',
431
+ initial: existingName,
432
+ },
433
+ {
434
+ type: 'text',
435
+ name: 'entityEmail',
436
+ message: 'Entity email address',
437
+ initial: existingEmail,
438
+ validate: (value) => {
439
+ const trimmed = value.trim();
440
+ if (trimmed === '') {
441
+ return true;
442
+ }
443
+ if (PATTERN_EMAIL_SIMPLE.test(trimmed)) {
444
+ return true;
445
+ }
446
+ return 'Enter a valid email address or leave blank.';
447
+ },
448
+ },
449
+ {
450
+ type: 'text',
451
+ name: 'entityUrl',
452
+ message: 'Entity website',
453
+ initial: existingUrl,
454
+ validate: (value) => {
455
+ const trimmed = value.trim();
456
+ if (trimmed === '') {
457
+ return true;
458
+ }
459
+ if (CLIUtilityInitialize.isAllowedHttpUrl(trimmed)) {
460
+ return true;
461
+ }
462
+ return 'Enter a valid URL (http:// or https://) or leave blank.';
463
+ },
464
+ },
465
+ {
466
+ type: 'multiselect',
467
+ name: 'entityRoles',
468
+ message: 'Entity roles',
469
+ choices: validRoles.map((validRole) => ({
470
+ title: `${validRole.charAt(0).toUpperCase()}${validRole.slice(1)}`,
471
+ value: validRole,
472
+ selected: existingRoles.includes(validRole),
473
+ })),
474
+ },
475
+ ]);
476
+ if (questionsOutput.cancelled) {
477
+ return {
478
+ action: 'back',
479
+ };
480
+ }
481
+ const questionsOutputResult = questionsOutput.result;
482
+ const entityNameInput = (typeof questionsOutputResult.entityName === 'string') ? questionsOutputResult.entityName.trim() : '';
483
+ const entityEmailInput = (typeof questionsOutputResult.entityEmail === 'string') ? questionsOutputResult.entityEmail.trim() : '';
484
+ const entityUrlInput = (typeof questionsOutputResult.entityUrl === 'string') ? questionsOutputResult.entityUrl.trim() : '';
485
+ const entityRolesInput = Array.isArray(questionsOutputResult.entityRoles) ? [...questionsOutputResult.entityRoles] : [];
486
+ const resolvedEntity = {};
487
+ if (entityNameInput !== '') {
488
+ resolvedEntity.name = entityNameInput;
489
+ }
490
+ if (entityEmailInput !== '') {
491
+ resolvedEntity.email = entityEmailInput;
492
+ }
493
+ if (entityUrlInput !== '') {
494
+ resolvedEntity.url = entityUrlInput;
495
+ }
496
+ if (entityRolesInput.length > 0) {
497
+ resolvedEntity.roles = entityRolesInput;
498
+ }
499
+ if (mode === 'create' && Object.keys(resolvedEntity).length < 1) {
500
+ return {
501
+ action: 'back',
502
+ };
503
+ }
504
+ return {
505
+ action: 'apply',
506
+ entity: resolvedEntity,
507
+ };
508
+ }
509
+ static async promptEntitiesDeleteForm(label) {
510
+ const confirmOutput = await CLIUtilityInitialize.promptWithCancel({
511
+ type: 'confirm',
512
+ name: 'confirm',
513
+ message: `Remove entity "${label}"?`,
514
+ initial: false,
515
+ });
516
+ if (confirmOutput.cancelled) {
517
+ return false;
518
+ }
519
+ const confirmOutputResult = confirmOutput.result;
520
+ return confirmOutputResult.confirm;
521
+ }
522
+ static async promptUrls(config) {
523
+ const existingUrls = config.urls;
524
+ const urls = (existingUrls !== undefined) ? { ...existingUrls } : {};
525
+ const validatedUrls = {};
526
+ const validate = (key, input) => {
527
+ const field = (key === 'repository') ? 'repository' : undefined;
528
+ const sanitizedUrl = CLIUtilityInitialize.sanitizeHttpUrl(input, field);
529
+ if (sanitizedUrl !== undefined) {
530
+ validatedUrls[key] = sanitizedUrl;
531
+ }
532
+ };
533
+ const questionsOutput = await CLIUtilityInitialize.promptWithCancel([
534
+ {
535
+ type: 'text',
536
+ name: 'urlsHomepage',
537
+ message: 'Homepage URL',
538
+ initial: urls.homepage ?? '',
539
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
540
+ },
541
+ {
542
+ type: 'text',
543
+ name: 'urlsRepository',
544
+ message: 'Repository URL',
545
+ initial: urls.repository ?? '',
546
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value, 'repository'),
547
+ },
548
+ {
549
+ type: 'text',
550
+ name: 'urlsBugs',
551
+ message: 'Issue tracker URL',
552
+ initial: urls.bugs ?? '',
553
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
554
+ },
555
+ {
556
+ type: 'text',
557
+ name: 'urlsLicense',
558
+ message: 'License URL',
559
+ initial: urls.license ?? '',
560
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
561
+ },
562
+ {
563
+ type: 'text',
564
+ name: 'urlsLogo',
565
+ message: 'Logo URL',
566
+ initial: urls.logo ?? '',
567
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
568
+ },
569
+ {
570
+ type: 'text',
571
+ name: 'urlsDocumentation',
572
+ message: 'Documentation URL',
573
+ initial: urls.documentation ?? '',
574
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
575
+ },
576
+ {
577
+ type: 'text',
578
+ name: 'urlsGithub',
579
+ message: 'GitHub URL',
580
+ initial: urls.github ?? '',
581
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
582
+ },
583
+ {
584
+ type: 'text',
585
+ name: 'urlsNpm',
586
+ message: 'npm package URL',
587
+ initial: urls.npm ?? '',
588
+ validate: (value) => CLIUtilityInitialize.validateHttpUrl(value),
589
+ },
590
+ {
591
+ type: 'text',
592
+ name: 'urlsFundSources',
593
+ message: 'Funding URLs (comma separated)',
594
+ initial: (Array.isArray(urls.fundSources)) ? urls.fundSources.join(', ') : '',
595
+ validate: CLIUtilityInitialize.validateFundSources,
596
+ },
597
+ ]);
598
+ if (questionsOutput.cancelled) {
599
+ return 'back';
600
+ }
601
+ const questionsOutputResult = questionsOutput.result;
602
+ validate('homepage', questionsOutputResult.urlsHomepage);
603
+ validate('repository', questionsOutputResult.urlsRepository);
604
+ validate('bugs', questionsOutputResult.urlsBugs);
605
+ validate('license', questionsOutputResult.urlsLicense);
606
+ validate('logo', questionsOutputResult.urlsLogo);
607
+ validate('documentation', questionsOutputResult.urlsDocumentation);
608
+ validate('github', questionsOutputResult.urlsGithub);
609
+ validate('npm', questionsOutputResult.urlsNpm);
610
+ const fundSourcesParts = questionsOutputResult.urlsFundSources
611
+ .split(',')
612
+ .map((fundSourceInput) => fundSourceInput.trim())
613
+ .filter((fundSourceInput) => fundSourceInput !== '');
614
+ if (fundSourcesParts.length > 0) {
615
+ const fundSourcesList = [];
616
+ for (const fundSourcesPart of fundSourcesParts) {
617
+ const sanitizedUrl = CLIUtilityInitialize.sanitizeHttpUrl(fundSourcesPart, 'generic');
618
+ if (sanitizedUrl !== undefined) {
619
+ fundSourcesList.push(sanitizedUrl);
620
+ }
621
+ }
622
+ if (fundSourcesList.length > 0) {
623
+ validatedUrls.fundSources = fundSourcesList;
624
+ }
625
+ }
626
+ if (Object.keys(validatedUrls).length > 0) {
627
+ Object.assign(config, { urls: validatedUrls });
628
+ }
629
+ else {
630
+ Reflect.deleteProperty(config, 'urls');
631
+ }
632
+ Logger.customize({
633
+ name: 'CLIUtilityInitialize.promptUrls::updated',
634
+ padTop: 1,
635
+ padBottom: 1,
636
+ }).info('URL references updated.');
637
+ return 'back';
638
+ }
639
+ static async promptWorkspaces(config) {
640
+ const workspaces = (config.workspaces) ? { ...(config.workspaces) } : {};
641
+ const rawWorkspacePaths = await discoverPathsWithFile('package.json', 'forward');
642
+ const workspacePaths = rawWorkspacePaths.map((rawWorkspacePath) => {
643
+ const relativePath = path.relative(process.cwd(), rawWorkspacePath);
644
+ if (relativePath === '') {
645
+ return './';
646
+ }
647
+ return `./${relativePath.split(path.sep).join('/')}`;
648
+ });
649
+ Logger.customize({ name: 'CLIUtilityInitialize.promptWorkspaces::paths' }).debug(workspacePaths);
650
+ while (true) {
651
+ const choices = workspacePaths.map((workspacePath) => {
652
+ const workspace = workspaces[workspacePath];
653
+ const summaryParts = [];
654
+ if (workspace.name) {
655
+ summaryParts.push(workspace.name);
656
+ }
657
+ if (workspace.role) {
658
+ summaryParts.push(workspace.role);
659
+ }
660
+ if (workspace.policy) {
661
+ summaryParts.push(workspace.policy);
662
+ }
663
+ return {
664
+ title: workspacePath,
665
+ description: (summaryParts.length > 0) ? summaryParts.join(' · ') : 'Not configured yet.',
666
+ value: workspacePath,
667
+ };
668
+ });
669
+ choices.push({
670
+ title: 'Back',
671
+ description: 'Return to the category selection.',
672
+ value: 'back',
673
+ });
674
+ const menuOutput = await CLIUtilityInitialize.promptWithCancel({
675
+ type: 'select',
676
+ name: 'workspacePath',
677
+ message: 'Select a workspace to configure.',
678
+ choices,
679
+ });
680
+ if (menuOutput.cancelled) {
681
+ return 'back';
682
+ }
683
+ const menuOutputResult = menuOutput.result;
684
+ if (menuOutputResult.workspacePath === undefined
685
+ || menuOutputResult.workspacePath === 'back') {
686
+ return 'back';
687
+ }
688
+ const workspacePath = menuOutputResult.workspacePath;
689
+ const formResult = await CLIUtilityInitialize.promptWorkspacesForm({
690
+ workspacePath,
691
+ existingWorkspace: workspaces[workspacePath],
692
+ projectSlug: config.project?.name?.slug,
693
+ });
694
+ if (formResult.action === 'back') {
695
+ continue;
696
+ }
697
+ workspaces[workspacePath] = formResult.workspace;
698
+ Object.assign(config, { workspaces });
699
+ Logger.customize({
700
+ name: 'CLIUtilityInitialize.promptWorkspaces::updated',
701
+ padTop: 1,
702
+ padBottom: 1,
703
+ }).info(`Updated workspace "${workspacePath}" → ${formResult.workspace.name} · ${formResult.workspace.role} · ${formResult.workspace.policy}`);
704
+ }
705
+ }
706
+ static async promptWorkspacesForm(options) {
707
+ const allowedRoles = [
708
+ {
709
+ title: 'Project Root',
710
+ description: 'Monorepo root workspace (e.g., root package.json)',
711
+ value: 'project',
712
+ },
713
+ {
714
+ title: 'Configuration',
715
+ description: 'Shared static config (e.g., eslint, tsconfig, prettier)',
716
+ value: 'config',
717
+ },
718
+ {
719
+ title: 'Documentation',
720
+ description: 'Documentation workspace (e.g., Docusaurus, Docsify)',
721
+ value: 'docs',
722
+ },
723
+ {
724
+ title: 'Application',
725
+ description: 'Deployable runtimes (e.g., web, mobile, workers, API, bots)',
726
+ value: 'app',
727
+ },
728
+ {
729
+ title: 'Package',
730
+ description: 'Reusable modules (e.g., libraries, plugins, components)',
731
+ value: 'package',
732
+ },
733
+ {
734
+ title: 'Tool',
735
+ description: 'Internal CLI or build tools (e.g., codegen, bundler)',
736
+ value: 'tool',
737
+ },
738
+ ];
739
+ const policy = {
740
+ freezable: {
741
+ label: 'Freezable',
742
+ description: 'Non-versioned (0.0.0) and never published (private: true)',
743
+ },
744
+ trackable: {
745
+ label: 'Trackable',
746
+ description: 'Versioned (>=0.0.1) and never published (private: true)',
747
+ },
748
+ distributable: {
749
+ label: 'Distributable',
750
+ description: 'Versioned (>=0.0.1) and published (private: false)',
751
+ },
752
+ };
753
+ const resolveName = async (role) => {
754
+ if (role === 'project' || role === 'docs') {
755
+ if (options.projectSlug === undefined) {
756
+ return (role === 'project') ? 'project' : 'docs';
757
+ }
758
+ return `${options.projectSlug}-${role}`;
759
+ }
760
+ const base = (options.projectSlug !== undefined) ? `${options.projectSlug}-${role}` : role;
761
+ const namePrompt = await CLIUtilityInitialize.promptWithCancel({
762
+ type: 'text',
763
+ name: 'workspaceName',
764
+ message: 'Workspace package name',
765
+ initial: options.existingWorkspace.name,
766
+ validate: (value) => {
767
+ const trimmed = value.trim();
768
+ if (trimmed === '') {
769
+ return 'Enter a package name.';
770
+ }
771
+ switch (role) {
772
+ case 'config':
773
+ case 'app':
774
+ case 'tool':
775
+ const expectedPrefix = `${base}-`;
776
+ if (!trimmed.startsWith(expectedPrefix)) {
777
+ return `Begin with "${expectedPrefix}" and add a descriptor slug.`;
778
+ }
779
+ const descriptor = trimmed.slice(expectedPrefix.length);
780
+ if (descriptor.length === 0) {
781
+ return 'Add a descriptor after the prefix.';
782
+ }
783
+ if (!PATTERN_SLUG_SIMPLE.test(descriptor)) {
784
+ return 'Descriptor must match the slug pattern (lowercase letters, numbers, hyphens, underscores).';
785
+ }
786
+ return true;
787
+ case 'package':
788
+ default:
789
+ if (PATTERN_SLUG_SIMPLE.test(trimmed) || PATTERN_SLUG_SCOPED.test(trimmed)) {
790
+ return true;
791
+ }
792
+ return 'Enter an unscoped slug or a scoped package name (e.g. @scope/name).';
793
+ }
794
+ },
795
+ });
796
+ if (namePrompt.cancelled) {
797
+ return undefined;
798
+ }
799
+ const name = namePrompt.result.workspaceName.trim();
800
+ return (name === '') ? undefined : name;
801
+ };
802
+ const rolePrompt = await CLIUtilityInitialize.promptWithCancel({
803
+ type: 'select',
804
+ name: 'workspaceRole',
805
+ message: `Select a role for "${options.workspacePath}"`,
806
+ choices: allowedRoles.map((role) => ({
807
+ title: role.title,
808
+ description: role.description,
809
+ value: role.value,
810
+ })),
811
+ initial: Math.max(0, allowedRoles.findIndex((role) => role.value === options.existingWorkspace.role)),
812
+ });
813
+ if (rolePrompt.cancelled) {
814
+ return {
815
+ action: 'back',
816
+ };
817
+ }
818
+ const selectedRole = rolePrompt.result.workspaceRole;
819
+ const allowedPolicies = itemAllowedPoliciesByRole[selectedRole];
820
+ const policyPrompt = await CLIUtilityInitialize.promptWithCancel({
821
+ type: 'select',
822
+ name: 'workspacePolicy',
823
+ message: 'Select a policy',
824
+ choices: allowedPolicies.map((allowedPolicy) => ({
825
+ title: policy[allowedPolicy].label,
826
+ description: policy[allowedPolicy].description,
827
+ value: allowedPolicy,
828
+ })),
829
+ initial: Math.max(0, allowedPolicies.findIndex((policy) => policy === options.existingWorkspace.policy)),
830
+ });
831
+ if (policyPrompt.cancelled) {
832
+ return {
833
+ action: 'back',
834
+ };
835
+ }
836
+ const selectedPolicy = policyPrompt.result.workspacePolicy;
837
+ const resolvedName = await resolveName(selectedRole);
838
+ if (resolvedName === undefined) {
839
+ return {
840
+ action: 'back',
841
+ };
842
+ }
843
+ return {
844
+ action: 'apply',
845
+ workspace: {
846
+ name: resolvedName,
847
+ role: selectedRole,
848
+ policy: selectedPolicy,
849
+ },
850
+ };
851
+ }
852
+ static async promptWithCancel(questions) {
853
+ let cancelled = false;
854
+ const result = await prompts(questions, {
855
+ onCancel: () => {
856
+ cancelled = true;
857
+ return false;
858
+ },
859
+ });
860
+ if (cancelled) {
861
+ return {
862
+ cancelled: true,
863
+ };
864
+ }
865
+ return {
866
+ cancelled: false,
867
+ result,
868
+ };
869
+ }
870
+ static async checkPath(currentDirectory) {
871
+ const locations = await discoverPathsWithFile('package.json', 'backward');
872
+ Logger.customize({ name: 'CLIUtilityInitialize.checkPath::detectedLocations' }).debug(locations);
873
+ if (locations.length < 1) {
874
+ Logger.customize({
875
+ name: 'CLIUtilityInitialize.checkPath::lessThanOne',
876
+ padBottom: 1,
877
+ }).error([
878
+ 'No "package.json" files were found. Re-run this command inside the project root directory.',
879
+ `Current directory is "${currentDirectory}"`,
880
+ ].join('\n'));
881
+ return false;
882
+ }
883
+ if (locations.length > 1) {
884
+ Logger.customize({
885
+ name: 'CLIUtilityInitialize.checkPath::greaterThanOne',
886
+ padBottom: 1,
887
+ }).error([
888
+ 'Multiple "package.json" files were found. Re-run this command inside the project root directory.',
889
+ `Current directory is "${currentDirectory}"`,
890
+ ].join('\n'));
891
+ return false;
892
+ }
893
+ if (locations.length === 1 && locations[0] !== currentDirectory) {
894
+ Logger.customize({
895
+ name: 'CLIUtilityInitialize.checkPath::notProjectRootDir',
896
+ padBottom: 1,
897
+ }).error([
898
+ 'Must be run inside the project root directory.',
899
+ `Current directory is "${currentDirectory}"`,
900
+ ].join('\n'));
901
+ return false;
902
+ }
903
+ return true;
904
+ }
905
+ static validateHttpUrl(value, field) {
906
+ const trimmed = value.trim();
907
+ if (trimmed === '') {
908
+ return true;
909
+ }
910
+ if (CLIUtilityInitialize.isAllowedHttpUrl(trimmed, field)) {
911
+ return true;
912
+ }
913
+ return 'Enter a valid URL (http:// or https://) or leave blank.';
914
+ }
915
+ static validateFundSources(value) {
916
+ const trimmed = value.trim();
917
+ if (trimmed === '') {
918
+ return true;
919
+ }
920
+ const parts = trimmed
921
+ .split(',')
922
+ .map((part) => part.trim())
923
+ .filter((part) => part !== '');
924
+ for (const part of parts) {
925
+ if (!CLIUtilityInitialize.isAllowedHttpUrl(part, 'generic')) {
926
+ return 'Enter comma separated URLs (http:// or https://) or leave blank.';
927
+ }
928
+ }
929
+ return true;
930
+ }
931
+ static sanitizeHttpUrl(value, field) {
932
+ if (typeof value !== 'string') {
933
+ return undefined;
934
+ }
935
+ const trimmed = value.trim();
936
+ if (trimmed === '') {
937
+ return undefined;
938
+ }
939
+ try {
940
+ const parsedUrl = new URL(trimmed);
941
+ if (CLIUtilityInitialize.isAllowedHttpUrl(parsedUrl.toString(), field)) {
942
+ return parsedUrl.toString();
943
+ }
944
+ }
945
+ catch {
946
+ }
947
+ return undefined;
948
+ }
949
+ static isAllowedHttpUrl(value, field) {
950
+ try {
951
+ const url = new URL(value);
952
+ const genericProtocols = ['http:', 'https:'];
953
+ const repositoryProtocols = ['git:', 'git+https:', 'git+ssh:', 'git+http:', 'https:', 'http:'];
954
+ const allowedProtocols = (field === 'repository') ? repositoryProtocols : genericProtocols;
955
+ return allowedProtocols.includes(url.protocol);
956
+ }
957
+ catch {
958
+ return false;
959
+ }
960
+ }
961
+ }
962
+ //# sourceMappingURL=initialize.js.map