@wpmoo/toolkit 0.9.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 (46) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +519 -0
  3. package/dist/addons-yaml.js +59 -0
  4. package/dist/args.js +259 -0
  5. package/dist/cli.js +1039 -0
  6. package/dist/cockpit/command-palette.js +23 -0
  7. package/dist/cockpit/command-registry.js +91 -0
  8. package/dist/cockpit/daily-prompts.js +177 -0
  9. package/dist/cockpit/menu.js +99 -0
  10. package/dist/cockpit/safety.js +22 -0
  11. package/dist/compose-layout.js +118 -0
  12. package/dist/daily-actions.js +190 -0
  13. package/dist/doctor.js +519 -0
  14. package/dist/environment-context.js +10 -0
  15. package/dist/environment-version.js +5 -0
  16. package/dist/environment.js +136 -0
  17. package/dist/external-assets.js +153 -0
  18. package/dist/external-templates.js +86 -0
  19. package/dist/git.js +98 -0
  20. package/dist/github.js +87 -0
  21. package/dist/help.js +157 -0
  22. package/dist/menu-navigation.js +67 -0
  23. package/dist/module-actions.js +114 -0
  24. package/dist/odoo-versions.js +1 -0
  25. package/dist/path-validation.js +50 -0
  26. package/dist/prompt-copy.js +8 -0
  27. package/dist/prompt-repositories.js +34 -0
  28. package/dist/prompts/index.js +174 -0
  29. package/dist/repo-actions.js +158 -0
  30. package/dist/repo-url.js +27 -0
  31. package/dist/repository-preflight.js +46 -0
  32. package/dist/safe-reset.js +217 -0
  33. package/dist/scaffold.js +161 -0
  34. package/dist/source-actions.js +65 -0
  35. package/dist/source-manifest.js +338 -0
  36. package/dist/status.js +239 -0
  37. package/dist/templates.js +758 -0
  38. package/dist/types.js +1 -0
  39. package/dist/update-check.js +106 -0
  40. package/dist/version.js +19 -0
  41. package/docs/assets/patreon-donate.png +0 -0
  42. package/docs/assets/wpmoo-banner.png +0 -0
  43. package/docs/external-resources.md +136 -0
  44. package/docs/generated-environment-verification.md +140 -0
  45. package/docs/handoff.md +29 -0
  46. package/package.json +65 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1039 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { basename, relative, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { commandFromArgs, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
6
+ import { collectDailyActionArgs } from './cockpit/daily-prompts.js';
7
+ import { selectCockpitTopLevelMenu } from './cockpit/menu.js';
8
+ import { confirmCockpitCommandRisk } from './cockpit/safety.js';
9
+ import { detectDevelopmentEnvironment } from './environment.js';
10
+ import { commandOdooVersion } from './environment-version.js';
11
+ import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
12
+ import { isDailyActionCommand, runDailyAction } from './daily-actions.js';
13
+ import { getDoctorReport, runDoctor } from './doctor.js';
14
+ import { getOriginUrl, realGit } from './git.js';
15
+ import { renderHelp } from './help.js';
16
+ import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
17
+ import { supportedOdooVersions } from './odoo-versions.js';
18
+ import { renderRepositorySetupNote } from './prompt-copy.js';
19
+ import { promptRepositoryUrl } from './prompt-repositories.js';
20
+ import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-url.js';
21
+ import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
22
+ import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
23
+ import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
24
+ import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, repositoryPreflightAvailable, } from './repository-preflight.js';
25
+ import { scaffold } from './scaffold.js';
26
+ import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
27
+ import { renderBanner } from './templates.js';
28
+ import { checkForUpdate, installLatestPackage, isUpdateCheckSkipped, restartCli } from './update-check.js';
29
+ import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
30
+ import { environmentStatusJson, getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
31
+ import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
32
+ import { environmentGitHubOwner } from './environment-context.js';
33
+ import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
34
+ function handleCancel(value, action) {
35
+ handlePromptCancel(isPromptCancel(value), action);
36
+ }
37
+ function showSubmenuIntro(title, showIntro, cancelAction) {
38
+ if (showIntro) {
39
+ introPrompt(menuIntroTitle(title, cancelAction));
40
+ }
41
+ }
42
+ function asString(value, fallback, cancelAction = 'exit') {
43
+ handleCancel(value, cancelAction);
44
+ return typeof value === 'string' && value.trim() ? value.trim() : fallback;
45
+ }
46
+ function githubAccountLabel(account) {
47
+ return account.type === 'user' ? `${account.login} (personal)` : `${account.login} (organization)`;
48
+ }
49
+ async function selectDefaultGitHubOwner(cancelAction = 'exit', preferredOwner) {
50
+ try {
51
+ const accounts = await getGitHubAccounts(realGitHub);
52
+ if (accounts.length === 0) {
53
+ return preferredOwner;
54
+ }
55
+ if (accounts.length === 1) {
56
+ return accounts[0].login;
57
+ }
58
+ const initialValue = accounts.some((account) => account.login === preferredOwner)
59
+ ? preferredOwner
60
+ : accounts[0].login;
61
+ const selectedOwner = await selectPrompt({
62
+ message: 'GitHub account/organization',
63
+ options: accounts.map((account) => ({
64
+ value: account.login,
65
+ label: githubAccountLabel(account),
66
+ })),
67
+ initialValue,
68
+ });
69
+ handleCancel(selectedOwner, cancelAction);
70
+ return String(selectedOwner);
71
+ }
72
+ catch (error) {
73
+ if (isMenuBackSignal(error))
74
+ throw error;
75
+ return preferredOwner;
76
+ }
77
+ }
78
+ function stringOption(values, key) {
79
+ const value = values[key];
80
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
81
+ }
82
+ function optionalSourceTypeValue(values) {
83
+ const value = stringOption(values, 'sourceType');
84
+ if (value === undefined) {
85
+ return undefined;
86
+ }
87
+ if (value === 'private' || value === 'oca' || value === 'external') {
88
+ return value;
89
+ }
90
+ throw new Error(`Invalid value for --source-type: ${value}`);
91
+ }
92
+ function sourceTypeValue(values) {
93
+ return optionalSourceTypeValue(values) ?? 'private';
94
+ }
95
+ function booleanOption(values, key, fallback) {
96
+ const value = values[key];
97
+ if (value === undefined)
98
+ return fallback;
99
+ if (typeof value === 'boolean')
100
+ return value;
101
+ const normalized = value.toLowerCase().trim();
102
+ if (['true', '1', 'yes', 'y'].includes(normalized))
103
+ return true;
104
+ if (['false', '0', 'no', 'n'].includes(normalized))
105
+ return false;
106
+ throw new Error(`Invalid boolean value for --${key}: ${value}`);
107
+ }
108
+ function jsonOption(values) {
109
+ return booleanOption(values, 'json', false);
110
+ }
111
+ function printJson(value) {
112
+ console.log(JSON.stringify(value));
113
+ }
114
+ function yellow(value) {
115
+ if (!process.stdout.isTTY || process.env.NO_COLOR !== undefined)
116
+ return value;
117
+ return `\u001b[33m${value}\u001b[39m`;
118
+ }
119
+ function shellQuote(value) {
120
+ if (/^[A-Za-z0-9_./:-]+$/.test(value))
121
+ return value;
122
+ return `'${value.replaceAll("'", "'\\''")}'`;
123
+ }
124
+ function renderedSourceRepoPath(target, sourceType, repoPath) {
125
+ if (repoPath) {
126
+ return `${target}/odoo/custom/src/${sourceType}/${repoPath}`;
127
+ }
128
+ return `${target}/odoo/custom/src/${sourceType}`;
129
+ }
130
+ function renderPostCreateGuidance(target, cwd) {
131
+ const relativeTarget = relative(cwd, target) || '.';
132
+ return yellow([
133
+ 'Environment is ready. Enter the development folder, then run the local WPMoo cockpit:',
134
+ '',
135
+ `cd ${shellQuote(relativeTarget)}`,
136
+ './moo',
137
+ ].join('\n'));
138
+ }
139
+ function validateRepoName(value) {
140
+ const normalized = value.trim();
141
+ if (!normalized)
142
+ return 'Enter a repository name.';
143
+ if (normalized.includes('/') || normalized.includes(':'))
144
+ return 'Enter only the repository name, not a URL.';
145
+ return undefined;
146
+ }
147
+ function startupVersionLine(latestVersion) {
148
+ return `v${packageVersion()}${latestVersion ? ` -> v${latestVersion} available` : ''}`;
149
+ }
150
+ function pluralize(count, singular, plural) {
151
+ return `${count} ${count === 1 ? singular : plural}`;
152
+ }
153
+ function renderStartupEnvironmentLine(status) {
154
+ if (status.kind !== 'environment') {
155
+ return `Environment: ${renderEnvironmentStatusSummary(status)}`;
156
+ }
157
+ const issueCount = status.composeErrors.length + status.invalidSourceRepoPaths.length + status.missingCoreFiles.length;
158
+ const issueSuffix = issueCount > 0 ? ` · ${pluralize(issueCount, 'issue', 'issues')}` : '';
159
+ return [
160
+ `Environment: Odoo ${status.odooVersion}`,
161
+ pluralize(status.sourceRepoCount, 'repo', 'repos'),
162
+ pluralize(status.moduleCandidateCount, 'module', 'modules'),
163
+ ].join(' · ') + issueSuffix;
164
+ }
165
+ function renderStartupBanner(details, latestVersion) {
166
+ const versionLine = startupVersionLine(latestVersion);
167
+ return renderBanner(details?.(versionLine), details ? { version: versionLine } : undefined);
168
+ }
169
+ function renderCockpitStatusLines(status, lastStatus) {
170
+ return [renderStartupEnvironmentLine(status), lastStatus];
171
+ }
172
+ function renderLastCommandStatus(command) {
173
+ return `Last: ${command.label} ✓ completed`;
174
+ }
175
+ function clearCockpitScreen() {
176
+ if (process.stdout.isTTY) {
177
+ process.stdout.write('\u001B[2J\u001B[H');
178
+ }
179
+ }
180
+ async function showStartup(argv, skipUpdateCheck, details) {
181
+ if (skipUpdateCheck) {
182
+ console.log(renderStartupBanner(details));
183
+ if (!details) {
184
+ console.log(renderVersionTag());
185
+ }
186
+ console.log();
187
+ return;
188
+ }
189
+ const updateCheck = await checkForUpdate(packageName(), packageVersion());
190
+ const latestVersion = updateCheck.status === 'update-available' ? updateCheck.latestVersion : undefined;
191
+ console.log(renderStartupBanner(details, latestVersion));
192
+ if (!details) {
193
+ console.log(renderVersionTag(latestVersion));
194
+ }
195
+ if (updateCheck.status === 'update-available') {
196
+ const shouldUpdate = await confirmPrompt({
197
+ message: `Update to v.${updateCheck.latestVersion}? (Y/n)`,
198
+ active: 'Y',
199
+ inactive: 'n',
200
+ initialValue: true,
201
+ });
202
+ handleCancel(shouldUpdate, 'exit');
203
+ if (shouldUpdate) {
204
+ try {
205
+ await installLatestPackage(packageName(), updateCheck.latestVersion);
206
+ const code = await restartCli(packageName(), updateCheck.latestVersion, argv);
207
+ if (code === 0) {
208
+ process.exit(0);
209
+ }
210
+ console.warn(`Update restart exited with code ${code ?? 'unknown'}; continuing with v.${packageVersion()}.`);
211
+ }
212
+ catch (error) {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ console.warn(`Update failed: ${message}. Continuing with v.${packageVersion()}.`);
215
+ }
216
+ }
217
+ }
218
+ console.log();
219
+ }
220
+ async function selectCockpitCommandFromMenu() {
221
+ const selection = await selectCockpitTopLevelMenu();
222
+ if (selection.kind === 'exit') {
223
+ return 'exit';
224
+ }
225
+ return selection.command;
226
+ }
227
+ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
228
+ if (showIntro) {
229
+ introPrompt('Create Odoo dev environment');
230
+ }
231
+ const product = asString(await textPrompt({
232
+ message: 'Product slug',
233
+ placeholder: 'odoo_sample_module',
234
+ validate: (value) => (value.trim() ? undefined : 'Enter a product/module slug.'),
235
+ }), 'odoo_sample_module', cancelAction);
236
+ const defaultTarget = `./${product}_dev`;
237
+ const target = resolve(asString(await textPrompt({
238
+ message: 'Environment folder',
239
+ placeholder: defaultTarget,
240
+ defaultValue: defaultTarget,
241
+ initialValue: defaultTarget,
242
+ }), defaultTarget, cancelAction));
243
+ const connectGitHub = await selectPrompt({
244
+ message: 'Connect this environment to Git/GitHub now?',
245
+ options: [
246
+ { value: true, label: 'Yes, connect Git/GitHub repositories' },
247
+ { value: false, label: 'No, scaffold local-only' },
248
+ ],
249
+ initialValue: true,
250
+ });
251
+ handleCancel(connectGitHub, cancelAction);
252
+ let selectedGitHubOwner;
253
+ if (connectGitHub) {
254
+ notePrompt(renderRepositorySetupNote(product), 'Repository setup');
255
+ selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
256
+ }
257
+ const selectedVersion = await selectPrompt({
258
+ message: menuPromptMessage('Odoo version', cancelAction),
259
+ options: supportedOdooVersions.map((version) => ({ value: version, label: version })),
260
+ initialValue: supportedOdooVersions[0],
261
+ });
262
+ handleCancel(selectedVersion, cancelAction);
263
+ const odooVersion = String(selectedVersion);
264
+ async function promptInstallAgentSkills() {
265
+ const installAgentSkills = await selectPrompt({
266
+ message: 'Install project-local Odoo Agent Skills?',
267
+ options: [
268
+ { value: true, label: 'Yes, install latest default skills' },
269
+ { value: false, label: 'No' },
270
+ ],
271
+ initialValue: false,
272
+ });
273
+ handleCancel(installAgentSkills, cancelAction);
274
+ return Boolean(installAgentSkills);
275
+ }
276
+ if (!connectGitHub) {
277
+ const installAgentSkills = await promptInstallAgentSkills();
278
+ return {
279
+ product,
280
+ odooVersion,
281
+ engine: 'compose',
282
+ devRepo: basename(target),
283
+ devRepoUrl: target,
284
+ sourceRepos: [],
285
+ target,
286
+ dryRun: false,
287
+ initEmptyRepos: false,
288
+ stage: false,
289
+ agentSkillsTemplateUrl: installAgentSkills ? defaultAgentSkillsTemplateUrl : undefined,
290
+ createMissingRepos: false,
291
+ repoVisibility: 'private',
292
+ skipSubmodules: true,
293
+ };
294
+ }
295
+ const detectedDevRepoUrl = await getOriginUrl(realGit, target);
296
+ const defaultDevRepoUrl = selectedGitHubOwner
297
+ ? githubRepositoryUrl(selectedGitHubOwner, `${product}_dev`)
298
+ : undefined;
299
+ const devRepoUrl = normalizeRepositoryUrl(await promptRepositoryUrl({
300
+ label: 'Dev environment repo URL',
301
+ suggestedUrl: detectedDevRepoUrl ?? defaultDevRepoUrl,
302
+ placeholder: `https://github.com/your-account/${product}_dev.git`,
303
+ cancelAction,
304
+ }));
305
+ const defaultOwner = inferGitHubOwner(devRepoUrl) ?? selectedGitHubOwner;
306
+ const sourceRepos = [];
307
+ let addAnother = true;
308
+ while (addAnother) {
309
+ const repoIndex = sourceRepos.length;
310
+ const suggestedRepo = defaultOwner === undefined
311
+ ? undefined
312
+ : githubRepositoryUrl(defaultOwner, repoIndex === 0 ? product : `${product}_${repoIndex + 1}`);
313
+ const sourceRepoUrl = normalizeRepositoryUrl(await promptRepositoryUrl({
314
+ label: repoIndex === 0 ? 'Source repo URL' : `Additional source repo ${repoIndex + 1} URL`,
315
+ suggestedUrl: suggestedRepo,
316
+ placeholder: `https://github.com/owner/${repoIndex === 0 ? product : `${product}_${repoIndex + 1}`}.git`,
317
+ cancelAction,
318
+ }));
319
+ const sourcePath = inferRepoPath(sourceRepoUrl);
320
+ sourceRepos.push({
321
+ url: sourceRepoUrl,
322
+ path: sourcePath,
323
+ addons: [sourcePath],
324
+ });
325
+ const shouldAddAnother = await selectPrompt({
326
+ message: 'Add another source repo?',
327
+ options: [
328
+ { value: false, label: 'No' },
329
+ { value: true, label: 'Yes' },
330
+ ],
331
+ initialValue: false,
332
+ });
333
+ handleCancel(shouldAddAnother, cancelAction);
334
+ addAnother = Boolean(shouldAddAnother);
335
+ }
336
+ const installAgentSkills = await promptInstallAgentSkills();
337
+ const initEmpty = await selectPrompt({
338
+ message: 'Initialize repositories that exist but have no commits?',
339
+ options: [
340
+ { value: true, label: 'Yes, create the selected Odoo branch' },
341
+ { value: false, label: 'No, fail with instructions' },
342
+ ],
343
+ initialValue: true,
344
+ });
345
+ handleCancel(initEmpty, cancelAction);
346
+ return {
347
+ product,
348
+ odooVersion,
349
+ engine: 'compose',
350
+ devRepo: inferRepoPath(devRepoUrl),
351
+ devRepoUrl,
352
+ sourceRepos,
353
+ target,
354
+ dryRun: false,
355
+ initEmptyRepos: Boolean(initEmpty),
356
+ stage: true,
357
+ agentSkillsTemplateUrl: Boolean(installAgentSkills) ? defaultAgentSkillsTemplateUrl : undefined,
358
+ createMissingRepos: false,
359
+ repoVisibility: 'private',
360
+ };
361
+ }
362
+ async function addRepoOptionsFromArgs(argv) {
363
+ const { values } = parseArgs(argv);
364
+ const repoUrl = stringOption(values, 'repoUrl') ?? stringOption(values, 'sourceRepoUrl');
365
+ if (!repoUrl) {
366
+ return undefined;
367
+ }
368
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
369
+ return {
370
+ target,
371
+ repoUrl: normalizeRepositoryUrl(repoUrl),
372
+ repoPath: stringOption(values, 'repo') ?? stringOption(values, 'sourcePath'),
373
+ sourceType: sourceTypeValue(values),
374
+ odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
375
+ initEmptyRepos: booleanOption(values, 'initEmptyRepos', false),
376
+ stage: booleanOption(values, 'stage', true),
377
+ };
378
+ }
379
+ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
380
+ showSubmenuIntro('Add source repo as submodule', showIntro, cancelAction);
381
+ const target = process.cwd();
382
+ const odooVersion = await commandOdooVersion(target);
383
+ const preferredOwner = await environmentGitHubOwner(target);
384
+ const selectedOwner = await selectDefaultGitHubOwner(cancelAction, preferredOwner);
385
+ const owner = selectedOwner ??
386
+ asString(await textPrompt({
387
+ message: menuPromptMessage('GitHub owner/organization', cancelAction),
388
+ placeholder: 'example-org',
389
+ validate: (value) => (value.trim() ? undefined : 'Enter a GitHub owner or organization.'),
390
+ }), 'example-org', cancelAction);
391
+ const repoName = asString(await textPrompt({
392
+ message: menuPromptMessage('Source repo name', cancelAction),
393
+ placeholder: 'odoo_sample_module_repo',
394
+ validate: validateRepoName,
395
+ }), 'odoo_sample_module_repo', cancelAction);
396
+ const repoUrl = githubRepositoryUrl(owner, repoName);
397
+ return {
398
+ target,
399
+ repoUrl,
400
+ sourceType: 'private',
401
+ odooVersion,
402
+ initEmptyRepos: true,
403
+ stage: true,
404
+ };
405
+ }
406
+ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
407
+ if (!(await repositoryPreflightAvailable())) {
408
+ notePrompt([
409
+ 'GitHub CLI (`gh`) is not available or not authenticated.',
410
+ 'The source repo will be used as-is. If it does not exist, create it first or authenticate gh.',
411
+ ].join('\n'), 'Repository check skipped');
412
+ return;
413
+ }
414
+ const status = await getGitHubRepositoryStatus(realGitHub, options.repoUrl);
415
+ if (status.status !== 'inaccessible') {
416
+ return;
417
+ }
418
+ notePrompt(`Source repo is not accessible: ${status.slug}`, 'Repository check');
419
+ const shouldCreate = await selectPrompt({
420
+ message: 'Create this source repository with GitHub CLI?',
421
+ options: [
422
+ { value: true, label: 'Yes, create it' },
423
+ { value: false, label: 'No, I will create/check access myself' },
424
+ ],
425
+ initialValue: true,
426
+ });
427
+ handleCancel(shouldCreate, cancelAction);
428
+ if (!shouldCreate) {
429
+ throw new Error(`Source repository is not accessible: ${status.slug}`);
430
+ }
431
+ const visibility = await selectPrompt({
432
+ message: 'Visibility for new repository',
433
+ options: [
434
+ { value: 'private', label: 'Private' },
435
+ { value: 'public', label: 'Public' },
436
+ ],
437
+ initialValue: 'private',
438
+ });
439
+ handleCancel(visibility, cancelAction);
440
+ await createGitHubRepository(realGitHub, options.repoUrl, visibility);
441
+ }
442
+ async function selectSourceRepo(target, cancelAction = 'exit') {
443
+ const repos = await listSources(target);
444
+ const repoOptions = repos.length > 0
445
+ ? repos.map((repo) => ({
446
+ value: { repoPath: repo.path, sourceType: repo.type },
447
+ label: `${repo.type}/${repo.path}`,
448
+ }))
449
+ : (await listModuleRepos(target)).map((repoPath) => ({
450
+ value: { repoPath, sourceType: 'private' },
451
+ label: `private/${repoPath}`,
452
+ }));
453
+ if (repoOptions.length === 0) {
454
+ if (cancelAction === 'back') {
455
+ notePrompt(`No source repos found under ${target}/odoo/custom/src.\nNext: choose "Add source repo" first.`, 'Nothing to select');
456
+ handleUnavailableMenuChoice(cancelAction);
457
+ }
458
+ throw new Error(`No source repos found under ${target}/odoo/custom/src`);
459
+ }
460
+ const selected = await selectPrompt({
461
+ message: menuPromptMessage('Source repo', cancelAction),
462
+ options: repoOptions,
463
+ initialValue: repoOptions[0].value,
464
+ });
465
+ handleCancel(selected, cancelAction);
466
+ if (typeof selected === 'string') {
467
+ return { repoPath: selected, sourceType: 'private' };
468
+ }
469
+ if (typeof selected === 'object' && selected !== null && 'repoPath' in selected && 'sourceType' in selected) {
470
+ return { repoPath: selected.repoPath, sourceType: selected.sourceType };
471
+ }
472
+ return { repoPath: String(selected), sourceType: 'private' };
473
+ }
474
+ function formatSourceRepoPromptPath(target, selected) {
475
+ return renderedSourceRepoPath(target, selected.sourceType, selected.repoPath);
476
+ }
477
+ function suggestedModuleName(repoPath) {
478
+ return 'odoo_sample_module';
479
+ }
480
+ async function addModuleOptionsFromArgs(argv) {
481
+ const { values } = parseArgs(argv);
482
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
483
+ const moduleName = stringOption(values, 'module') ?? stringOption(values, 'moduleName');
484
+ if (!repoPath || !moduleName) {
485
+ return undefined;
486
+ }
487
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
488
+ return {
489
+ target,
490
+ repoPath,
491
+ moduleName,
492
+ sourceType: optionalSourceTypeValue(values),
493
+ odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
494
+ stage: booleanOption(values, 'stage', true),
495
+ };
496
+ }
497
+ async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
498
+ showSubmenuIntro('Add module to source repo', showIntro, cancelAction);
499
+ const target = process.cwd();
500
+ const sourceRepo = await selectSourceRepo(target, cancelAction);
501
+ const moduleName = asString(await textPrompt({
502
+ message: menuPromptMessage('Module name', cancelAction),
503
+ placeholder: suggestedModuleName(sourceRepo.repoPath),
504
+ validate: (value) => (value.trim() ? undefined : 'Enter the module technical name.'),
505
+ }), suggestedModuleName(sourceRepo.repoPath), cancelAction);
506
+ return {
507
+ target,
508
+ repoPath: sourceRepo.repoPath,
509
+ sourceType: sourceRepo.sourceType,
510
+ moduleName,
511
+ odooVersion: await commandOdooVersion(target),
512
+ stage: true,
513
+ };
514
+ }
515
+ function removeRepoOptionsFromArgs(argv) {
516
+ const { values } = parseArgs(argv);
517
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
518
+ if (!repoPath) {
519
+ return undefined;
520
+ }
521
+ return {
522
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
523
+ repoPath,
524
+ sourceType: optionalSourceTypeValue(values),
525
+ stage: booleanOption(values, 'stage', true),
526
+ };
527
+ }
528
+ function resetCommandOptionsFromArgs(argv) {
529
+ const { values } = parseArgs(argv);
530
+ return {
531
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
532
+ stage: booleanOption(values, 'stage', true),
533
+ dryRun: booleanOption(values, 'dryRun', false),
534
+ };
535
+ }
536
+ function doctorOptionsFromArgs(argv) {
537
+ const { values } = parseArgs(argv);
538
+ const keys = Object.keys(values);
539
+ const allowedKeys = new Set(['fix', 'json']);
540
+ if (!keys.every((key) => allowedKeys.has(key))) {
541
+ throw new Error('Usage: wpmoo doctor');
542
+ }
543
+ const options = {
544
+ json: jsonOption(values),
545
+ };
546
+ if (Object.hasOwn(values, 'fix')) {
547
+ options.fix = booleanOption(values, 'fix', false);
548
+ }
549
+ return options;
550
+ }
551
+ function sourceUsage() {
552
+ return 'Usage: wpmoo source <list|sync|add|remove> [options]';
553
+ }
554
+ function sourceSyncOptionsFromArgs(argv) {
555
+ const { values } = parseArgs(argv);
556
+ return {
557
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
558
+ stage: booleanOption(values, 'stage', true),
559
+ json: jsonOption(values),
560
+ };
561
+ }
562
+ function sourceListOptionsFromArgs(argv) {
563
+ const { values } = parseArgs(argv);
564
+ return {
565
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
566
+ json: jsonOption(values),
567
+ };
568
+ }
569
+ async function runSourceCommand(argv) {
570
+ const [subcommand, ...subcommandArgv] = argv;
571
+ if (!subcommand) {
572
+ throw new Error(sourceUsage());
573
+ }
574
+ if (subcommand === 'list') {
575
+ const options = sourceListOptionsFromArgs(subcommandArgv);
576
+ const sources = await listSources(options.target);
577
+ if (options.json) {
578
+ printJson(sourceListJson(sources));
579
+ return;
580
+ }
581
+ console.log(renderBanner());
582
+ console.log(renderSourceList(sources));
583
+ return;
584
+ }
585
+ if (subcommand === 'sync') {
586
+ const options = sourceSyncOptionsFromArgs(subcommandArgv);
587
+ const sources = await syncSources({ target: options.target, stage: options.stage });
588
+ if (options.json) {
589
+ printJson(sourceSyncJson(sources, options.target));
590
+ return;
591
+ }
592
+ console.log(renderBanner());
593
+ outroPrompt(`Synced source manifest in ${options.target}.`);
594
+ return;
595
+ }
596
+ if (subcommand === 'add') {
597
+ const options = await addRepoOptionsFromArgs(subcommandArgv);
598
+ if (!options) {
599
+ throw new Error('Usage: wpmoo source add --repo-url <url> [--source-type private|oca|external]');
600
+ }
601
+ console.log(renderBanner());
602
+ await addModuleRepo(options);
603
+ outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
604
+ return;
605
+ }
606
+ if (subcommand === 'remove') {
607
+ const options = removeRepoOptionsFromArgs(subcommandArgv);
608
+ if (!options) {
609
+ throw new Error('Usage: wpmoo source remove --repo <name> [--source-type private|oca|external]');
610
+ }
611
+ console.log(renderBanner());
612
+ await removeModuleRepo(options);
613
+ outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
614
+ return;
615
+ }
616
+ throw new Error(sourceUsage());
617
+ }
618
+ async function confirmSafeResetFromMenu(options) {
619
+ notePrompt(renderSafeResetPreview(options.target, options.stage), 'Safe reset preview');
620
+ const confirmed = await confirmPrompt({
621
+ message: menuPromptMessage('Continue with safe reset?', 'back'),
622
+ active: 'Yes',
623
+ inactive: 'No',
624
+ initialValue: false,
625
+ });
626
+ handleCancel(confirmed, 'back');
627
+ if (!confirmed) {
628
+ throw new MenuBackSignal();
629
+ }
630
+ }
631
+ async function removeRepoOptionsFromPrompts(argv, showIntro = true, cancelAction = 'exit') {
632
+ showSubmenuIntro('Remove a repo', showIntro, cancelAction);
633
+ const { values } = parseArgs(argv);
634
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
635
+ const repos = await listModuleRepos(target);
636
+ if (repos.length === 0) {
637
+ if (cancelAction === 'back') {
638
+ notePrompt(`No module submodules found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to remove');
639
+ handleUnavailableMenuChoice(cancelAction);
640
+ }
641
+ throw new Error(`No module submodules found under ${target}/odoo/custom/src/private`);
642
+ }
643
+ const repoPath = await selectPrompt({
644
+ message: menuPromptMessage('Repo to remove', cancelAction),
645
+ options: repos.map((repo) => ({ value: repo, label: repo })),
646
+ initialValue: repos[0],
647
+ });
648
+ handleCancel(repoPath, cancelAction);
649
+ return {
650
+ target,
651
+ repoPath: String(repoPath),
652
+ stage: true,
653
+ };
654
+ }
655
+ function removeModuleOptionsFromArgs(argv) {
656
+ const { values } = parseArgs(argv);
657
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
658
+ const moduleName = stringOption(values, 'module') ?? stringOption(values, 'moduleName');
659
+ if (!repoPath || !moduleName) {
660
+ return undefined;
661
+ }
662
+ return {
663
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
664
+ repoPath,
665
+ moduleName,
666
+ sourceType: optionalSourceTypeValue(values),
667
+ deleteFiles: booleanOption(values, 'deleteFiles', false),
668
+ stage: booleanOption(values, 'stage', true),
669
+ };
670
+ }
671
+ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
672
+ showSubmenuIntro('Remove module from source repo', showIntro, cancelAction);
673
+ const target = process.cwd();
674
+ const sourceRepo = await selectSourceRepo(target, cancelAction);
675
+ const modules = await listModulesInSourceRepo(target, sourceRepo.repoPath, sourceRepo.sourceType);
676
+ if (modules.length === 0) {
677
+ if (cancelAction === 'back') {
678
+ notePrompt(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
679
+ handleUnavailableMenuChoice(cancelAction);
680
+ }
681
+ throw new Error(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}`);
682
+ }
683
+ const moduleName = await selectPrompt({
684
+ message: menuPromptMessage('Module to remove', cancelAction),
685
+ options: modules.map((module) => ({ value: module, label: module })),
686
+ initialValue: modules[0],
687
+ });
688
+ handleCancel(moduleName, cancelAction);
689
+ const deleteFiles = await confirmPrompt({
690
+ message: menuPromptMessage('Delete module files too? (y/N)', cancelAction),
691
+ active: 'Y',
692
+ inactive: 'n',
693
+ initialValue: false,
694
+ });
695
+ handleCancel(deleteFiles, cancelAction);
696
+ return {
697
+ target,
698
+ repoPath: sourceRepo.repoPath,
699
+ sourceType: sourceRepo.sourceType,
700
+ moduleName: String(moduleName),
701
+ deleteFiles: Boolean(deleteFiles),
702
+ stage: true,
703
+ };
704
+ }
705
+ async function ensureGitHubRepositories(options, interactive) {
706
+ if (options.dryRun) {
707
+ return;
708
+ }
709
+ if (options.skipSubmodules) {
710
+ return;
711
+ }
712
+ if (!interactive && !options.createMissingRepos) {
713
+ return;
714
+ }
715
+ if (!(await repositoryPreflightAvailable())) {
716
+ const message = [
717
+ 'GitHub CLI (`gh`) is not available or not authenticated.',
718
+ 'Install and authenticate it to auto-create missing GitHub repositories:',
719
+ '',
720
+ 'brew install gh',
721
+ 'gh auth login',
722
+ ].join('\n');
723
+ if (options.createMissingRepos) {
724
+ throw new Error(message);
725
+ }
726
+ if (interactive) {
727
+ notePrompt(message, 'Repository check skipped');
728
+ }
729
+ return;
730
+ }
731
+ const { accessible, inaccessible: missing } = await checkGitHubRepositories(options);
732
+ if (interactive && accessible.length > 0) {
733
+ notePrompt([
734
+ 'These GitHub repositories already exist and are accessible:',
735
+ '',
736
+ ...accessible.map((repository) => `- ${repository.label}: ${repository.slug}`),
737
+ ].join('\n'), 'Repository check');
738
+ }
739
+ if (missing.length === 0) {
740
+ return;
741
+ }
742
+ const missingList = missing
743
+ .map((repository) => `- ${repository.label}: ${repository.slug}`)
744
+ .join('\n');
745
+ if (!interactive && options.createMissingRepos) {
746
+ await createGitHubRepositories(missing, options.repoVisibility ?? 'private');
747
+ return;
748
+ }
749
+ notePrompt([
750
+ 'These GitHub repositories are not accessible. They may not exist, or your account may not have access:',
751
+ '',
752
+ missingList,
753
+ ].join('\n'), 'Repository check');
754
+ const shouldCreate = await selectPrompt({
755
+ message: 'Create the inaccessible repositories with GitHub CLI?',
756
+ options: [
757
+ { value: true, label: 'Yes, create them' },
758
+ { value: false, label: 'No, I will create/check access myself' },
759
+ ],
760
+ initialValue: true,
761
+ });
762
+ handleCancel(shouldCreate, 'exit');
763
+ if (!shouldCreate) {
764
+ throw new Error(['Required repositories are not accessible. Create them first:', '', ...manualCreateCommands(missing)].join('\n'));
765
+ }
766
+ const visibility = await selectPrompt({
767
+ message: 'Visibility for new repositories',
768
+ options: [
769
+ { value: 'private', label: 'Private' },
770
+ { value: 'public', label: 'Public' },
771
+ ],
772
+ initialValue: 'private',
773
+ });
774
+ handleCancel(visibility, 'exit');
775
+ await createGitHubRepositories(missing, visibility);
776
+ }
777
+ async function runCockpitCommand(command, cwd) {
778
+ if (command.id === 'exit') {
779
+ return 'exit';
780
+ }
781
+ if (command.target.kind === 'daily') {
782
+ const argv = await collectDailyActionArgs(command.target.command, cwd);
783
+ if (!(await confirmCockpitCommandRisk(command))) {
784
+ notePrompt(`${command.slashAlias} was not run.`, 'Action skipped');
785
+ return 'continue';
786
+ }
787
+ await runDailyAction(command.target.command, argv, cwd);
788
+ notePrompt(`${command.slashAlias} completed.`, 'Done');
789
+ return 'continue';
790
+ }
791
+ if (command.id === 'status') {
792
+ notePrompt(await renderEnvironmentStatusForTarget(cwd), 'Environment status');
793
+ return 'continue';
794
+ }
795
+ if (command.id === 'doctor') {
796
+ notePrompt(await runDoctor(cwd), 'Doctor');
797
+ return 'continue';
798
+ }
799
+ if (command.id === 'add-repo') {
800
+ const options = await addRepoOptionsFromPrompts(false, 'back');
801
+ await ensureAddRepoGitHubRepository(options, 'back');
802
+ await addModuleRepo(options);
803
+ notePrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private')}.`, 'Done');
804
+ return 'continue';
805
+ }
806
+ if (command.id === 'remove-repo') {
807
+ const options = await removeRepoOptionsFromPrompts([], false, 'back');
808
+ if (!(await confirmCockpitCommandRisk(command))) {
809
+ notePrompt(`Source repo ${options.repoPath} was not removed.`, 'Action skipped');
810
+ return 'continue';
811
+ }
812
+ await removeModuleRepo(options);
813
+ notePrompt(`Removed source repo ${options.repoPath} from ${options.target}.`, 'Done');
814
+ return 'continue';
815
+ }
816
+ if (command.id === 'add-module') {
817
+ const options = await addModuleOptionsFromPrompts(false, 'back');
818
+ await addModuleToSourceRepo(options);
819
+ notePrompt(`Added module ${options.moduleName} under source repo ${options.repoPath}.`, 'Done');
820
+ return 'continue';
821
+ }
822
+ if (command.id === 'remove-module') {
823
+ const options = await removeModuleOptionsFromPrompts(false, 'back');
824
+ if (!(await confirmCockpitCommandRisk(command))) {
825
+ notePrompt(`Module ${options.moduleName} was not removed.`, 'Action skipped');
826
+ return 'continue';
827
+ }
828
+ await removeModuleFromSourceRepo(options);
829
+ notePrompt(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`, 'Done');
830
+ return 'continue';
831
+ }
832
+ if (command.id === 'safe-reset') {
833
+ const options = { target: cwd, stage: true };
834
+ await confirmSafeResetFromMenu(options);
835
+ await safeResetEnvironment(options);
836
+ notePrompt(`Safe reset refreshed generated environment files in ${cwd}.`, 'Done');
837
+ return 'continue';
838
+ }
839
+ notePrompt(`Unknown cockpit command: ${command.slashAlias}`, 'No action');
840
+ return 'continue';
841
+ }
842
+ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd()) {
843
+ installPromptCancelKeyTracker();
844
+ const rawArgv = cliArgv;
845
+ const skipUpdateCheck = isUpdateCheckSkipped(rawArgv);
846
+ const argv = stripInternalFlags(rawArgv);
847
+ if (isHelpRequested(argv)) {
848
+ console.log(renderHelp());
849
+ return;
850
+ }
851
+ if (isVersionRequested(argv)) {
852
+ console.log(renderVersion());
853
+ return;
854
+ }
855
+ const route = commandFromArgs(argv);
856
+ if (route.command === 'menu') {
857
+ const detection = await detectDevelopmentEnvironment(cwd);
858
+ if (!detection.isEnvironment) {
859
+ await showStartup(argv, skipUpdateCheck);
860
+ const resolvedOptions = await optionsFromPrompts();
861
+ await ensureGitHubRepositories(resolvedOptions, true);
862
+ await scaffold(resolvedOptions);
863
+ notePrompt(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
864
+ outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
865
+ return;
866
+ }
867
+ let lastStatus = 'Last: Ready';
868
+ const initialStatus = await getEnvironmentStatus(cwd);
869
+ await showStartup(argv, skipUpdateCheck, () => renderCockpitStatusLines(initialStatus, lastStatus));
870
+ while (true) {
871
+ try {
872
+ const command = await selectCockpitCommandFromMenu();
873
+ if (command === 'exit') {
874
+ return;
875
+ }
876
+ const outcome = await runCockpitCommand(command, cwd);
877
+ if (outcome === 'exit') {
878
+ return;
879
+ }
880
+ lastStatus = renderLastCommandStatus(command);
881
+ const status = await getEnvironmentStatus(cwd);
882
+ clearCockpitScreen();
883
+ console.log(renderBanner(renderCockpitStatusLines(status, lastStatus), { version: startupVersionLine() }));
884
+ }
885
+ catch (error) {
886
+ if (isMenuBackSignal(error)) {
887
+ continue;
888
+ }
889
+ throw error;
890
+ }
891
+ }
892
+ }
893
+ if (route.command === 'add-repo') {
894
+ const options = await addRepoOptionsFromArgs(route.argv);
895
+ if (options) {
896
+ console.log(renderBanner());
897
+ await addModuleRepo(options);
898
+ outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
899
+ return;
900
+ }
901
+ await showStartup(argv, skipUpdateCheck);
902
+ const promptedOptions = await addRepoOptionsFromPrompts();
903
+ await ensureAddRepoGitHubRepository(promptedOptions);
904
+ await addModuleRepo(promptedOptions);
905
+ outroPrompt(`Added source repo under ${promptedOptions.target}/odoo/custom/src/private.`);
906
+ return;
907
+ }
908
+ if (route.command === 'remove-repo') {
909
+ const options = removeRepoOptionsFromArgs(route.argv);
910
+ if (options) {
911
+ console.log(renderBanner());
912
+ await removeModuleRepo(options);
913
+ outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
914
+ return;
915
+ }
916
+ await showStartup(argv, skipUpdateCheck);
917
+ const promptedOptions = await removeRepoOptionsFromPrompts(route.argv);
918
+ await removeModuleRepo(promptedOptions);
919
+ outroPrompt(`Removed source repo ${promptedOptions.repoPath} from ${promptedOptions.target}.`);
920
+ return;
921
+ }
922
+ if (route.command === 'source') {
923
+ await runSourceCommand(route.argv);
924
+ return;
925
+ }
926
+ if (route.command === 'add-module') {
927
+ const options = await addModuleOptionsFromArgs(route.argv);
928
+ if (options) {
929
+ console.log(renderBanner());
930
+ await addModuleToSourceRepo(options);
931
+ outroPrompt(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
932
+ return;
933
+ }
934
+ await showStartup(argv, skipUpdateCheck);
935
+ const promptedOptions = await addModuleOptionsFromPrompts();
936
+ await addModuleToSourceRepo(promptedOptions);
937
+ outroPrompt(`Added module ${promptedOptions.moduleName} under source repo ${promptedOptions.repoPath}.`);
938
+ return;
939
+ }
940
+ if (route.command === 'remove-module') {
941
+ const options = removeModuleOptionsFromArgs(route.argv);
942
+ if (options) {
943
+ console.log(renderBanner());
944
+ await removeModuleFromSourceRepo(options);
945
+ outroPrompt(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
946
+ return;
947
+ }
948
+ await showStartup(argv, skipUpdateCheck);
949
+ const promptedOptions = await removeModuleOptionsFromPrompts();
950
+ await removeModuleFromSourceRepo(promptedOptions);
951
+ outroPrompt(`Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
952
+ return;
953
+ }
954
+ if (route.command === 'reset') {
955
+ console.log(renderBanner());
956
+ const options = resetCommandOptionsFromArgs(route.argv);
957
+ if (options.dryRun) {
958
+ console.log(renderSafeResetPreview(options.target, options.stage));
959
+ return;
960
+ }
961
+ const resetOptions = { target: options.target, stage: options.stage };
962
+ await safeResetEnvironment(resetOptions);
963
+ outroPrompt(`Safe reset refreshed generated environment files in ${options.target}.`);
964
+ return;
965
+ }
966
+ if (route.command === 'doctor') {
967
+ const options = doctorOptionsFromArgs(route.argv);
968
+ const doctorOptions = {};
969
+ if (options.fix !== undefined) {
970
+ doctorOptions.fix = options.fix;
971
+ }
972
+ if (options.json) {
973
+ printJson(await getDoctorReport(cwd, doctorOptions));
974
+ return;
975
+ }
976
+ console.log(renderBanner());
977
+ console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd, doctorOptions));
978
+ return;
979
+ }
980
+ if (route.command === 'status') {
981
+ const { values } = parseArgs(route.argv);
982
+ const keys = Object.keys(values);
983
+ if (!keys.every((key) => key === 'json')) {
984
+ throw new Error('Usage: wpmoo status');
985
+ }
986
+ if (jsonOption(values)) {
987
+ printJson(environmentStatusJson(await getEnvironmentStatus(cwd)));
988
+ return;
989
+ }
990
+ console.log(renderBanner());
991
+ console.log(await renderEnvironmentStatusForTarget(cwd));
992
+ return;
993
+ }
994
+ if (isDailyActionCommand(route.command)) {
995
+ console.log(renderBanner());
996
+ await runDailyAction(route.command, route.argv, cwd);
997
+ return;
998
+ }
999
+ const options = optionsFromArgs(route.argv);
1000
+ if (options) {
1001
+ console.log(renderBanner());
1002
+ }
1003
+ else {
1004
+ await showStartup(argv, skipUpdateCheck);
1005
+ }
1006
+ const resolvedOptions = options ?? (await optionsFromPrompts());
1007
+ await ensureGitHubRepositories(resolvedOptions, options === undefined);
1008
+ const result = await scaffold(resolvedOptions);
1009
+ if (resolvedOptions.dryRun) {
1010
+ console.log('Dry run: planned files');
1011
+ for (const file of result.plannedFiles)
1012
+ console.log(`- ${file}`);
1013
+ console.log('Dry run: planned commands');
1014
+ for (const command of result.plannedCommands)
1015
+ console.log(`- ${command}`);
1016
+ return;
1017
+ }
1018
+ notePrompt(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
1019
+ outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
1020
+ }
1021
+ export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
1022
+ if (!argvPath)
1023
+ return false;
1024
+ try {
1025
+ const entrypointUrl = pathToFileURL(realpathSync(fileURLToPath(metaUrl))).href;
1026
+ const argvUrl = pathToFileURL(realpathSync(argvPath)).href;
1027
+ return entrypointUrl === argvUrl;
1028
+ }
1029
+ catch {
1030
+ return metaUrl === pathToFileURL(argvPath).href;
1031
+ }
1032
+ }
1033
+ if (isCliEntrypoint(import.meta.url)) {
1034
+ runCli().catch((error) => {
1035
+ const message = error instanceof Error ? error.message : String(error);
1036
+ console.error(message);
1037
+ process.exit(1);
1038
+ });
1039
+ }