@wpmoo/odoo 0.8.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,703 @@
1
+ #!/usr/bin/env node
2
+ import { confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts';
3
+ import { resolve } from 'node:path';
4
+ import { commandFromArgs, defaultTargetForProduct, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
5
+ import { detectDevelopmentEnvironment } from './environment.js';
6
+ import { commandOdooVersion } from './environment-version.js';
7
+ import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
8
+ import { getOriginUrl, realGit } from './git.js';
9
+ import { renderHelp } from './help.js';
10
+ import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
11
+ import { supportedOdooVersions } from './odoo-versions.js';
12
+ import { renderRepositorySetupNote } from './prompt-copy.js';
13
+ import { promptRepositoryUrl } from './prompt-repositories.js';
14
+ import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-url.js';
15
+ import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
16
+ import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
17
+ import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, repositoryPreflightAvailable, } from './repository-preflight.js';
18
+ import { scaffold } from './scaffold.js';
19
+ import { renderBanner } from './templates.js';
20
+ import { checkForUpdate, installLatestPackage, isUpdateCheckSkipped, restartCli } from './update-check.js';
21
+ import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
22
+ import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
23
+ import { environmentGitHubOwner } from './environment-context.js';
24
+ import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
25
+ function handleCancel(value, action) {
26
+ handlePromptCancel(isCancel(value), action);
27
+ }
28
+ function showSubmenuIntro(title, showIntro, cancelAction) {
29
+ if (showIntro) {
30
+ intro(menuIntroTitle(title, cancelAction));
31
+ }
32
+ }
33
+ function asString(value, fallback, cancelAction = 'exit') {
34
+ handleCancel(value, cancelAction);
35
+ return typeof value === 'string' && value.trim() ? value.trim() : fallback;
36
+ }
37
+ function githubAccountLabel(account) {
38
+ return account.type === 'user' ? `${account.login} (personal)` : `${account.login} (organization)`;
39
+ }
40
+ async function selectDefaultGitHubOwner(cancelAction = 'exit', preferredOwner) {
41
+ try {
42
+ const accounts = await getGitHubAccounts(realGitHub);
43
+ if (accounts.length === 0) {
44
+ return preferredOwner;
45
+ }
46
+ if (accounts.length === 1) {
47
+ return accounts[0].login;
48
+ }
49
+ const initialValue = accounts.some((account) => account.login === preferredOwner)
50
+ ? preferredOwner
51
+ : accounts[0].login;
52
+ const selectedOwner = await select({
53
+ message: 'GitHub account/organization',
54
+ options: accounts.map((account) => ({
55
+ value: account.login,
56
+ label: githubAccountLabel(account),
57
+ })),
58
+ initialValue,
59
+ });
60
+ handleCancel(selectedOwner, cancelAction);
61
+ return String(selectedOwner);
62
+ }
63
+ catch (error) {
64
+ if (isMenuBackSignal(error))
65
+ throw error;
66
+ return preferredOwner;
67
+ }
68
+ }
69
+ function stringOption(values, key) {
70
+ const value = values[key];
71
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
72
+ }
73
+ function booleanOption(values, key, fallback) {
74
+ const value = values[key];
75
+ if (value === undefined)
76
+ return fallback;
77
+ if (typeof value === 'boolean')
78
+ return value;
79
+ const normalized = value.toLowerCase().trim();
80
+ if (['true', '1', 'yes', 'y'].includes(normalized))
81
+ return true;
82
+ if (['false', '0', 'no', 'n'].includes(normalized))
83
+ return false;
84
+ throw new Error(`Invalid boolean value for --${key}: ${value}`);
85
+ }
86
+ function validateRepoName(value) {
87
+ const normalized = value.trim();
88
+ if (!normalized)
89
+ return 'Enter a repository name.';
90
+ if (normalized.includes('/') || normalized.includes(':'))
91
+ return 'Enter only the repository name, not a URL.';
92
+ return undefined;
93
+ }
94
+ async function showStartup(argv, skipUpdateCheck) {
95
+ console.log(renderBanner());
96
+ if (skipUpdateCheck) {
97
+ console.log(renderVersionTag());
98
+ console.log();
99
+ return;
100
+ }
101
+ const updateCheck = await checkForUpdate(packageName(), packageVersion());
102
+ console.log(renderVersionTag(updateCheck.status === 'update-available' ? updateCheck.latestVersion : undefined));
103
+ if (updateCheck.status === 'update-available') {
104
+ const shouldUpdate = await confirm({
105
+ message: `Update to v.${updateCheck.latestVersion}? (Y/n)`,
106
+ active: 'Y',
107
+ inactive: 'n',
108
+ initialValue: true,
109
+ });
110
+ handleCancel(shouldUpdate, 'exit');
111
+ if (shouldUpdate) {
112
+ try {
113
+ await installLatestPackage(packageName(), updateCheck.latestVersion);
114
+ const code = await restartCli(packageName(), updateCheck.latestVersion, argv);
115
+ if (code === 0) {
116
+ process.exit(0);
117
+ }
118
+ console.warn(`Update restart exited with code ${code ?? 'unknown'}; continuing with v.${packageVersion()}.`);
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ console.warn(`Update failed: ${message}. Continuing with v.${packageVersion()}.`);
123
+ }
124
+ }
125
+ }
126
+ console.log();
127
+ }
128
+ async function selectEnvironmentActionFromMenu() {
129
+ intro('WPMoo Odoo Dev');
130
+ const action = await select({
131
+ message: 'What do you want to do?',
132
+ options: [
133
+ { value: 'add-repo', label: 'Add source repo' },
134
+ { value: 'remove-repo', label: 'Remove source repo' },
135
+ { value: 'add-module', label: 'Add module to source repo' },
136
+ { value: 'remove-module', label: 'Remove module from source repo' },
137
+ { value: 'reset', label: 'Safe reset environment' },
138
+ { value: 'exit', label: 'Exit' },
139
+ ],
140
+ initialValue: 'add-module',
141
+ });
142
+ handleCancel(action, 'back');
143
+ return action;
144
+ }
145
+ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
146
+ if (showIntro) {
147
+ intro('Create Odoo dev environment');
148
+ }
149
+ const product = asString(await text({
150
+ message: 'Product slug',
151
+ placeholder: 'odoo_sample_module',
152
+ validate: (value) => (value.trim() ? undefined : 'Enter a product/module slug.'),
153
+ }), 'odoo_sample_module', cancelAction);
154
+ const target = defaultTargetForProduct(product);
155
+ note(renderRepositorySetupNote(product), 'Repository setup');
156
+ const selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
157
+ const selectedVersion = await select({
158
+ message: menuPromptMessage('Odoo version', cancelAction),
159
+ options: supportedOdooVersions.map((version) => ({ value: version, label: version })),
160
+ initialValue: supportedOdooVersions[0],
161
+ });
162
+ handleCancel(selectedVersion, cancelAction);
163
+ const odooVersion = String(selectedVersion);
164
+ const detectedDevRepoUrl = await getOriginUrl(realGit, target);
165
+ const defaultDevRepoUrl = selectedGitHubOwner
166
+ ? githubRepositoryUrl(selectedGitHubOwner, `${product}_dev`)
167
+ : undefined;
168
+ const devRepoUrl = normalizeRepositoryUrl(await promptRepositoryUrl({
169
+ label: 'Dev environment repo URL',
170
+ suggestedUrl: detectedDevRepoUrl ?? defaultDevRepoUrl,
171
+ placeholder: `https://github.com/your-account/${product}_dev.git`,
172
+ cancelAction,
173
+ }));
174
+ const defaultOwner = inferGitHubOwner(devRepoUrl) ?? selectedGitHubOwner;
175
+ const sourceRepos = [];
176
+ let addAnother = true;
177
+ while (addAnother) {
178
+ const repoIndex = sourceRepos.length;
179
+ const suggestedRepo = defaultOwner === undefined
180
+ ? undefined
181
+ : githubRepositoryUrl(defaultOwner, repoIndex === 0 ? product : `${product}_${repoIndex + 1}`);
182
+ const sourceRepoUrl = normalizeRepositoryUrl(await promptRepositoryUrl({
183
+ label: repoIndex === 0 ? 'Source repo URL' : `Additional source repo ${repoIndex + 1} URL`,
184
+ suggestedUrl: suggestedRepo,
185
+ placeholder: `https://github.com/owner/${repoIndex === 0 ? product : `${product}_${repoIndex + 1}`}.git`,
186
+ cancelAction,
187
+ }));
188
+ const sourcePath = inferRepoPath(sourceRepoUrl);
189
+ sourceRepos.push({
190
+ url: sourceRepoUrl,
191
+ path: sourcePath,
192
+ addons: [sourcePath],
193
+ });
194
+ const shouldAddAnother = await select({
195
+ message: 'Add another source repo?',
196
+ options: [
197
+ { value: false, label: 'No' },
198
+ { value: true, label: 'Yes' },
199
+ ],
200
+ initialValue: false,
201
+ });
202
+ handleCancel(shouldAddAnother, cancelAction);
203
+ addAnother = Boolean(shouldAddAnother);
204
+ }
205
+ const installAgentSkills = await select({
206
+ message: 'Install project-local Odoo Agent Skills?',
207
+ options: [
208
+ { value: true, label: 'Yes, install latest default skills' },
209
+ { value: false, label: 'No' },
210
+ ],
211
+ initialValue: false,
212
+ });
213
+ handleCancel(installAgentSkills, cancelAction);
214
+ const initEmpty = await select({
215
+ message: 'Initialize repositories that exist but have no commits?',
216
+ options: [
217
+ { value: true, label: 'Yes, create the selected Odoo branch' },
218
+ { value: false, label: 'No, fail with instructions' },
219
+ ],
220
+ initialValue: true,
221
+ });
222
+ handleCancel(initEmpty, cancelAction);
223
+ return {
224
+ product,
225
+ odooVersion,
226
+ engine: 'compose',
227
+ devRepo: inferRepoPath(devRepoUrl),
228
+ devRepoUrl,
229
+ sourceRepos,
230
+ target,
231
+ dryRun: false,
232
+ initEmptyRepos: Boolean(initEmpty),
233
+ stage: true,
234
+ agentSkillsTemplateUrl: Boolean(installAgentSkills) ? defaultAgentSkillsTemplateUrl : undefined,
235
+ createMissingRepos: false,
236
+ repoVisibility: 'private',
237
+ };
238
+ }
239
+ async function addRepoOptionsFromArgs(argv) {
240
+ const { values } = parseArgs(argv);
241
+ const repoUrl = stringOption(values, 'repoUrl') ?? stringOption(values, 'sourceRepoUrl');
242
+ if (!repoUrl) {
243
+ return undefined;
244
+ }
245
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
246
+ return {
247
+ target,
248
+ repoUrl: normalizeRepositoryUrl(repoUrl),
249
+ repoPath: stringOption(values, 'repo') ?? stringOption(values, 'sourcePath'),
250
+ odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
251
+ initEmptyRepos: booleanOption(values, 'initEmptyRepos', false),
252
+ stage: booleanOption(values, 'stage', true),
253
+ };
254
+ }
255
+ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
256
+ showSubmenuIntro('Add source repo as submodule', showIntro, cancelAction);
257
+ const target = process.cwd();
258
+ const odooVersion = await commandOdooVersion(target);
259
+ const preferredOwner = await environmentGitHubOwner(target);
260
+ const selectedOwner = await selectDefaultGitHubOwner(cancelAction, preferredOwner);
261
+ const owner = selectedOwner ??
262
+ asString(await text({
263
+ message: menuPromptMessage('GitHub owner/organization', cancelAction),
264
+ placeholder: 'example-org',
265
+ validate: (value) => (value.trim() ? undefined : 'Enter a GitHub owner or organization.'),
266
+ }), 'example-org', cancelAction);
267
+ const repoName = asString(await text({
268
+ message: menuPromptMessage('Source repo name', cancelAction),
269
+ placeholder: 'odoo_sample_module_repo',
270
+ validate: validateRepoName,
271
+ }), 'odoo_sample_module_repo', cancelAction);
272
+ const repoUrl = githubRepositoryUrl(owner, repoName);
273
+ return {
274
+ target,
275
+ repoUrl,
276
+ odooVersion,
277
+ initEmptyRepos: true,
278
+ stage: true,
279
+ };
280
+ }
281
+ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
282
+ if (!(await repositoryPreflightAvailable())) {
283
+ note([
284
+ 'GitHub CLI (`gh`) is not available or not authenticated.',
285
+ 'The source repo will be used as-is. If it does not exist, create it first or authenticate gh.',
286
+ ].join('\n'), 'Repository check skipped');
287
+ return;
288
+ }
289
+ const status = await getGitHubRepositoryStatus(realGitHub, options.repoUrl);
290
+ if (status.status !== 'inaccessible') {
291
+ return;
292
+ }
293
+ note(`Source repo is not accessible: ${status.slug}`, 'Repository check');
294
+ const shouldCreate = await select({
295
+ message: 'Create this source repository with GitHub CLI?',
296
+ options: [
297
+ { value: true, label: 'Yes, create it' },
298
+ { value: false, label: 'No, I will create/check access myself' },
299
+ ],
300
+ initialValue: true,
301
+ });
302
+ handleCancel(shouldCreate, cancelAction);
303
+ if (!shouldCreate) {
304
+ throw new Error(`Source repository is not accessible: ${status.slug}`);
305
+ }
306
+ const visibility = await select({
307
+ message: 'Visibility for new repository',
308
+ options: [
309
+ { value: 'private', label: 'Private' },
310
+ { value: 'public', label: 'Public' },
311
+ ],
312
+ initialValue: 'private',
313
+ });
314
+ handleCancel(visibility, cancelAction);
315
+ await createGitHubRepository(realGitHub, options.repoUrl, visibility);
316
+ }
317
+ async function selectSourceRepo(target, cancelAction = 'exit') {
318
+ const repos = await listModuleRepos(target);
319
+ if (repos.length === 0) {
320
+ if (cancelAction === 'back') {
321
+ note(`No source repos found under ${target}/odoo/custom/src/private.`, 'Nothing to select');
322
+ handleUnavailableMenuChoice(cancelAction);
323
+ }
324
+ throw new Error(`No source repos found under ${target}/odoo/custom/src/private`);
325
+ }
326
+ const repoPath = await select({
327
+ message: menuPromptMessage('Source repo', cancelAction),
328
+ options: repos.map((repo) => ({ value: repo, label: repo })),
329
+ initialValue: repos[0],
330
+ });
331
+ handleCancel(repoPath, cancelAction);
332
+ return String(repoPath);
333
+ }
334
+ function suggestedModuleName(repoPath) {
335
+ return 'odoo_sample_module';
336
+ }
337
+ async function addModuleOptionsFromArgs(argv) {
338
+ const { values } = parseArgs(argv);
339
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
340
+ const moduleName = stringOption(values, 'module') ?? stringOption(values, 'moduleName');
341
+ if (!repoPath || !moduleName) {
342
+ return undefined;
343
+ }
344
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
345
+ return {
346
+ target,
347
+ repoPath,
348
+ moduleName,
349
+ odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
350
+ stage: booleanOption(values, 'stage', true),
351
+ };
352
+ }
353
+ async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
354
+ showSubmenuIntro('Add module to source repo', showIntro, cancelAction);
355
+ const target = process.cwd();
356
+ const repoPath = await selectSourceRepo(target, cancelAction);
357
+ const moduleName = asString(await text({
358
+ message: menuPromptMessage('Module name', cancelAction),
359
+ placeholder: suggestedModuleName(repoPath),
360
+ validate: (value) => (value.trim() ? undefined : 'Enter the module technical name.'),
361
+ }), suggestedModuleName(repoPath), cancelAction);
362
+ return {
363
+ target,
364
+ repoPath,
365
+ moduleName,
366
+ odooVersion: await commandOdooVersion(target),
367
+ stage: true,
368
+ };
369
+ }
370
+ function removeRepoOptionsFromArgs(argv) {
371
+ const { values } = parseArgs(argv);
372
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
373
+ if (!repoPath) {
374
+ return undefined;
375
+ }
376
+ return {
377
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
378
+ repoPath,
379
+ stage: booleanOption(values, 'stage', true),
380
+ };
381
+ }
382
+ function resetOptionsFromArgs(argv) {
383
+ const { values } = parseArgs(argv);
384
+ return {
385
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
386
+ stage: booleanOption(values, 'stage', true),
387
+ };
388
+ }
389
+ async function confirmSafeResetFromMenu(options) {
390
+ note(renderSafeResetPreview(options.target, options.stage), 'Safe reset preview');
391
+ const confirmed = await confirm({
392
+ message: menuPromptMessage('Continue with safe reset?', 'back'),
393
+ active: 'Yes',
394
+ inactive: 'No',
395
+ initialValue: false,
396
+ });
397
+ handleCancel(confirmed, 'back');
398
+ if (!confirmed) {
399
+ throw new MenuBackSignal();
400
+ }
401
+ }
402
+ async function removeRepoOptionsFromPrompts(argv, showIntro = true, cancelAction = 'exit') {
403
+ showSubmenuIntro('Remove a repo', showIntro, cancelAction);
404
+ const { values } = parseArgs(argv);
405
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
406
+ const repos = await listModuleRepos(target);
407
+ if (repos.length === 0) {
408
+ if (cancelAction === 'back') {
409
+ note(`No module submodules found under ${target}/odoo/custom/src/private.`, 'Nothing to remove');
410
+ handleUnavailableMenuChoice(cancelAction);
411
+ }
412
+ throw new Error(`No module submodules found under ${target}/odoo/custom/src/private`);
413
+ }
414
+ const repoPath = await select({
415
+ message: menuPromptMessage('Repo to remove', cancelAction),
416
+ options: repos.map((repo) => ({ value: repo, label: repo })),
417
+ initialValue: repos[0],
418
+ });
419
+ handleCancel(repoPath, cancelAction);
420
+ return {
421
+ target,
422
+ repoPath: String(repoPath),
423
+ stage: true,
424
+ };
425
+ }
426
+ function removeModuleOptionsFromArgs(argv) {
427
+ const { values } = parseArgs(argv);
428
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
429
+ const moduleName = stringOption(values, 'module') ?? stringOption(values, 'moduleName');
430
+ if (!repoPath || !moduleName) {
431
+ return undefined;
432
+ }
433
+ return {
434
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
435
+ repoPath,
436
+ moduleName,
437
+ deleteFiles: booleanOption(values, 'deleteFiles', false),
438
+ stage: booleanOption(values, 'stage', true),
439
+ };
440
+ }
441
+ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
442
+ showSubmenuIntro('Remove module from source repo', showIntro, cancelAction);
443
+ const target = process.cwd();
444
+ const repoPath = await selectSourceRepo(target, cancelAction);
445
+ const modules = await listModulesInSourceRepo(target, repoPath);
446
+ if (modules.length === 0) {
447
+ if (cancelAction === 'back') {
448
+ note(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}.`, 'Nothing to remove');
449
+ handleUnavailableMenuChoice(cancelAction);
450
+ }
451
+ throw new Error(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}`);
452
+ }
453
+ const moduleName = await select({
454
+ message: menuPromptMessage('Module to remove', cancelAction),
455
+ options: modules.map((module) => ({ value: module, label: module })),
456
+ initialValue: modules[0],
457
+ });
458
+ handleCancel(moduleName, cancelAction);
459
+ const deleteFiles = await confirm({
460
+ message: menuPromptMessage('Delete module files too? (y/N)', cancelAction),
461
+ active: 'Y',
462
+ inactive: 'n',
463
+ initialValue: false,
464
+ });
465
+ handleCancel(deleteFiles, cancelAction);
466
+ return {
467
+ target,
468
+ repoPath,
469
+ moduleName: String(moduleName),
470
+ deleteFiles: Boolean(deleteFiles),
471
+ stage: true,
472
+ };
473
+ }
474
+ async function ensureGitHubRepositories(options, interactive) {
475
+ if (options.dryRun) {
476
+ return;
477
+ }
478
+ if (!interactive && !options.createMissingRepos) {
479
+ return;
480
+ }
481
+ if (!(await repositoryPreflightAvailable())) {
482
+ const message = [
483
+ 'GitHub CLI (`gh`) is not available or not authenticated.',
484
+ 'Install and authenticate it to auto-create missing GitHub repositories:',
485
+ '',
486
+ 'brew install gh',
487
+ 'gh auth login',
488
+ ].join('\n');
489
+ if (options.createMissingRepos) {
490
+ throw new Error(message);
491
+ }
492
+ if (interactive) {
493
+ note(message, 'Repository check skipped');
494
+ }
495
+ return;
496
+ }
497
+ const { accessible, inaccessible: missing } = await checkGitHubRepositories(options);
498
+ if (interactive && accessible.length > 0) {
499
+ note([
500
+ 'These GitHub repositories already exist and are accessible:',
501
+ '',
502
+ ...accessible.map((repository) => `- ${repository.label}: ${repository.slug}`),
503
+ ].join('\n'), 'Repository check');
504
+ }
505
+ if (missing.length === 0) {
506
+ return;
507
+ }
508
+ const missingList = missing
509
+ .map((repository) => `- ${repository.label}: ${repository.slug}`)
510
+ .join('\n');
511
+ if (!interactive && options.createMissingRepos) {
512
+ await createGitHubRepositories(missing, options.repoVisibility ?? 'private');
513
+ return;
514
+ }
515
+ note([
516
+ 'These GitHub repositories are not accessible. They may not exist, or your account may not have access:',
517
+ '',
518
+ missingList,
519
+ ].join('\n'), 'Repository check');
520
+ const shouldCreate = await select({
521
+ message: 'Create the inaccessible repositories with GitHub CLI?',
522
+ options: [
523
+ { value: true, label: 'Yes, create them' },
524
+ { value: false, label: 'No, I will create/check access myself' },
525
+ ],
526
+ initialValue: true,
527
+ });
528
+ if (isCancel(shouldCreate))
529
+ process.exit(1);
530
+ if (!shouldCreate) {
531
+ throw new Error(['Required repositories are not accessible. Create them first:', '', ...manualCreateCommands(missing)].join('\n'));
532
+ }
533
+ const visibility = await select({
534
+ message: 'Visibility for new repositories',
535
+ options: [
536
+ { value: 'private', label: 'Private' },
537
+ { value: 'public', label: 'Public' },
538
+ ],
539
+ initialValue: 'private',
540
+ });
541
+ if (isCancel(visibility))
542
+ process.exit(1);
543
+ await createGitHubRepositories(missing, visibility);
544
+ }
545
+ async function main() {
546
+ installPromptCancelKeyTracker();
547
+ const rawArgv = process.argv.slice(2);
548
+ const skipUpdateCheck = isUpdateCheckSkipped(rawArgv);
549
+ const argv = stripInternalFlags(rawArgv);
550
+ if (isHelpRequested(argv)) {
551
+ console.log(renderHelp());
552
+ return;
553
+ }
554
+ if (isVersionRequested(argv)) {
555
+ console.log(renderVersion());
556
+ return;
557
+ }
558
+ const route = commandFromArgs(argv);
559
+ if (route.command === 'menu') {
560
+ await showStartup(argv, skipUpdateCheck);
561
+ const detection = await detectDevelopmentEnvironment(process.cwd());
562
+ if (!detection.isEnvironment) {
563
+ const resolvedOptions = await optionsFromPrompts();
564
+ await ensureGitHubRepositories(resolvedOptions, true);
565
+ await scaffold(resolvedOptions);
566
+ outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
567
+ return;
568
+ }
569
+ while (true) {
570
+ try {
571
+ const action = await selectEnvironmentActionFromMenu();
572
+ if (action === 'exit') {
573
+ return;
574
+ }
575
+ if (action === 'add-repo') {
576
+ const options = await addRepoOptionsFromPrompts(false, 'back');
577
+ await ensureAddRepoGitHubRepository(options, 'back');
578
+ await addModuleRepo(options);
579
+ outro(`Added source repo under ${options.target}/odoo/custom/src/private.`);
580
+ return;
581
+ }
582
+ if (action === 'remove-repo') {
583
+ const options = await removeRepoOptionsFromPrompts([], false, 'back');
584
+ await removeModuleRepo(options);
585
+ outro(`Removed source repo ${options.repoPath} from ${options.target}.`);
586
+ return;
587
+ }
588
+ if (action === 'add-module') {
589
+ const options = await addModuleOptionsFromPrompts(false, 'back');
590
+ await addModuleToSourceRepo(options);
591
+ outro(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
592
+ return;
593
+ }
594
+ if (action === 'remove-module') {
595
+ const options = await removeModuleOptionsFromPrompts(false, 'back');
596
+ await removeModuleFromSourceRepo(options);
597
+ outro(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
598
+ return;
599
+ }
600
+ const options = { target: process.cwd(), stage: true };
601
+ await confirmSafeResetFromMenu(options);
602
+ await safeResetEnvironment(options);
603
+ outro(`Safe reset refreshed generated environment files in ${process.cwd()}.`);
604
+ return;
605
+ }
606
+ catch (error) {
607
+ if (isMenuBackSignal(error)) {
608
+ continue;
609
+ }
610
+ throw error;
611
+ }
612
+ }
613
+ }
614
+ if (route.command === 'add-repo') {
615
+ const options = await addRepoOptionsFromArgs(route.argv);
616
+ if (options) {
617
+ console.log(renderBanner());
618
+ await addModuleRepo(options);
619
+ outro(`Added source repo under ${options.target}/odoo/custom/src/private.`);
620
+ return;
621
+ }
622
+ await showStartup(argv, skipUpdateCheck);
623
+ const promptedOptions = await addRepoOptionsFromPrompts();
624
+ await ensureAddRepoGitHubRepository(promptedOptions);
625
+ await addModuleRepo(promptedOptions);
626
+ outro(`Added source repo under ${promptedOptions.target}/odoo/custom/src/private.`);
627
+ return;
628
+ }
629
+ if (route.command === 'remove-repo') {
630
+ const options = removeRepoOptionsFromArgs(route.argv);
631
+ if (options) {
632
+ console.log(renderBanner());
633
+ await removeModuleRepo(options);
634
+ outro(`Removed source repo ${options.repoPath} from ${options.target}.`);
635
+ return;
636
+ }
637
+ await showStartup(argv, skipUpdateCheck);
638
+ const promptedOptions = await removeRepoOptionsFromPrompts(route.argv);
639
+ await removeModuleRepo(promptedOptions);
640
+ outro(`Removed source repo ${promptedOptions.repoPath} from ${promptedOptions.target}.`);
641
+ return;
642
+ }
643
+ if (route.command === 'add-module') {
644
+ const options = await addModuleOptionsFromArgs(route.argv);
645
+ if (options) {
646
+ console.log(renderBanner());
647
+ await addModuleToSourceRepo(options);
648
+ outro(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
649
+ return;
650
+ }
651
+ await showStartup(argv, skipUpdateCheck);
652
+ const promptedOptions = await addModuleOptionsFromPrompts();
653
+ await addModuleToSourceRepo(promptedOptions);
654
+ outro(`Added module ${promptedOptions.moduleName} under source repo ${promptedOptions.repoPath}.`);
655
+ return;
656
+ }
657
+ if (route.command === 'remove-module') {
658
+ const options = removeModuleOptionsFromArgs(route.argv);
659
+ if (options) {
660
+ console.log(renderBanner());
661
+ await removeModuleFromSourceRepo(options);
662
+ outro(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
663
+ return;
664
+ }
665
+ await showStartup(argv, skipUpdateCheck);
666
+ const promptedOptions = await removeModuleOptionsFromPrompts();
667
+ await removeModuleFromSourceRepo(promptedOptions);
668
+ outro(`Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
669
+ return;
670
+ }
671
+ if (route.command === 'reset') {
672
+ console.log(renderBanner());
673
+ const options = resetOptionsFromArgs(route.argv);
674
+ await safeResetEnvironment(options);
675
+ outro(`Safe reset refreshed generated environment files in ${options.target}.`);
676
+ return;
677
+ }
678
+ const options = optionsFromArgs(route.argv);
679
+ if (options) {
680
+ console.log(renderBanner());
681
+ }
682
+ else {
683
+ await showStartup(argv, skipUpdateCheck);
684
+ }
685
+ const resolvedOptions = options ?? (await optionsFromPrompts());
686
+ await ensureGitHubRepositories(resolvedOptions, options === undefined);
687
+ const result = await scaffold(resolvedOptions);
688
+ if (resolvedOptions.dryRun) {
689
+ console.log('Dry run: planned files');
690
+ for (const file of result.plannedFiles)
691
+ console.log(`- ${file}`);
692
+ console.log('Dry run: planned commands');
693
+ for (const command of result.plannedCommands)
694
+ console.log(`- ${command}`);
695
+ return;
696
+ }
697
+ outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
698
+ }
699
+ main().catch((error) => {
700
+ const message = error instanceof Error ? error.message : String(error);
701
+ console.error(message);
702
+ process.exit(1);
703
+ });