@wpmoo/odoo 0.8.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ import { readEnvironmentMetadata } from './environment.js';
2
+ import { inferGitHubOwner } from './repo-url.js';
3
+ export async function environmentGitHubOwner(target) {
4
+ const metadata = await readEnvironmentMetadata(target);
5
+ return inferGitHubOwner(metadata?.devRepoUrl ?? '');
6
+ }
7
+ export async function environmentProduct(target) {
8
+ const metadata = await readEnvironmentMetadata(target);
9
+ return metadata?.product;
10
+ }
@@ -0,0 +1,5 @@
1
+ import { environmentOdooVersion } from './environment.js';
2
+ export async function commandOdooVersion(target, explicitVersion) {
3
+ const normalizedVersion = explicitVersion?.trim();
4
+ return normalizedVersion || environmentOdooVersion(target);
5
+ }
@@ -0,0 +1,61 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { packageName, packageVersion } from './version.js';
4
+ export const markerPath = '.wpmoo/odoo.json';
5
+ export const defaultOdooVersion = '19.0';
6
+ async function exists(path) {
7
+ try {
8
+ await access(path);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export function environmentMetadata(options) {
16
+ return {
17
+ tool: packageName(),
18
+ version: packageVersion(),
19
+ product: options.product,
20
+ odooVersion: options.odooVersion,
21
+ devRepo: options.devRepo,
22
+ devRepoUrl: options.devRepoUrl,
23
+ sourceRepos: options.sourceRepos,
24
+ engine: options.engine ?? 'compose',
25
+ composeTemplateUrl: options.composeTemplateUrl,
26
+ composeTemplateRef: options.composeTemplateRef,
27
+ agentSkillsTemplateUrl: options.agentSkillsTemplateUrl,
28
+ agentSkillsTemplateRef: options.agentSkillsTemplateRef,
29
+ postgresVersion: options.postgresVersion,
30
+ httpPort: options.httpPort,
31
+ geventPort: options.geventPort,
32
+ };
33
+ }
34
+ export function renderEnvironmentMetadata(options) {
35
+ return `${JSON.stringify(environmentMetadata(options), null, 2)}\n`;
36
+ }
37
+ export async function readEnvironmentMetadata(target) {
38
+ try {
39
+ const content = await readFile(join(target, markerPath), 'utf8');
40
+ return JSON.parse(content);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ export async function detectDevelopmentEnvironment(target) {
47
+ if (await readEnvironmentMetadata(target)) {
48
+ return { isEnvironment: true, source: 'marker' };
49
+ }
50
+ const hasAddonsYaml = await exists(join(target, 'odoo/custom/src/addons.yaml'));
51
+ const hasReposYaml = await exists(join(target, 'odoo/custom/src/repos.yaml'));
52
+ const hasPrivateDir = await exists(join(target, 'odoo/custom/src/private'));
53
+ if (hasAddonsYaml && hasReposYaml && hasPrivateDir) {
54
+ return { isEnvironment: true, source: 'layout' };
55
+ }
56
+ return { isEnvironment: false, source: 'none' };
57
+ }
58
+ export async function environmentOdooVersion(target) {
59
+ const metadata = await readEnvironmentMetadata(target);
60
+ return metadata?.odooVersion || defaultOdooVersion;
61
+ }
@@ -0,0 +1,113 @@
1
+ import { cp, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { realGit } from './git.js';
5
+ const defaultExcludes = ['.git', 'node_modules', '.DS_Store'];
6
+ async function pathExists(path) {
7
+ try {
8
+ await stat(path);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function expandHome(path) {
16
+ if (path === '~')
17
+ return process.env.HOME ?? path;
18
+ if (path.startsWith('~/'))
19
+ return join(process.env.HOME ?? '~', path.slice(2));
20
+ return path;
21
+ }
22
+ export function gitUrlFromSource(source) {
23
+ if (source.startsWith('gh:')) {
24
+ return `https://github.com/${source.slice(3).replace(/\.git$/, '')}.git`;
25
+ }
26
+ if (source.startsWith('git:github.com/')) {
27
+ return `https://github.com/${source.slice('git:github.com/'.length).replace(/\.git$/, '')}.git`;
28
+ }
29
+ if (/^(https?:|ssh:|git:)/.test(source) || /^[^\s@]+@[^\s:]+:.+/.test(source)) {
30
+ return source;
31
+ }
32
+ return undefined;
33
+ }
34
+ async function checkoutSource(git, source, ref) {
35
+ const gitUrl = gitUrlFromSource(source);
36
+ if (!gitUrl) {
37
+ return { root: resolve(expandHome(source)) };
38
+ }
39
+ const tempRoot = await mkdtemp(join(tmpdir(), 'wpmoo-external-'));
40
+ const cloneDir = join(tempRoot, 'source');
41
+ const cloneArgs = ['clone', '--depth', '1'];
42
+ if (ref) {
43
+ cloneArgs.push('--branch', ref);
44
+ }
45
+ cloneArgs.push(gitUrl, cloneDir);
46
+ try {
47
+ await git.run(tempRoot, cloneArgs);
48
+ }
49
+ catch (error) {
50
+ if (!ref) {
51
+ await rm(tempRoot, { recursive: true, force: true });
52
+ throw error;
53
+ }
54
+ await rm(cloneDir, { recursive: true, force: true });
55
+ await git.run(tempRoot, ['clone', gitUrl, cloneDir]);
56
+ await git.run(cloneDir, ['checkout', ref]);
57
+ }
58
+ return { root: cloneDir, cleanup: tempRoot };
59
+ }
60
+ function isExcluded(relativePath, excludes) {
61
+ const normalized = relativePath.split('\\').join('/');
62
+ return excludes.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
63
+ }
64
+ async function copyDirectory(options, checkedOut) {
65
+ const sourcePath = options.sourceSubdir ? join(checkedOut.root, options.sourceSubdir) : checkedOut.root;
66
+ const destinationPath = options.destinationSubdir
67
+ ? join(options.destination, options.destinationSubdir)
68
+ : options.destination;
69
+ if (!(await pathExists(sourcePath))) {
70
+ throw new Error(`External asset source path does not exist: ${sourcePath}`);
71
+ }
72
+ const excludes = [...defaultExcludes, ...(options.exclude ?? [])];
73
+ await mkdir(destinationPath, { recursive: true });
74
+ await cp(sourcePath, destinationPath, {
75
+ recursive: true,
76
+ force: true,
77
+ filter: (source) => {
78
+ const rel = relative(sourcePath, source);
79
+ return !rel || !isExcluded(rel, excludes);
80
+ },
81
+ });
82
+ if (options.readmeDestination) {
83
+ const readmePath = join(checkedOut.root, 'README.md');
84
+ if (await pathExists(readmePath)) {
85
+ const destination = join(options.destination, options.readmeDestination);
86
+ await mkdir(dirname(destination), { recursive: true });
87
+ await cp(readmePath, destination, { force: true });
88
+ }
89
+ }
90
+ }
91
+ export function renderExternalAssetCommand(options) {
92
+ const sourcePath = options.sourceSubdir ? `${options.source}/${options.sourceSubdir}` : options.source;
93
+ const destinationPath = options.destinationSubdir
94
+ ? `${options.destination}/${options.destinationSubdir}`
95
+ : options.destination;
96
+ const ref = options.ref ? `#${options.ref}` : '';
97
+ return `copy external ${options.label}: ${sourcePath}${ref} -> ${destinationPath}`;
98
+ }
99
+ export async function applyExternalAsset(options, git = realGit) {
100
+ const checkedOut = await checkoutSource(git, options.source, options.ref);
101
+ try {
102
+ await copyDirectory(options, checkedOut);
103
+ }
104
+ finally {
105
+ if (checkedOut.cleanup) {
106
+ await rm(checkedOut.cleanup, { recursive: true, force: true });
107
+ }
108
+ }
109
+ }
110
+ export async function writeTextFile(path, content) {
111
+ await mkdir(dirname(path), { recursive: true });
112
+ await writeFile(path, content, 'utf8');
113
+ }
@@ -0,0 +1,69 @@
1
+ export const defaultComposeTemplateUrl = 'gh:wpmoo-org/odoo-docker-compose';
2
+ export const defaultAgentSkillsTemplateUrl = 'gh:wpmoo-org/odoo-skills';
3
+ function odooMajorVersion(odooVersion) {
4
+ return odooVersion.split('.', 1)[0] || odooVersion;
5
+ }
6
+ export function defaultPostgresVersion(odooVersion) {
7
+ const major = odooMajorVersion(odooVersion);
8
+ if (major === '19')
9
+ return '18';
10
+ if (major === '18')
11
+ return '17';
12
+ if (major === '17')
13
+ return '15';
14
+ if (major === '16')
15
+ return '14';
16
+ return '17';
17
+ }
18
+ export function defaultHttpPort(odooVersion) {
19
+ const major = odooMajorVersion(odooVersion).padStart(2, '0');
20
+ return `100${major}`;
21
+ }
22
+ export function defaultGeventPort(odooVersion) {
23
+ const major = odooMajorVersion(odooVersion).padStart(2, '0');
24
+ return `200${major}`;
25
+ }
26
+ export function defaultTestModule(options) {
27
+ return options.sourceRepos.flatMap((repo) => repo.addons)[0] ?? options.product;
28
+ }
29
+ export function renderComposeEnvExample(options) {
30
+ return [
31
+ '# Copy to .env and edit for local development.',
32
+ `ODOO_VERSION=${options.odooVersion}`,
33
+ `ODOO_IMAGE=odoo:${odooMajorVersion(options.odooVersion)}`,
34
+ `POSTGRES_IMAGE=postgres:${options.postgresVersion ?? defaultPostgresVersion(options.odooVersion)}`,
35
+ `HTTP_PORT=${options.httpPort ?? defaultHttpPort(options.odooVersion)}`,
36
+ `GEVENT_PORT=${options.geventPort ?? defaultGeventPort(options.odooVersion)}`,
37
+ 'POSTGRES_PASSWORD=odoo',
38
+ 'ODOO_MASTER_PASSWORD=admin',
39
+ `ODOO_TEST_MODULE=${defaultTestModule(options)}`,
40
+ '',
41
+ ].join('\n');
42
+ }
43
+ export function composeTemplateOptions(options) {
44
+ return {
45
+ label: 'compose',
46
+ source: options.composeTemplateUrl ?? defaultComposeTemplateUrl,
47
+ destination: options.target,
48
+ ref: options.composeTemplateRef,
49
+ exclude: ['README.md', 'README-template.md', '.gitignore', 'LICENSE', 'package.json', 'package-lock.json'],
50
+ readmeDestination: 'docs/compose.md',
51
+ };
52
+ }
53
+ export function agentSkillsTemplateOptions(options) {
54
+ if (!options.agentSkillsTemplateUrl) {
55
+ return undefined;
56
+ }
57
+ return {
58
+ label: 'agent-skills',
59
+ source: options.agentSkillsTemplateUrl,
60
+ destination: options.target,
61
+ ref: options.agentSkillsTemplateRef,
62
+ sourceSubdir: 'skills',
63
+ destinationSubdir: '.agents/skills',
64
+ exclude: ['package.json', 'package-lock.json'],
65
+ };
66
+ }
67
+ export function plannedExternalAssetOptions(options) {
68
+ return [composeTemplateOptions(options), agentSkillsTemplateOptions(options)].filter((assetOptions) => Boolean(assetOptions));
69
+ }
package/dist/git.js ADDED
@@ -0,0 +1,98 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, join } from 'node:path';
4
+ import { execa } from 'execa';
5
+ export const realGit = {
6
+ async run(cwd, args) {
7
+ const result = await execa('git', args, {
8
+ cwd,
9
+ env: {
10
+ ...process.env,
11
+ GIT_ALLOW_PROTOCOL: 'file:https:ssh:git',
12
+ },
13
+ });
14
+ return { stdout: result.stdout, stderr: result.stderr };
15
+ },
16
+ };
17
+ export async function hasRemoteHeads(git, cwd, repoUrl) {
18
+ const result = await git.run(cwd, ['ls-remote', '--heads', repoUrl]);
19
+ return result.stdout.trim().length > 0;
20
+ }
21
+ export async function initializeEmptyRemote(git, cwd, repoUrl, branch) {
22
+ const tempRoot = await mkdtemp(join(tmpdir(), 'wpmoo-init-'));
23
+ const cloneDir = join(tempRoot, basename(repoUrl).replace(/\.git$/, '') || 'repo');
24
+ try {
25
+ await git.run(tempRoot, ['clone', repoUrl, cloneDir]);
26
+ await git.run(cloneDir, ['config', 'user.name', 'Create Odoo Dev Bot']);
27
+ await git.run(cloneDir, ['config', 'user.email', 'dev@example.com']);
28
+ await git.run(cloneDir, ['commit', '--allow-empty', '-m', 'Initial commit']);
29
+ await git.run(cloneDir, ['push', 'origin', `HEAD:${branch}`]);
30
+ }
31
+ finally {
32
+ await rm(tempRoot, { recursive: true, force: true });
33
+ }
34
+ }
35
+ export async function ensureRemoteHasBranch(git, cwd, repoUrl, branch, initEmptyRepos) {
36
+ const hasHeads = await hasRemoteHeads(git, cwd, repoUrl);
37
+ if (!hasHeads) {
38
+ if (!initEmptyRepos) {
39
+ throw new Error(`Repository has no commits: ${repoUrl}`);
40
+ }
41
+ await initializeEmptyRemote(git, cwd, repoUrl, branch);
42
+ return;
43
+ }
44
+ const branchResult = await git.run(cwd, ['ls-remote', '--heads', repoUrl, branch]);
45
+ if (!branchResult.stdout.trim()) {
46
+ throw new Error(`Repository ${repoUrl} does not have branch ${branch}`);
47
+ }
48
+ }
49
+ export async function addSubmodule(git, target, repoUrl, branch, path) {
50
+ await git.run(target, ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-b', branch, repoUrl, path]);
51
+ }
52
+ export async function isTrackedPath(git, target, path) {
53
+ try {
54
+ await git.run(target, ['ls-files', '--error-unmatch', path]);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ export async function ensureSubmodule(git, target, repoUrl, branch, path) {
62
+ if (await isTrackedPath(git, target, path)) {
63
+ await git.run(target, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init', '--recursive', path]);
64
+ return;
65
+ }
66
+ await addSubmodule(git, target, repoUrl, branch, path);
67
+ }
68
+ export async function hasUncommittedChanges(git, cwd) {
69
+ const result = await git.run(cwd, ['status', '--porcelain']);
70
+ return result.stdout.trim().length > 0;
71
+ }
72
+ export async function removeSubmodule(git, target, path) {
73
+ try {
74
+ await git.run(target, ['submodule', 'deinit', '-f', path]);
75
+ }
76
+ catch {
77
+ // `git rm` below is the authoritative removal; deinit can fail for partially initialized submodules.
78
+ }
79
+ await git.run(target, ['rm', '-f', path]);
80
+ }
81
+ export async function cloneRepository(git, cwd, repoUrl, target) {
82
+ await git.run(cwd, ['clone', repoUrl, target]);
83
+ }
84
+ export async function syncSubmodules(git, target) {
85
+ await git.run(target, ['submodule', 'sync', '--recursive']);
86
+ }
87
+ export async function stageAll(git, target) {
88
+ await git.run(target, ['add', '.']);
89
+ }
90
+ export async function getOriginUrl(git, target) {
91
+ try {
92
+ const result = await git.run(target, ['remote', 'get-url', 'origin']);
93
+ return result.stdout.trim() || undefined;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
package/dist/github.js ADDED
@@ -0,0 +1,87 @@
1
+ import { execa } from 'execa';
2
+ export const realGitHub = {
3
+ async run(args) {
4
+ const result = await execa('gh', args);
5
+ return { stdout: result.stdout, stderr: result.stderr };
6
+ },
7
+ };
8
+ export function parseGitHubRepoUrl(repoUrl) {
9
+ const normalized = repoUrl.trim().replace(/[?#].*$/, '').replace(/\/+$/, '').replace(/\.git$/, '');
10
+ const httpsMatch = normalized.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/);
11
+ if (httpsMatch) {
12
+ return { owner: httpsMatch[1], name: httpsMatch[2] };
13
+ }
14
+ const sshMatch = normalized.match(/^git@github\.com:([^/]+)\/([^/]+)$/);
15
+ if (sshMatch) {
16
+ return { owner: sshMatch[1], name: sshMatch[2] };
17
+ }
18
+ return undefined;
19
+ }
20
+ export function githubSlug(repoUrl) {
21
+ const repo = parseGitHubRepoUrl(repoUrl);
22
+ return repo ? `${repo.owner}/${repo.name}` : undefined;
23
+ }
24
+ export function githubRepositoryUrl(owner, repo) {
25
+ return `https://github.com/${owner}/${repo}.git`;
26
+ }
27
+ export async function isGitHubCliAvailable(runner = realGitHub) {
28
+ try {
29
+ await runner.run(['--version']);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ export async function getAuthenticatedGitHubLogin(runner = realGitHub) {
37
+ const result = await runner.run(['api', 'user', '--jq', '.login']);
38
+ const login = result.stdout.trim();
39
+ if (!login) {
40
+ throw new Error('GitHub CLI is not authenticated');
41
+ }
42
+ return login;
43
+ }
44
+ export async function isGitHubAuthenticated(runner = realGitHub) {
45
+ try {
46
+ await getAuthenticatedGitHubLogin(runner);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export async function listGitHubOrganizations(runner = realGitHub) {
54
+ const result = await runner.run(['api', 'user/orgs', '--jq', '.[].login']);
55
+ return result.stdout
56
+ .split('\n')
57
+ .map((line) => line.trim())
58
+ .filter(Boolean);
59
+ }
60
+ export async function getGitHubAccounts(runner = realGitHub) {
61
+ const user = await getAuthenticatedGitHubLogin(runner);
62
+ const organizations = await listGitHubOrganizations(runner);
63
+ return [
64
+ { login: user, type: 'user' },
65
+ ...organizations.map((login) => ({ login, type: 'organization' })),
66
+ ];
67
+ }
68
+ export async function getGitHubRepositoryStatus(runner, repoUrl) {
69
+ const slug = githubSlug(repoUrl);
70
+ if (!slug) {
71
+ return { status: 'unsupported' };
72
+ }
73
+ try {
74
+ await runner.run(['repo', 'view', slug, '--json', 'name']);
75
+ return { status: 'accessible', slug };
76
+ }
77
+ catch {
78
+ return { status: 'inaccessible', slug };
79
+ }
80
+ }
81
+ export async function createGitHubRepository(runner, repoUrl, visibility) {
82
+ const slug = githubSlug(repoUrl);
83
+ if (!slug) {
84
+ throw new Error(`Only GitHub repository URLs can be created automatically: ${repoUrl}`);
85
+ }
86
+ await runner.run(['repo', 'create', slug, visibility === 'private' ? '--private' : '--public']);
87
+ }
package/dist/help.js ADDED
@@ -0,0 +1,62 @@
1
+ export function renderHelp() {
2
+ return `@wpmoo/odoo
3
+
4
+ WPMoo Odoo lifecycle tooling.
5
+
6
+ Usage:
7
+ npx @wpmoo/odoo
8
+ npx @wpmoo/odoo create --product <slug> --dev-repo-url <url> --source-repo-url <url>
9
+ npx @wpmoo/odoo add-repo --repo-url <url>
10
+ npx @wpmoo/odoo remove-repo --repo <name>
11
+ npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
12
+ npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
13
+ npx @wpmoo/odoo reset
14
+
15
+ Options:
16
+ --product <slug> Product slug, for example my_odoo_module.
17
+ --odoo-version <branch> Odoo branch to pin submodules to. Default: 19.0.
18
+ --dev-repo-url <url> Development environment repository URL for docs.
19
+ --target <path> Target dev repo directory. Default: ./<product>_dev.
20
+ --engine <value> Environment engine: compose. Default: compose.
21
+ --compose-template-url <url> Standalone compose resource source. Default: gh:wpmoo-org/odoo-docker-compose.
22
+ --compose-template-ref <ref> Git ref for the compose resource.
23
+ --agent-skills-template Install project Agent Skills from a standalone skills resource.
24
+ --agent-skills-template-url <url>
25
+ Agent Skills resource source. Default: gh:wpmoo-org/odoo-skills.
26
+ --agent-skills-template-ref <ref>
27
+ Git ref for the Agent Skills resource.
28
+ --postgres-version <value> PostgreSQL image version written to compose .env.example.
29
+ --http-port <port> Host HTTP port written to .env.example.
30
+ --gevent-port <port> Host gevent/live chat port written to .env.example.
31
+ --repo-url <url> Source repo URL for add-repo.
32
+ --repo <name> Source repo folder name for repo/module actions.
33
+ --module <name> Odoo module technical name for module actions.
34
+ --delete-files Also delete module files in remove-module. Default: false.
35
+ --odoo-version <branch> Override the environment Odoo branch for add-repo/add-module.
36
+ --source-repo-url <url> Source repo URL. Repeat for multiple repos.
37
+ --source-path <path> Advanced: local folder for the preceding source repo.
38
+ --source-addons <list> Advanced: comma-separated addons for the preceding source repo.
39
+ --create-missing-repos Create inaccessible GitHub repos with gh CLI.
40
+ --repo-visibility <value> Visibility for created repos: private or public. Default: private.
41
+ --init-empty-repos Initialize empty source repos with the selected branch.
42
+ --dry-run Print planned files and commands without writing.
43
+ --stage=false Do not run git add .
44
+ --no-update-check Skip the startup npm update check.
45
+ --version, -v Show the package version.
46
+ --help, -h Show this help.
47
+
48
+ Example:
49
+ npx @wpmoo/odoo create \\
50
+ --product odoo_sample_module \\
51
+ --odoo-version 19.0 \\
52
+ --dev-repo-url https://github.com/example-org/odoo_sample_module_dev.git \\
53
+ --source-repo-url https://github.com/example-org/odoo_sample_module.git
54
+
55
+ Compose resource example:
56
+ npx @wpmoo/odoo create \\
57
+ --engine compose \\
58
+ --product odoo_sample_module \\
59
+ --source-repo-url https://github.com/example-org/odoo_sample_module.git \\
60
+ --agent-skills-template
61
+ `;
62
+ }
@@ -0,0 +1,67 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ let lastPromptCancelKey;
3
+ export class MenuBackSignal extends Error {
4
+ constructor() {
5
+ super('Return to previous menu');
6
+ this.name = 'MenuBackSignal';
7
+ }
8
+ }
9
+ export function isMenuBackSignal(error) {
10
+ return error instanceof MenuBackSignal;
11
+ }
12
+ export function menuIntroTitle(title, action) {
13
+ return action === 'back' ? `${title} · Back (Esc)` : title;
14
+ }
15
+ export function menuPromptMessage(message, action) {
16
+ return action === 'back' ? `${message} · Esc to go back` : message;
17
+ }
18
+ export function promptCancelOutcome(cancelled, action, key) {
19
+ if (!cancelled) {
20
+ return 'continue';
21
+ }
22
+ if (action === 'back' && key !== 'interrupt') {
23
+ return 'back';
24
+ }
25
+ return 'exit';
26
+ }
27
+ export function recordPromptCancelKey(key) {
28
+ if (key.ctrl && key.name === 'c') {
29
+ lastPromptCancelKey = 'interrupt';
30
+ return;
31
+ }
32
+ if (key.name === 'escape' || key.sequence === '\u001B') {
33
+ lastPromptCancelKey = 'escape';
34
+ return;
35
+ }
36
+ lastPromptCancelKey = 'other';
37
+ }
38
+ export function consumePromptCancelKey() {
39
+ const key = lastPromptCancelKey;
40
+ lastPromptCancelKey = undefined;
41
+ return key;
42
+ }
43
+ export function installPromptCancelKeyTracker(input = process.stdin) {
44
+ emitKeypressEvents(input);
45
+ const listener = (_value, key) => {
46
+ if (key.ctrl || key.name === 'escape' || key.sequence === '\u001B') {
47
+ recordPromptCancelKey(key);
48
+ }
49
+ };
50
+ input.on('keypress', listener);
51
+ return () => input.off('keypress', listener);
52
+ }
53
+ export function handlePromptCancel(cancelled, action) {
54
+ const outcome = promptCancelOutcome(cancelled, action, consumePromptCancelKey());
55
+ if (outcome === 'continue') {
56
+ return;
57
+ }
58
+ if (outcome === 'back') {
59
+ throw new MenuBackSignal();
60
+ }
61
+ process.exit(1);
62
+ }
63
+ export function handleUnavailableMenuChoice(action) {
64
+ if (action === 'back') {
65
+ throw new MenuBackSignal();
66
+ }
67
+ }