@wpmoo/toolkit 0.9.2 → 0.9.4
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/README.md +12 -11
- package/dist/cli.js +257 -79
- package/dist/environment-target-preflight.js +48 -0
- package/dist/github-prerequisites.js +22 -0
- package/dist/local-cockpit.js +15 -0
- package/dist/prompts/index.js +3 -2
- package/dist/repository-preflight.js +36 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,25 +23,26 @@ WPMoo Toolkit is an independent project and is not affiliated with, endorsed by,
|
|
|
23
23
|
- Optionally copy project-local Agent Skills from `wpmoo-org/odoo-skills` into generated environments.
|
|
24
24
|
- Use either a guided terminal cockpit or direct CLI commands for the same lifecycle tasks.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Prerequisites
|
|
27
27
|
|
|
28
|
-
- Node.js
|
|
28
|
+
- Node.js `20.17+`
|
|
29
29
|
- Git
|
|
30
|
-
- Docker
|
|
31
|
-
- GitHub
|
|
30
|
+
- Docker + Docker Compose for generated environment runtime commands
|
|
31
|
+
- For GitHub-connected setup, install and authenticate GitHub CLI:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
brew install gh
|
|
35
|
+
gh auth login
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
GitHub CLI (`gh`) is optional.
|
|
39
|
+
WPMoo uses `gh` to inspect source/dev repositories and to create missing repos during setup. It also uses repository inspection to detect existing non-empty dev repositories and avoid overwriting them; if you do not want GitHub checks, keep setup local-only.
|
|
32
40
|
|
|
33
41
|
The wizard currently offers Odoo `19.0`, `18.0`, `17.0`, and `16.0`. Generated
|
|
34
42
|
environments now use the compact compose layout (`compose.yaml` with
|
|
35
43
|
`compose/<env>.yaml` overlays). Legacy root-level
|
|
36
44
|
`docker-compose_<version>.yml` layouts are still supported for compatibility.
|
|
37
45
|
|
|
38
|
-
Set up GitHub CLI only when you want WPMoo to discover your personal account and organizations or create missing repositories from the interactive wizard:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
brew install gh
|
|
42
|
-
gh auth login
|
|
43
|
-
```
|
|
44
|
-
|
|
45
46
|
## Quick Start
|
|
46
47
|
|
|
47
48
|
Run the guided wizard from a workspace directory:
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { rm, rename } from 'node:fs/promises';
|
|
3
4
|
import { basename, relative, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { commandFromArgs, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
|
|
@@ -13,6 +14,7 @@ import { isDailyActionCommand, runDailyAction } from './daily-actions.js';
|
|
|
13
14
|
import { getDoctorReport, runDoctor } from './doctor.js';
|
|
14
15
|
import { getOriginUrl, realGit } from './git.js';
|
|
15
16
|
import { renderHelp } from './help.js';
|
|
17
|
+
import { runLocalCockpit } from './local-cockpit.js';
|
|
16
18
|
import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
|
|
17
19
|
import { supportedOdooVersions } from './odoo-versions.js';
|
|
18
20
|
import { renderRepositorySetupNote } from './prompt-copy.js';
|
|
@@ -21,7 +23,9 @@ import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-
|
|
|
21
23
|
import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
|
|
22
24
|
import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
|
|
23
25
|
import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
|
|
24
|
-
import {
|
|
26
|
+
import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget, renderExistingEnvironmentSummary, renderForeignEnvironmentTargetWarning, } from './environment-target-preflight.js';
|
|
27
|
+
import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
|
|
28
|
+
import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, } from './repository-preflight.js';
|
|
25
29
|
import { scaffold } from './scaffold.js';
|
|
26
30
|
import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
|
|
27
31
|
import { renderBanner } from './templates.js';
|
|
@@ -111,10 +115,25 @@ function jsonOption(values) {
|
|
|
111
115
|
function printJson(value) {
|
|
112
116
|
console.log(JSON.stringify(value));
|
|
113
117
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
118
|
+
function supportsAnsi() {
|
|
119
|
+
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
|
|
120
|
+
}
|
|
121
|
+
function ansi(value, open, close) {
|
|
122
|
+
if (!supportsAnsi())
|
|
116
123
|
return value;
|
|
117
|
-
return
|
|
124
|
+
return `${open}${value}${close}`;
|
|
125
|
+
}
|
|
126
|
+
function yellow(value) {
|
|
127
|
+
return ansi(value, '\u001B[33m', '\u001B[39m');
|
|
128
|
+
}
|
|
129
|
+
function cyan(value) {
|
|
130
|
+
return ansi(value, '\u001B[36m', '\u001B[39m');
|
|
131
|
+
}
|
|
132
|
+
function boldGreen(value) {
|
|
133
|
+
return ansi(value, '\u001B[1m\u001B[32m', '\u001B[39m\u001B[22m');
|
|
134
|
+
}
|
|
135
|
+
function dim(value) {
|
|
136
|
+
return ansi(value, '\u001B[2m', '\u001B[22m');
|
|
118
137
|
}
|
|
119
138
|
function shellQuote(value) {
|
|
120
139
|
if (/^[A-Za-z0-9_./:-]+$/.test(value))
|
|
@@ -129,12 +148,22 @@ function renderedSourceRepoPath(target, sourceType, repoPath) {
|
|
|
129
148
|
}
|
|
130
149
|
function renderPostCreateGuidance(target, cwd) {
|
|
131
150
|
const relativeTarget = relative(cwd, target) || '.';
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
const cdCommand = `cd ${shellQuote(relativeTarget)}`;
|
|
152
|
+
if (!supportsAnsi()) {
|
|
153
|
+
return [
|
|
154
|
+
'Environment is ready. Open it now, or copy these commands:',
|
|
155
|
+
'',
|
|
156
|
+
cdCommand,
|
|
157
|
+
'./moo',
|
|
158
|
+
].join('\n');
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
boldGreen('✓ Environment is ready.'),
|
|
162
|
+
cyan('Open it now, or copy these commands:'),
|
|
134
163
|
'',
|
|
135
|
-
|
|
136
|
-
'./moo',
|
|
137
|
-
].join('\n')
|
|
164
|
+
yellow(cdCommand),
|
|
165
|
+
yellow('./moo'),
|
|
166
|
+
].join('\n');
|
|
138
167
|
}
|
|
139
168
|
function validateRepoName(value) {
|
|
140
169
|
const normalized = value.trim();
|
|
@@ -223,6 +252,99 @@ async function selectCockpitCommandFromMenu() {
|
|
|
223
252
|
}
|
|
224
253
|
return selection.command;
|
|
225
254
|
}
|
|
255
|
+
async function resolveEnvironmentTargetFromPrompts(product, cancelAction) {
|
|
256
|
+
const defaultTarget = `./${product}_dev`;
|
|
257
|
+
while (true) {
|
|
258
|
+
const target = resolve(asString(await textPrompt({
|
|
259
|
+
message: 'Environment folder',
|
|
260
|
+
placeholder: defaultTarget,
|
|
261
|
+
defaultValue: defaultTarget,
|
|
262
|
+
initialValue: defaultTarget,
|
|
263
|
+
}), defaultTarget, cancelAction));
|
|
264
|
+
const state = await inspectEnvironmentTarget(target);
|
|
265
|
+
if (state.kind === 'missing_target') {
|
|
266
|
+
return { kind: 'create', target };
|
|
267
|
+
}
|
|
268
|
+
if (state.kind === 'foreign_target') {
|
|
269
|
+
notePrompt(renderForeignEnvironmentTargetWarning(state), 'Environment folder exists');
|
|
270
|
+
const action = await selectPrompt({
|
|
271
|
+
message: 'What do you want to do?',
|
|
272
|
+
options: [
|
|
273
|
+
{ value: 'choose-another-folder', label: 'Choose another folder' },
|
|
274
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
275
|
+
],
|
|
276
|
+
initialValue: 'choose-another-folder',
|
|
277
|
+
});
|
|
278
|
+
handleCancel(action, cancelAction);
|
|
279
|
+
if (action === 'choose-another-folder') {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
return { kind: 'cancelled' };
|
|
283
|
+
}
|
|
284
|
+
notePrompt(renderExistingEnvironmentSummary(state), 'Existing environment');
|
|
285
|
+
const action = await selectPrompt({
|
|
286
|
+
message: 'This environment folder already exists. What do you want to do?',
|
|
287
|
+
options: [
|
|
288
|
+
{ value: 'update-existing', label: 'Update existing environment' },
|
|
289
|
+
{ value: 'reinstall-environment', label: 'Back up existing environment folder and create a new one' },
|
|
290
|
+
{ value: 'delete-environment', label: 'Delete environment' },
|
|
291
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
292
|
+
],
|
|
293
|
+
initialValue: 'update-existing',
|
|
294
|
+
});
|
|
295
|
+
handleCancel(action, cancelAction);
|
|
296
|
+
if (action === 'update-existing') {
|
|
297
|
+
await safeResetEnvironment({ target, stage: true });
|
|
298
|
+
return { kind: 'updated', target };
|
|
299
|
+
}
|
|
300
|
+
if (action === 'reinstall-environment') {
|
|
301
|
+
const backup = backupTargetPath(target);
|
|
302
|
+
await rename(target, backup);
|
|
303
|
+
notePrompt(`Existing environment moved to:\n${backup}`, 'Environment backup');
|
|
304
|
+
return { kind: 'create', target };
|
|
305
|
+
}
|
|
306
|
+
if (action === 'delete-environment') {
|
|
307
|
+
const expectedName = basename(target);
|
|
308
|
+
const confirmation = await textPrompt({
|
|
309
|
+
message: `Type ${expectedName} to confirm deletion`,
|
|
310
|
+
validate: (value) => (expectedTargetConfirmation(target, value) ? undefined : `Type ${expectedName} exactly to confirm.`),
|
|
311
|
+
});
|
|
312
|
+
handleCancel(confirmation, cancelAction);
|
|
313
|
+
if (!expectedTargetConfirmation(target, String(confirmation))) {
|
|
314
|
+
throw new Error(`Deletion confirmation did not match ${expectedName}.`);
|
|
315
|
+
}
|
|
316
|
+
await rm(target, { recursive: true, force: true });
|
|
317
|
+
return { kind: 'deleted', target };
|
|
318
|
+
}
|
|
319
|
+
return { kind: 'cancelled' };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function promptGitHubPrerequisites(cancelAction) {
|
|
323
|
+
while (true) {
|
|
324
|
+
const status = await getGitHubPrerequisiteStatus();
|
|
325
|
+
if (status.status === 'ready') {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
notePrompt(renderGitHubPrerequisiteGuidance(status), 'GitHub prerequisites');
|
|
329
|
+
const action = await selectPrompt({
|
|
330
|
+
message: 'GitHub repository prerequisites',
|
|
331
|
+
options: [
|
|
332
|
+
{ value: 'retry', label: 'Retry prerequisite check' },
|
|
333
|
+
{ value: 'continue-local-only', label: 'Continue local-only' },
|
|
334
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
335
|
+
],
|
|
336
|
+
initialValue: 'retry',
|
|
337
|
+
});
|
|
338
|
+
handleCancel(action, cancelAction);
|
|
339
|
+
if (action === 'retry') {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (action === 'continue-local-only') {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
throw new Error('GitHub prerequisites were not completed.');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
226
348
|
async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
227
349
|
if (showIntro) {
|
|
228
350
|
introPrompt('Create Odoo dev environment');
|
|
@@ -232,13 +354,11 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
232
354
|
placeholder: 'odoo_sample_module',
|
|
233
355
|
validate: (value) => (value.trim() ? undefined : 'Enter a product/module slug.'),
|
|
234
356
|
}), 'odoo_sample_module', cancelAction);
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
initialValue: defaultTarget,
|
|
241
|
-
}), defaultTarget, cancelAction));
|
|
357
|
+
const targetResult = await resolveEnvironmentTargetFromPrompts(product, cancelAction);
|
|
358
|
+
if (targetResult.kind !== 'create') {
|
|
359
|
+
return targetResult;
|
|
360
|
+
}
|
|
361
|
+
const { target } = targetResult;
|
|
242
362
|
const connectGitHub = await selectPrompt({
|
|
243
363
|
message: 'Connect this environment to Git/GitHub now?',
|
|
244
364
|
options: [
|
|
@@ -249,7 +369,8 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
249
369
|
});
|
|
250
370
|
handleCancel(connectGitHub, cancelAction);
|
|
251
371
|
let selectedGitHubOwner;
|
|
252
|
-
|
|
372
|
+
const useGitHub = Boolean(connectGitHub) && (await promptGitHubPrerequisites(cancelAction));
|
|
373
|
+
if (useGitHub) {
|
|
253
374
|
notePrompt(renderRepositorySetupNote(product), 'Repository setup');
|
|
254
375
|
selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
|
|
255
376
|
}
|
|
@@ -272,23 +393,26 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
272
393
|
handleCancel(installAgentSkills, cancelAction);
|
|
273
394
|
return Boolean(installAgentSkills);
|
|
274
395
|
}
|
|
275
|
-
if (!
|
|
396
|
+
if (!useGitHub) {
|
|
276
397
|
const installAgentSkills = await promptInstallAgentSkills();
|
|
277
398
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
399
|
+
kind: 'create',
|
|
400
|
+
options: {
|
|
401
|
+
product,
|
|
402
|
+
odooVersion,
|
|
403
|
+
engine: 'compose',
|
|
404
|
+
devRepo: basename(target),
|
|
405
|
+
devRepoUrl: target,
|
|
406
|
+
sourceRepos: [],
|
|
407
|
+
target,
|
|
408
|
+
dryRun: false,
|
|
409
|
+
initEmptyRepos: false,
|
|
410
|
+
stage: false,
|
|
411
|
+
agentSkillsTemplateUrl: installAgentSkills ? defaultAgentSkillsTemplateUrl : undefined,
|
|
412
|
+
createMissingRepos: false,
|
|
413
|
+
repoVisibility: 'private',
|
|
414
|
+
skipSubmodules: true,
|
|
415
|
+
},
|
|
292
416
|
};
|
|
293
417
|
}
|
|
294
418
|
const detectedDevRepoUrl = await getOriginUrl(realGit, target);
|
|
@@ -343,19 +467,22 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
343
467
|
});
|
|
344
468
|
handleCancel(initEmpty, cancelAction);
|
|
345
469
|
return {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
470
|
+
kind: 'create',
|
|
471
|
+
options: {
|
|
472
|
+
product,
|
|
473
|
+
odooVersion,
|
|
474
|
+
engine: 'compose',
|
|
475
|
+
devRepo: inferRepoPath(devRepoUrl),
|
|
476
|
+
devRepoUrl,
|
|
477
|
+
sourceRepos,
|
|
478
|
+
target,
|
|
479
|
+
dryRun: false,
|
|
480
|
+
initEmptyRepos: Boolean(initEmpty),
|
|
481
|
+
stage: true,
|
|
482
|
+
agentSkillsTemplateUrl: Boolean(installAgentSkills) ? defaultAgentSkillsTemplateUrl : undefined,
|
|
483
|
+
createMissingRepos: false,
|
|
484
|
+
repoVisibility: 'private',
|
|
485
|
+
},
|
|
359
486
|
};
|
|
360
487
|
}
|
|
361
488
|
async function addRepoOptionsFromArgs(argv) {
|
|
@@ -403,9 +530,10 @@ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit'
|
|
|
403
530
|
};
|
|
404
531
|
}
|
|
405
532
|
async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
406
|
-
|
|
533
|
+
const prerequisiteStatus = await getGitHubPrerequisiteStatus();
|
|
534
|
+
if (prerequisiteStatus.status !== 'ready') {
|
|
407
535
|
notePrompt([
|
|
408
|
-
|
|
536
|
+
renderGitHubPrerequisiteGuidance(prerequisiteStatus),
|
|
409
537
|
'The source repo will be used as-is. If it does not exist, create it first or authenticate gh.',
|
|
410
538
|
].join('\n'), 'Repository check skipped');
|
|
411
539
|
return;
|
|
@@ -711,29 +839,35 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
711
839
|
if (!interactive && !options.createMissingRepos) {
|
|
712
840
|
return;
|
|
713
841
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
'',
|
|
719
|
-
'brew install gh',
|
|
720
|
-
'gh auth login',
|
|
721
|
-
].join('\n');
|
|
722
|
-
if (options.createMissingRepos) {
|
|
842
|
+
const prerequisiteStatus = await getGitHubPrerequisiteStatus();
|
|
843
|
+
if (prerequisiteStatus.status !== 'ready') {
|
|
844
|
+
const message = renderGitHubPrerequisiteGuidance(prerequisiteStatus);
|
|
845
|
+
if (options.createMissingRepos || !interactive) {
|
|
723
846
|
throw new Error(message);
|
|
724
847
|
}
|
|
725
|
-
|
|
726
|
-
notePrompt(message, 'Repository check skipped');
|
|
727
|
-
}
|
|
848
|
+
notePrompt(message, 'GitHub prerequisites');
|
|
728
849
|
return;
|
|
729
850
|
}
|
|
730
|
-
const { accessible, inaccessible: missing } = await checkGitHubRepositories(options);
|
|
731
|
-
if (
|
|
851
|
+
const { accessible, inaccessible: missing, blocked } = await checkGitHubRepositories(options);
|
|
852
|
+
if (blocked.length > 0) {
|
|
853
|
+
const blockedList = blocked
|
|
854
|
+
.map((repository) => `- ${repository.label}: ${repository.slug}`)
|
|
855
|
+
.join('\n');
|
|
732
856
|
notePrompt([
|
|
857
|
+
'These dev environment repositories already contain files and cannot be used automatically:',
|
|
858
|
+
'',
|
|
859
|
+
blockedList,
|
|
860
|
+
'',
|
|
861
|
+
'Choose another dev repository, empty the existing repository, or cancel and handle it manually.',
|
|
862
|
+
].join('\n'), 'Repository check blocked');
|
|
863
|
+
throw new Error(`Dev environment repository is non-empty or could not be verified: ${blocked.map((repo) => repo.slug).join(', ')}`);
|
|
864
|
+
}
|
|
865
|
+
if (interactive && accessible.length > 0) {
|
|
866
|
+
notePrompt(dim([
|
|
733
867
|
'These GitHub repositories already exist and are accessible:',
|
|
734
868
|
'',
|
|
735
869
|
...accessible.map((repository) => `- ${repository.label}: ${repository.slug}`),
|
|
736
|
-
].join('\n'), 'Repository check');
|
|
870
|
+
].join('\n')), 'Repository check');
|
|
737
871
|
}
|
|
738
872
|
if (missing.length === 0) {
|
|
739
873
|
return;
|
|
@@ -773,6 +907,62 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
773
907
|
handleCancel(visibility, 'exit');
|
|
774
908
|
await createGitHubRepositories(missing, visibility);
|
|
775
909
|
}
|
|
910
|
+
async function ensureNonInteractiveCreateTarget(options) {
|
|
911
|
+
if (options.dryRun) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const state = await inspectEnvironmentTarget(options.target);
|
|
915
|
+
if (state.kind === 'missing_target') {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (state.kind === 'existing_environment') {
|
|
919
|
+
throw new Error([
|
|
920
|
+
`Target already contains a WPMoo environment: ${options.target}`,
|
|
921
|
+
'Run `wpmoo reset` to refresh it, or choose another --target.',
|
|
922
|
+
].join('\n'));
|
|
923
|
+
}
|
|
924
|
+
throw new Error(renderForeignEnvironmentTargetWarning(state));
|
|
925
|
+
}
|
|
926
|
+
async function finishCreateFlow(result, cwd, interactive) {
|
|
927
|
+
if (result.kind === 'cancelled') {
|
|
928
|
+
outroPrompt('Create flow cancelled.');
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (result.kind === 'updated') {
|
|
932
|
+
outroPrompt(`Updated existing Odoo dev overlay in ${result.target}.`);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (result.kind === 'deleted') {
|
|
936
|
+
outroPrompt(`Deleted Odoo dev overlay in ${result.target}.`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const { options } = result;
|
|
940
|
+
await ensureGitHubRepositories(options, interactive);
|
|
941
|
+
const scaffoldResult = await scaffold(options);
|
|
942
|
+
if (options.dryRun) {
|
|
943
|
+
console.log('Dry run: planned files');
|
|
944
|
+
for (const file of scaffoldResult.plannedFiles)
|
|
945
|
+
console.log(`- ${file}`);
|
|
946
|
+
console.log('Dry run: planned commands');
|
|
947
|
+
for (const command of scaffoldResult.plannedCommands)
|
|
948
|
+
console.log(`- ${command}`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
notePrompt(renderPostCreateGuidance(options.target, cwd), 'Next steps', { indent: false });
|
|
952
|
+
if (interactive) {
|
|
953
|
+
const shouldOpenCockpit = await confirmPrompt({
|
|
954
|
+
message: 'Open the local WPMoo cockpit now?',
|
|
955
|
+
active: 'Y',
|
|
956
|
+
inactive: 'n',
|
|
957
|
+
initialValue: true,
|
|
958
|
+
});
|
|
959
|
+
if (shouldOpenCockpit === true) {
|
|
960
|
+
await runLocalCockpit(options.target);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
outroPrompt(`Created Odoo dev overlay in ${options.target}. Review staged changes, then commit.`);
|
|
965
|
+
}
|
|
776
966
|
async function runCockpitCommand(command, cwd) {
|
|
777
967
|
if (command.id === 'exit') {
|
|
778
968
|
return 'exit';
|
|
@@ -856,11 +1046,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
856
1046
|
const detection = await detectDevelopmentEnvironment(cwd);
|
|
857
1047
|
if (!detection.isEnvironment) {
|
|
858
1048
|
await showStartup(argv, skipUpdateCheck);
|
|
859
|
-
|
|
860
|
-
await ensureGitHubRepositories(resolvedOptions, true);
|
|
861
|
-
await scaffold(resolvedOptions);
|
|
862
|
-
notePrompt(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
|
|
863
|
-
outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
1049
|
+
await finishCreateFlow(await optionsFromPrompts(), cwd, true);
|
|
864
1050
|
return;
|
|
865
1051
|
}
|
|
866
1052
|
let lastStatus = 'Last: Ready';
|
|
@@ -1003,20 +1189,12 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1003
1189
|
else {
|
|
1004
1190
|
await showStartup(argv, skipUpdateCheck);
|
|
1005
1191
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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}`);
|
|
1192
|
+
if (options) {
|
|
1193
|
+
await ensureNonInteractiveCreateTarget(options);
|
|
1194
|
+
await finishCreateFlow({ kind: 'create', options }, cwd, false);
|
|
1016
1195
|
return;
|
|
1017
1196
|
}
|
|
1018
|
-
|
|
1019
|
-
outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
1197
|
+
await finishCreateFlow(await optionsFromPrompts(), cwd, true);
|
|
1020
1198
|
}
|
|
1021
1199
|
export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
|
|
1022
1200
|
if (!argvPath)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { markerPath, readEnvironmentMetadata } from './environment.js';
|
|
4
|
+
function pad2(value) {
|
|
5
|
+
return value.toString().padStart(2, '0');
|
|
6
|
+
}
|
|
7
|
+
function formatBackupTimestamp(date) {
|
|
8
|
+
return [
|
|
9
|
+
`${date.getUTCFullYear()}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}`,
|
|
10
|
+
`${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}`,
|
|
11
|
+
].join('-');
|
|
12
|
+
}
|
|
13
|
+
async function pathExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await access(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function inspectEnvironmentTarget(target) {
|
|
23
|
+
if (!(await pathExists(target))) {
|
|
24
|
+
return { kind: 'missing_target', target };
|
|
25
|
+
}
|
|
26
|
+
const metadata = await readEnvironmentMetadata(target);
|
|
27
|
+
if (metadata) {
|
|
28
|
+
return { kind: 'existing_environment', target, metadata };
|
|
29
|
+
}
|
|
30
|
+
return { kind: 'foreign_target', target };
|
|
31
|
+
}
|
|
32
|
+
export function renderExistingEnvironmentSummary(state) {
|
|
33
|
+
return [
|
|
34
|
+
`Existing WPMoo environment detected at ${state.target}`,
|
|
35
|
+
`- Product: ${state.metadata.product}`,
|
|
36
|
+
`- Odoo version: ${state.metadata.odooVersion}`,
|
|
37
|
+
`- Source repos: ${state.metadata.sourceRepos.length}`,
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
export function renderForeignEnvironmentTargetWarning(state) {
|
|
41
|
+
return `Target already exists: ${state.target}\nIt does not contain a WPMoo environment marker at ${markerPath}.`;
|
|
42
|
+
}
|
|
43
|
+
export function expectedTargetConfirmation(target, input) {
|
|
44
|
+
return basename(target) === input;
|
|
45
|
+
}
|
|
46
|
+
export function backupTargetPath(target, date = new Date()) {
|
|
47
|
+
return `${target}.backup-${formatBackupTimestamp(date)}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isGitHubAuthenticated, isGitHubCliAvailable, realGitHub } from './github.js';
|
|
2
|
+
export async function getGitHubPrerequisiteStatus(runner = realGitHub) {
|
|
3
|
+
if (!(await isGitHubCliAvailable(runner))) {
|
|
4
|
+
return { status: 'missing', reason: 'gh-missing' };
|
|
5
|
+
}
|
|
6
|
+
if (!(await isGitHubAuthenticated(runner))) {
|
|
7
|
+
return { status: 'unauthenticated', reason: 'gh-unauthenticated' };
|
|
8
|
+
}
|
|
9
|
+
return { status: 'ready' };
|
|
10
|
+
}
|
|
11
|
+
export function renderGitHubPrerequisiteGuidance(status) {
|
|
12
|
+
if (status.status === 'ready') {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
'GitHub CLI (`gh`) is not available or not authenticated.',
|
|
17
|
+
'Install and authenticate it to auto-create missing GitHub repositories:',
|
|
18
|
+
'',
|
|
19
|
+
'brew install gh',
|
|
20
|
+
'gh auth login',
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function runLocalCockpit(target) {
|
|
4
|
+
const child = spawn(join(target, 'moo'), [], {
|
|
5
|
+
cwd: target,
|
|
6
|
+
stdio: 'inherit',
|
|
7
|
+
});
|
|
8
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
9
|
+
child.on('error', reject);
|
|
10
|
+
child.on('close', resolve);
|
|
11
|
+
});
|
|
12
|
+
if (exitCode !== 0) {
|
|
13
|
+
throw new Error(`Local WPMoo cockpit exited with code ${exitCode ?? 'unknown'}: ${join(target, 'moo')}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/prompts/index.js
CHANGED
|
@@ -162,11 +162,12 @@ export function introPrompt(title) {
|
|
|
162
162
|
console.log(title);
|
|
163
163
|
console.log(rule);
|
|
164
164
|
}
|
|
165
|
-
export function notePrompt(message, title = 'Note') {
|
|
165
|
+
export function notePrompt(message, title = 'Note', options = {}) {
|
|
166
166
|
const lines = message.split('\n');
|
|
167
|
+
const prefix = options.indent === false ? '' : ' ';
|
|
167
168
|
console.log(`[${title}]`);
|
|
168
169
|
for (const line of lines) {
|
|
169
|
-
console.log(
|
|
170
|
+
console.log(`${prefix}${line}`);
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
export function outroPrompt(message) {
|
|
@@ -16,19 +16,53 @@ export function repositoryRequirements(options) {
|
|
|
16
16
|
export async function findInaccessibleGitHubRepositories(options, runner = realGitHub) {
|
|
17
17
|
return (await checkGitHubRepositories(options, runner)).inaccessible;
|
|
18
18
|
}
|
|
19
|
+
export async function getGitHubRepositorySize(runner, slug) {
|
|
20
|
+
const result = await runner.run(['api', `repos/${slug}`, '--jq', '.size']);
|
|
21
|
+
const rawSize = result.stdout.trim();
|
|
22
|
+
if (!rawSize) {
|
|
23
|
+
throw new Error(`Unable to parse repository size for ${slug}`);
|
|
24
|
+
}
|
|
25
|
+
const size = Number(rawSize);
|
|
26
|
+
if (!Number.isFinite(size)) {
|
|
27
|
+
throw new Error(`Unable to parse repository size for ${slug}`);
|
|
28
|
+
}
|
|
29
|
+
return size;
|
|
30
|
+
}
|
|
31
|
+
export async function inspectGitHubRepository(runner, repository) {
|
|
32
|
+
try {
|
|
33
|
+
const size = await getGitHubRepositorySize(runner, repository.slug);
|
|
34
|
+
return { status: size === 0 ? 'empty' : 'non-empty', repository };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { status: 'unknown', repository };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
export async function checkGitHubRepositories(options, runner = realGitHub) {
|
|
20
41
|
const accessible = [];
|
|
21
42
|
const inaccessible = [];
|
|
43
|
+
const blocked = [];
|
|
22
44
|
for (const requirement of repositoryRequirements(options)) {
|
|
23
45
|
const status = await getGitHubRepositoryStatus(runner, requirement.url);
|
|
24
46
|
if (status.status === 'accessible') {
|
|
25
|
-
|
|
47
|
+
const repository = { ...requirement, slug: status.slug };
|
|
48
|
+
if (requirement.url === options.devRepoUrl) {
|
|
49
|
+
const inspection = await inspectGitHubRepository(runner, repository);
|
|
50
|
+
if (inspection.status === 'empty') {
|
|
51
|
+
accessible.push(repository);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
blocked.push(repository);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
accessible.push(repository);
|
|
59
|
+
}
|
|
26
60
|
}
|
|
27
61
|
if (status.status === 'inaccessible') {
|
|
28
62
|
inaccessible.push({ ...requirement, slug: status.slug });
|
|
29
63
|
}
|
|
30
64
|
}
|
|
31
|
-
return { accessible, inaccessible };
|
|
65
|
+
return { accessible, inaccessible, blocked };
|
|
32
66
|
}
|
|
33
67
|
export async function createGitHubRepositories(repositories, visibility, runner = realGitHub) {
|
|
34
68
|
for (const repository of repositories) {
|