@wpmoo/toolkit 0.9.1 → 0.9.3
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 +210 -71
- package/dist/environment-target-preflight.js +48 -0
- package/dist/github-prerequisites.js +22 -0
- package/dist/repository-preflight.js +36 -2
- package/dist/update-check.js +0 -3
- 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';
|
|
@@ -21,11 +22,13 @@ import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-
|
|
|
21
22
|
import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
|
|
22
23
|
import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
|
|
23
24
|
import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
|
|
24
|
-
import {
|
|
25
|
+
import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget, renderExistingEnvironmentSummary, renderForeignEnvironmentTargetWarning, } from './environment-target-preflight.js';
|
|
26
|
+
import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
|
|
27
|
+
import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, } from './repository-preflight.js';
|
|
25
28
|
import { scaffold } from './scaffold.js';
|
|
26
29
|
import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
|
|
27
30
|
import { renderBanner } from './templates.js';
|
|
28
|
-
import { checkForUpdate,
|
|
31
|
+
import { checkForUpdate, isUpdateCheckSkipped, restartCli } from './update-check.js';
|
|
29
32
|
import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
|
|
30
33
|
import { environmentStatusJson, getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
|
|
31
34
|
import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
|
|
@@ -202,7 +205,6 @@ async function showStartup(argv, skipUpdateCheck, details) {
|
|
|
202
205
|
handleCancel(shouldUpdate, 'exit');
|
|
203
206
|
if (shouldUpdate) {
|
|
204
207
|
try {
|
|
205
|
-
await installLatestPackage(packageName(), updateCheck.latestVersion);
|
|
206
208
|
const code = await restartCli(packageName(), updateCheck.latestVersion, argv);
|
|
207
209
|
if (code === 0) {
|
|
208
210
|
process.exit(0);
|
|
@@ -224,6 +226,99 @@ async function selectCockpitCommandFromMenu() {
|
|
|
224
226
|
}
|
|
225
227
|
return selection.command;
|
|
226
228
|
}
|
|
229
|
+
async function resolveEnvironmentTargetFromPrompts(product, cancelAction) {
|
|
230
|
+
const defaultTarget = `./${product}_dev`;
|
|
231
|
+
while (true) {
|
|
232
|
+
const target = resolve(asString(await textPrompt({
|
|
233
|
+
message: 'Environment folder',
|
|
234
|
+
placeholder: defaultTarget,
|
|
235
|
+
defaultValue: defaultTarget,
|
|
236
|
+
initialValue: defaultTarget,
|
|
237
|
+
}), defaultTarget, cancelAction));
|
|
238
|
+
const state = await inspectEnvironmentTarget(target);
|
|
239
|
+
if (state.kind === 'missing_target') {
|
|
240
|
+
return { kind: 'create', target };
|
|
241
|
+
}
|
|
242
|
+
if (state.kind === 'foreign_target') {
|
|
243
|
+
notePrompt(renderForeignEnvironmentTargetWarning(state), 'Environment folder exists');
|
|
244
|
+
const action = await selectPrompt({
|
|
245
|
+
message: 'What do you want to do?',
|
|
246
|
+
options: [
|
|
247
|
+
{ value: 'choose-another-folder', label: 'Choose another folder' },
|
|
248
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
249
|
+
],
|
|
250
|
+
initialValue: 'choose-another-folder',
|
|
251
|
+
});
|
|
252
|
+
handleCancel(action, cancelAction);
|
|
253
|
+
if (action === 'choose-another-folder') {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
return { kind: 'cancelled' };
|
|
257
|
+
}
|
|
258
|
+
notePrompt(renderExistingEnvironmentSummary(state), 'Existing environment');
|
|
259
|
+
const action = await selectPrompt({
|
|
260
|
+
message: 'This environment folder already exists. What do you want to do?',
|
|
261
|
+
options: [
|
|
262
|
+
{ value: 'update-existing', label: 'Update existing environment' },
|
|
263
|
+
{ value: 'reinstall-environment', label: 'Reinstall environment from backup' },
|
|
264
|
+
{ value: 'delete-environment', label: 'Delete environment' },
|
|
265
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
266
|
+
],
|
|
267
|
+
initialValue: 'update-existing',
|
|
268
|
+
});
|
|
269
|
+
handleCancel(action, cancelAction);
|
|
270
|
+
if (action === 'update-existing') {
|
|
271
|
+
await safeResetEnvironment({ target, stage: true });
|
|
272
|
+
return { kind: 'updated', target };
|
|
273
|
+
}
|
|
274
|
+
if (action === 'reinstall-environment') {
|
|
275
|
+
const backup = backupTargetPath(target);
|
|
276
|
+
await rename(target, backup);
|
|
277
|
+
notePrompt(`Existing environment moved to:\n${backup}`, 'Environment backup');
|
|
278
|
+
return { kind: 'create', target };
|
|
279
|
+
}
|
|
280
|
+
if (action === 'delete-environment') {
|
|
281
|
+
const expectedName = basename(target);
|
|
282
|
+
const confirmation = await textPrompt({
|
|
283
|
+
message: `Type ${expectedName} to confirm deletion`,
|
|
284
|
+
validate: (value) => (expectedTargetConfirmation(target, value) ? undefined : `Type ${expectedName} exactly to confirm.`),
|
|
285
|
+
});
|
|
286
|
+
handleCancel(confirmation, cancelAction);
|
|
287
|
+
if (!expectedTargetConfirmation(target, String(confirmation))) {
|
|
288
|
+
throw new Error(`Deletion confirmation did not match ${expectedName}.`);
|
|
289
|
+
}
|
|
290
|
+
await rm(target, { recursive: true, force: true });
|
|
291
|
+
return { kind: 'deleted', target };
|
|
292
|
+
}
|
|
293
|
+
return { kind: 'cancelled' };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function promptGitHubPrerequisites(cancelAction) {
|
|
297
|
+
while (true) {
|
|
298
|
+
const status = await getGitHubPrerequisiteStatus();
|
|
299
|
+
if (status.status === 'ready') {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
notePrompt(renderGitHubPrerequisiteGuidance(status), 'GitHub prerequisites');
|
|
303
|
+
const action = await selectPrompt({
|
|
304
|
+
message: 'GitHub repository prerequisites',
|
|
305
|
+
options: [
|
|
306
|
+
{ value: 'retry', label: 'Retry prerequisite check' },
|
|
307
|
+
{ value: 'continue-local-only', label: 'Continue local-only' },
|
|
308
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
309
|
+
],
|
|
310
|
+
initialValue: 'retry',
|
|
311
|
+
});
|
|
312
|
+
handleCancel(action, cancelAction);
|
|
313
|
+
if (action === 'retry') {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (action === 'continue-local-only') {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
throw new Error('GitHub prerequisites were not completed.');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
227
322
|
async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
228
323
|
if (showIntro) {
|
|
229
324
|
introPrompt('Create Odoo dev environment');
|
|
@@ -233,13 +328,11 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
233
328
|
placeholder: 'odoo_sample_module',
|
|
234
329
|
validate: (value) => (value.trim() ? undefined : 'Enter a product/module slug.'),
|
|
235
330
|
}), 'odoo_sample_module', cancelAction);
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
initialValue: defaultTarget,
|
|
242
|
-
}), defaultTarget, cancelAction));
|
|
331
|
+
const targetResult = await resolveEnvironmentTargetFromPrompts(product, cancelAction);
|
|
332
|
+
if (targetResult.kind !== 'create') {
|
|
333
|
+
return targetResult;
|
|
334
|
+
}
|
|
335
|
+
const { target } = targetResult;
|
|
243
336
|
const connectGitHub = await selectPrompt({
|
|
244
337
|
message: 'Connect this environment to Git/GitHub now?',
|
|
245
338
|
options: [
|
|
@@ -250,7 +343,8 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
250
343
|
});
|
|
251
344
|
handleCancel(connectGitHub, cancelAction);
|
|
252
345
|
let selectedGitHubOwner;
|
|
253
|
-
|
|
346
|
+
const useGitHub = Boolean(connectGitHub) && (await promptGitHubPrerequisites(cancelAction));
|
|
347
|
+
if (useGitHub) {
|
|
254
348
|
notePrompt(renderRepositorySetupNote(product), 'Repository setup');
|
|
255
349
|
selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
|
|
256
350
|
}
|
|
@@ -273,23 +367,26 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
273
367
|
handleCancel(installAgentSkills, cancelAction);
|
|
274
368
|
return Boolean(installAgentSkills);
|
|
275
369
|
}
|
|
276
|
-
if (!
|
|
370
|
+
if (!useGitHub) {
|
|
277
371
|
const installAgentSkills = await promptInstallAgentSkills();
|
|
278
372
|
return {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
373
|
+
kind: 'create',
|
|
374
|
+
options: {
|
|
375
|
+
product,
|
|
376
|
+
odooVersion,
|
|
377
|
+
engine: 'compose',
|
|
378
|
+
devRepo: basename(target),
|
|
379
|
+
devRepoUrl: target,
|
|
380
|
+
sourceRepos: [],
|
|
381
|
+
target,
|
|
382
|
+
dryRun: false,
|
|
383
|
+
initEmptyRepos: false,
|
|
384
|
+
stage: false,
|
|
385
|
+
agentSkillsTemplateUrl: installAgentSkills ? defaultAgentSkillsTemplateUrl : undefined,
|
|
386
|
+
createMissingRepos: false,
|
|
387
|
+
repoVisibility: 'private',
|
|
388
|
+
skipSubmodules: true,
|
|
389
|
+
},
|
|
293
390
|
};
|
|
294
391
|
}
|
|
295
392
|
const detectedDevRepoUrl = await getOriginUrl(realGit, target);
|
|
@@ -344,19 +441,22 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
344
441
|
});
|
|
345
442
|
handleCancel(initEmpty, cancelAction);
|
|
346
443
|
return {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
444
|
+
kind: 'create',
|
|
445
|
+
options: {
|
|
446
|
+
product,
|
|
447
|
+
odooVersion,
|
|
448
|
+
engine: 'compose',
|
|
449
|
+
devRepo: inferRepoPath(devRepoUrl),
|
|
450
|
+
devRepoUrl,
|
|
451
|
+
sourceRepos,
|
|
452
|
+
target,
|
|
453
|
+
dryRun: false,
|
|
454
|
+
initEmptyRepos: Boolean(initEmpty),
|
|
455
|
+
stage: true,
|
|
456
|
+
agentSkillsTemplateUrl: Boolean(installAgentSkills) ? defaultAgentSkillsTemplateUrl : undefined,
|
|
457
|
+
createMissingRepos: false,
|
|
458
|
+
repoVisibility: 'private',
|
|
459
|
+
},
|
|
360
460
|
};
|
|
361
461
|
}
|
|
362
462
|
async function addRepoOptionsFromArgs(argv) {
|
|
@@ -404,9 +504,10 @@ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit'
|
|
|
404
504
|
};
|
|
405
505
|
}
|
|
406
506
|
async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
407
|
-
|
|
507
|
+
const prerequisiteStatus = await getGitHubPrerequisiteStatus();
|
|
508
|
+
if (prerequisiteStatus.status !== 'ready') {
|
|
408
509
|
notePrompt([
|
|
409
|
-
|
|
510
|
+
renderGitHubPrerequisiteGuidance(prerequisiteStatus),
|
|
410
511
|
'The source repo will be used as-is. If it does not exist, create it first or authenticate gh.',
|
|
411
512
|
].join('\n'), 'Repository check skipped');
|
|
412
513
|
return;
|
|
@@ -712,23 +813,29 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
712
813
|
if (!interactive && !options.createMissingRepos) {
|
|
713
814
|
return;
|
|
714
815
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
'',
|
|
720
|
-
'brew install gh',
|
|
721
|
-
'gh auth login',
|
|
722
|
-
].join('\n');
|
|
723
|
-
if (options.createMissingRepos) {
|
|
816
|
+
const prerequisiteStatus = await getGitHubPrerequisiteStatus();
|
|
817
|
+
if (prerequisiteStatus.status !== 'ready') {
|
|
818
|
+
const message = renderGitHubPrerequisiteGuidance(prerequisiteStatus);
|
|
819
|
+
if (options.createMissingRepos || !interactive) {
|
|
724
820
|
throw new Error(message);
|
|
725
821
|
}
|
|
726
|
-
|
|
727
|
-
notePrompt(message, 'Repository check skipped');
|
|
728
|
-
}
|
|
822
|
+
notePrompt(message, 'GitHub prerequisites');
|
|
729
823
|
return;
|
|
730
824
|
}
|
|
731
|
-
const { accessible, inaccessible: missing } = await checkGitHubRepositories(options);
|
|
825
|
+
const { accessible, inaccessible: missing, blocked } = await checkGitHubRepositories(options);
|
|
826
|
+
if (blocked.length > 0) {
|
|
827
|
+
const blockedList = blocked
|
|
828
|
+
.map((repository) => `- ${repository.label}: ${repository.slug}`)
|
|
829
|
+
.join('\n');
|
|
830
|
+
notePrompt([
|
|
831
|
+
'These dev environment repositories already contain files and cannot be used automatically:',
|
|
832
|
+
'',
|
|
833
|
+
blockedList,
|
|
834
|
+
'',
|
|
835
|
+
'Choose another dev repository, empty the existing repository, or cancel and handle it manually.',
|
|
836
|
+
].join('\n'), 'Repository check blocked');
|
|
837
|
+
throw new Error(`Dev environment repository is non-empty or could not be verified: ${blocked.map((repo) => repo.slug).join(', ')}`);
|
|
838
|
+
}
|
|
732
839
|
if (interactive && accessible.length > 0) {
|
|
733
840
|
notePrompt([
|
|
734
841
|
'These GitHub repositories already exist and are accessible:',
|
|
@@ -774,6 +881,50 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
774
881
|
handleCancel(visibility, 'exit');
|
|
775
882
|
await createGitHubRepositories(missing, visibility);
|
|
776
883
|
}
|
|
884
|
+
async function ensureNonInteractiveCreateTarget(options) {
|
|
885
|
+
if (options.dryRun) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const state = await inspectEnvironmentTarget(options.target);
|
|
889
|
+
if (state.kind === 'missing_target') {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (state.kind === 'existing_environment') {
|
|
893
|
+
throw new Error([
|
|
894
|
+
`Target already contains a WPMoo environment: ${options.target}`,
|
|
895
|
+
'Run `wpmoo reset` to refresh it, or choose another --target.',
|
|
896
|
+
].join('\n'));
|
|
897
|
+
}
|
|
898
|
+
throw new Error(renderForeignEnvironmentTargetWarning(state));
|
|
899
|
+
}
|
|
900
|
+
async function finishCreateFlow(result, cwd, interactive) {
|
|
901
|
+
if (result.kind === 'cancelled') {
|
|
902
|
+
outroPrompt('Create flow cancelled.');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (result.kind === 'updated') {
|
|
906
|
+
outroPrompt(`Updated existing Odoo dev overlay in ${result.target}.`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (result.kind === 'deleted') {
|
|
910
|
+
outroPrompt(`Deleted Odoo dev overlay in ${result.target}.`);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const { options } = result;
|
|
914
|
+
await ensureGitHubRepositories(options, interactive);
|
|
915
|
+
const scaffoldResult = await scaffold(options);
|
|
916
|
+
if (options.dryRun) {
|
|
917
|
+
console.log('Dry run: planned files');
|
|
918
|
+
for (const file of scaffoldResult.plannedFiles)
|
|
919
|
+
console.log(`- ${file}`);
|
|
920
|
+
console.log('Dry run: planned commands');
|
|
921
|
+
for (const command of scaffoldResult.plannedCommands)
|
|
922
|
+
console.log(`- ${command}`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
notePrompt(renderPostCreateGuidance(options.target, cwd), 'Next steps');
|
|
926
|
+
outroPrompt(`Created Odoo dev overlay in ${options.target}. Review staged changes, then commit.`);
|
|
927
|
+
}
|
|
777
928
|
async function runCockpitCommand(command, cwd) {
|
|
778
929
|
if (command.id === 'exit') {
|
|
779
930
|
return 'exit';
|
|
@@ -857,11 +1008,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
857
1008
|
const detection = await detectDevelopmentEnvironment(cwd);
|
|
858
1009
|
if (!detection.isEnvironment) {
|
|
859
1010
|
await showStartup(argv, skipUpdateCheck);
|
|
860
|
-
|
|
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.`);
|
|
1011
|
+
await finishCreateFlow(await optionsFromPrompts(), cwd, true);
|
|
865
1012
|
return;
|
|
866
1013
|
}
|
|
867
1014
|
let lastStatus = 'Last: Ready';
|
|
@@ -1004,20 +1151,12 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1004
1151
|
else {
|
|
1005
1152
|
await showStartup(argv, skipUpdateCheck);
|
|
1006
1153
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (resolvedOptions.dryRun) {
|
|
1011
|
-
console.log('Dry run: planned files');
|
|
1012
|
-
for (const file of result.plannedFiles)
|
|
1013
|
-
console.log(`- ${file}`);
|
|
1014
|
-
console.log('Dry run: planned commands');
|
|
1015
|
-
for (const command of result.plannedCommands)
|
|
1016
|
-
console.log(`- ${command}`);
|
|
1154
|
+
if (options) {
|
|
1155
|
+
await ensureNonInteractiveCreateTarget(options);
|
|
1156
|
+
await finishCreateFlow({ kind: 'create', options }, cwd, false);
|
|
1017
1157
|
return;
|
|
1018
1158
|
}
|
|
1019
|
-
|
|
1020
|
-
outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
1159
|
+
await finishCreateFlow(await optionsFromPrompts(), cwd, true);
|
|
1021
1160
|
}
|
|
1022
1161
|
export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
|
|
1023
1162
|
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
|
+
}
|
|
@@ -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) {
|
package/dist/update-check.js
CHANGED
|
@@ -85,9 +85,6 @@ export async function checkForUpdate(packageName, currentVersion, runner = realN
|
|
|
85
85
|
export function packageSpec(packageName, version) {
|
|
86
86
|
return `${packageName}@${version}`;
|
|
87
87
|
}
|
|
88
|
-
export async function installLatestPackage(packageName, version, runner = realNpm) {
|
|
89
|
-
await runner.run(['install', '-g', packageSpec(packageName, version)]);
|
|
90
|
-
}
|
|
91
88
|
export function restartArgs(packageName, version, argv) {
|
|
92
89
|
return ['exec', '--yes', '--package', packageSpec(packageName, version), '--', 'wpmoo', ...argv];
|
|
93
90
|
}
|