happy-stacks 0.4.0 → 0.5.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.
- package/README.md +64 -33
- package/bin/happys.mjs +44 -1
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +1 -2
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +1 -1
- package/scripts/auth.mjs +21 -3
- package/scripts/build.mjs +1 -1
- package/scripts/dev.mjs +20 -7
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +2 -2
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +5 -2
- package/scripts/install.mjs +99 -57
- package/scripts/migrate.mjs +3 -12
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/review.mjs +715 -24
- package/scripts/review_pr.mjs +5 -20
- package/scripts/run.mjs +21 -15
- package/scripts/setup.mjs +147 -25
- package/scripts/setup_pr.mjs +19 -28
- package/scripts/stack.mjs +493 -157
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tui.mjs +88 -2
- package/scripts/utils/cli/cli_registry.mjs +20 -5
- package/scripts/utils/cli/cwd_scope.mjs +56 -2
- package/scripts/utils/cli/cwd_scope.test.mjs +40 -7
- package/scripts/utils/cli/prereqs.mjs +8 -5
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +14 -1
- package/scripts/utils/dev/expo_dev.mjs +188 -4
- package/scripts/utils/dev/server.mjs +21 -17
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/git/worktrees.mjs +63 -12
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/paths/paths.mjs +118 -3
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/pm.mjs +113 -16
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +68 -10
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +56 -14
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +32 -22
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -0
- package/scripts/utils/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- package/scripts/utils/server/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/stack/editor_workspace.mjs +4 -4
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/startup.mjs +113 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +627 -137
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
function runNode(args, { cwd, env }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const proc = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
15
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
16
|
+
proc.on('error', reject);
|
|
17
|
+
proc.on('exit', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('happys <stack> <cmd> ... rewrites to happys stack <cmd> <stack> ... when stack exists', async () => {
|
|
22
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const rootDir = dirname(scriptsDir);
|
|
24
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-shorthand-'));
|
|
25
|
+
|
|
26
|
+
const storageDir = join(tmp, 'storage');
|
|
27
|
+
const homeDir = join(tmp, 'home');
|
|
28
|
+
const stackName = 'exp-test';
|
|
29
|
+
|
|
30
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
31
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
32
|
+
await mkdir(homeDir, { recursive: true });
|
|
33
|
+
await writeFile(envPath, 'FOO=bar\n', 'utf-8');
|
|
34
|
+
|
|
35
|
+
const baseEnv = {
|
|
36
|
+
...process.env,
|
|
37
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
38
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
39
|
+
HAPPY_STACKS_CLI_ROOT_DISABLE: '1',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const res = await runNode([join(rootDir, 'bin', 'happys.mjs'), stackName, 'env', 'path', '--json'], {
|
|
43
|
+
cwd: rootDir,
|
|
44
|
+
env: baseEnv,
|
|
45
|
+
});
|
|
46
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
47
|
+
|
|
48
|
+
const out = JSON.parse(res.stdout || '{}');
|
|
49
|
+
assert.equal(out.ok, true);
|
|
50
|
+
assert.ok(
|
|
51
|
+
typeof out.envPath === 'string' && out.envPath.endsWith(`/${stackName}/env`),
|
|
52
|
+
`expected envPath to end with /${stackName}/env, got: ${out.envPath}`
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
function runNode(args, { cwd, env }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const cleanEnv = {};
|
|
12
|
+
for (const [k, v] of Object.entries(env ?? {})) {
|
|
13
|
+
if (v == null) continue;
|
|
14
|
+
cleanEnv[k] = String(v);
|
|
15
|
+
}
|
|
16
|
+
const proc = spawn(process.execPath, args, { cwd, env: cleanEnv, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
20
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
21
|
+
proc.on('error', reject);
|
|
22
|
+
proc.on('exit', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function touchWorktree(dir) {
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
// In a git worktree, ".git" is often a file; our detection treats either file or dir as truthy.
|
|
29
|
+
await writeFile(join(dir, '.git'), 'gitdir: /dev/null\n', 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('happys stack wt <stack> -- list defaults to active-only (no exhaustive enumeration)', async () => {
|
|
33
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const rootDir = dirname(scriptsDir);
|
|
35
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-stack-wt-list-'));
|
|
36
|
+
|
|
37
|
+
const storageDir = join(tmp, 'storage');
|
|
38
|
+
const homeDir = join(tmp, 'home');
|
|
39
|
+
const workspaceDir = join(tmp, 'workspace');
|
|
40
|
+
const componentsDir = join(workspaceDir, 'components');
|
|
41
|
+
const stackName = 'exp-test';
|
|
42
|
+
|
|
43
|
+
// Create isolated worktrees on disk (inside our temp workspace).
|
|
44
|
+
const wtRoot = join(componentsDir, '.worktrees');
|
|
45
|
+
const happyActive = join(wtRoot, 'happy', 'slopus', 'pr', 'active-branch');
|
|
46
|
+
const happyOther = join(wtRoot, 'happy', 'slopus', 'pr', 'other-branch');
|
|
47
|
+
const cliActive = join(wtRoot, 'happy-cli', 'slopus', 'pr', 'cli-active');
|
|
48
|
+
const cliOther = join(wtRoot, 'happy-cli', 'slopus', 'pr', 'cli-other');
|
|
49
|
+
await touchWorktree(happyActive);
|
|
50
|
+
await touchWorktree(happyOther);
|
|
51
|
+
await touchWorktree(cliActive);
|
|
52
|
+
await touchWorktree(cliOther);
|
|
53
|
+
|
|
54
|
+
// Stack env selects the active worktrees.
|
|
55
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
56
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
57
|
+
await writeFile(
|
|
58
|
+
envPath,
|
|
59
|
+
[
|
|
60
|
+
`HAPPY_STACKS_STACK=${stackName}`,
|
|
61
|
+
`HAPPY_STACKS_COMPONENT_DIR_HAPPY=${happyActive}`,
|
|
62
|
+
`HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI=${cliActive}`,
|
|
63
|
+
'',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
'utf-8'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const baseEnv = {
|
|
69
|
+
...process.env,
|
|
70
|
+
// Prevent loading the user's real ~/.happy-stacks/.env via canonical discovery.
|
|
71
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
72
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
73
|
+
HAPPY_STACKS_WORKSPACE_DIR: workspaceDir,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const res = await runNode([join(rootDir, 'scripts', 'stack.mjs'), 'wt', stackName, '--', 'list'], { cwd: rootDir, env: baseEnv });
|
|
77
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
78
|
+
|
|
79
|
+
assert.ok(res.stdout.includes(`- active: ${happyActive}`), `expected happy active in output\n${res.stdout}`);
|
|
80
|
+
assert.ok(res.stdout.includes(`- active: ${cliActive}`), `expected happy-cli active in output\n${res.stdout}`);
|
|
81
|
+
|
|
82
|
+
// Should NOT enumerate other worktrees unless --all was passed.
|
|
83
|
+
assert.ok(!res.stdout.includes(`- ${happyOther}`), `expected happy other to be omitted\n${res.stdout}`);
|
|
84
|
+
assert.ok(!res.stdout.includes(`- ${cliOther}`), `expected happy-cli other to be omitted\n${res.stdout}`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('happys stack wt <stack> -- list --all shows all worktrees (opt-in)', async () => {
|
|
88
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
89
|
+
const rootDir = dirname(scriptsDir);
|
|
90
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-stack-wt-list-'));
|
|
91
|
+
|
|
92
|
+
const storageDir = join(tmp, 'storage');
|
|
93
|
+
const homeDir = join(tmp, 'home');
|
|
94
|
+
const workspaceDir = join(tmp, 'workspace');
|
|
95
|
+
const componentsDir = join(workspaceDir, 'components');
|
|
96
|
+
const stackName = 'exp-test';
|
|
97
|
+
|
|
98
|
+
const wtRoot = join(componentsDir, '.worktrees');
|
|
99
|
+
const happyActive = join(wtRoot, 'happy', 'slopus', 'pr', 'active-branch');
|
|
100
|
+
const happyOther = join(wtRoot, 'happy', 'slopus', 'pr', 'other-branch');
|
|
101
|
+
await touchWorktree(happyActive);
|
|
102
|
+
await touchWorktree(happyOther);
|
|
103
|
+
|
|
104
|
+
const envPath = join(storageDir, stackName, 'env');
|
|
105
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
106
|
+
await writeFile(
|
|
107
|
+
envPath,
|
|
108
|
+
[`HAPPY_STACKS_STACK=${stackName}`, `HAPPY_STACKS_COMPONENT_DIR_HAPPY=${happyActive}`, ''].join('\n'),
|
|
109
|
+
'utf-8'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const baseEnv = {
|
|
113
|
+
...process.env,
|
|
114
|
+
HAPPY_STACKS_HOME_DIR: homeDir,
|
|
115
|
+
HAPPY_STACKS_STORAGE_DIR: storageDir,
|
|
116
|
+
HAPPY_STACKS_WORKSPACE_DIR: workspaceDir,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const res = await runNode([join(rootDir, 'scripts', 'stack.mjs'), 'wt', stackName, '--', 'list', '--all'], {
|
|
120
|
+
cwd: rootDir,
|
|
121
|
+
env: baseEnv,
|
|
122
|
+
});
|
|
123
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
124
|
+
|
|
125
|
+
assert.ok(res.stdout.includes(`- active: ${happyActive}`), `expected happy active in output\n${res.stdout}`);
|
|
126
|
+
assert.ok(res.stdout.includes(`- ${happyOther}`), `expected happy other to be listed with --all\n${res.stdout}`);
|
|
127
|
+
});
|
|
128
|
+
|
package/scripts/tui.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import './utils/env/env.mjs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
4
|
import { join, resolve, sep } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { printResult } from './utils/cli/cli.mjs';
|
|
@@ -115,6 +116,76 @@ function inferStackNameFromForwardedArgs(args) {
|
|
|
115
116
|
|
|
116
117
|
const readEnvObject = readEnvObjectFromFile;
|
|
117
118
|
|
|
119
|
+
async function preflightCorepackYarnForStack({ envPath }) {
|
|
120
|
+
// Corepack caches (and therefore "download yarn?" prompts) are tied to XDG/HOME.
|
|
121
|
+
// In stack mode we isolate HOME/XDG caches per stack, which can cause Corepack to prompt
|
|
122
|
+
// the first time a stack runs Yarn.
|
|
123
|
+
//
|
|
124
|
+
// In `happys tui`, the child runs under a pseudo-TTY (via `script`) and the TUI consumes
|
|
125
|
+
// all keyboard input, so Corepack's interactive prompt deadlocks.
|
|
126
|
+
//
|
|
127
|
+
// Fix: pre-download Yarn in a *non-tty* subprocess using the stack's isolated HOME/XDG,
|
|
128
|
+
// so later pty runs don't prompt.
|
|
129
|
+
if (!envPath) return;
|
|
130
|
+
const baseDir = resolve(join(envPath, '..'));
|
|
131
|
+
const stackHome = join(baseDir, 'home');
|
|
132
|
+
const cacheBase = join(baseDir, 'cache');
|
|
133
|
+
const env = {
|
|
134
|
+
...process.env,
|
|
135
|
+
HOME: stackHome,
|
|
136
|
+
USERPROFILE: stackHome,
|
|
137
|
+
XDG_CACHE_HOME: join(cacheBase, 'xdg'),
|
|
138
|
+
YARN_CACHE_FOLDER: join(cacheBase, 'yarn'),
|
|
139
|
+
npm_config_cache: join(cacheBase, 'npm'),
|
|
140
|
+
// Avoid Corepack mutating package.json automatically.
|
|
141
|
+
COREPACK_ENABLE_AUTO_PIN: '0',
|
|
142
|
+
// Best-effort: disable download prompts (may not be honored by all Corepack versions).
|
|
143
|
+
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0',
|
|
144
|
+
// Treat this as non-interactive (helps some tooling).
|
|
145
|
+
CI: process.env.CI ?? '1',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await mkdir(stackHome, { recursive: true }).catch(() => {});
|
|
149
|
+
await mkdir(env.XDG_CACHE_HOME, { recursive: true }).catch(() => {});
|
|
150
|
+
await mkdir(env.YARN_CACHE_FOLDER, { recursive: true }).catch(() => {});
|
|
151
|
+
await mkdir(env.npm_config_cache, { recursive: true }).catch(() => {});
|
|
152
|
+
await mkdir(env.COREPACK_HOME, { recursive: true }).catch(() => {});
|
|
153
|
+
|
|
154
|
+
await new Promise((resolvePromise) => {
|
|
155
|
+
const proc = spawn('yarn', ['--version'], {
|
|
156
|
+
env,
|
|
157
|
+
cwd: baseDir,
|
|
158
|
+
// Non-tty stdio: Corepack typically won't prompt; if it does, we still provide "y\n".
|
|
159
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
160
|
+
shell: false,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
proc.stdin?.write('y\n');
|
|
164
|
+
proc.stdin?.end();
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const t = setTimeout(() => {
|
|
170
|
+
try {
|
|
171
|
+
proc.kill('SIGKILL');
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
resolvePromise();
|
|
176
|
+
}, 60_000);
|
|
177
|
+
|
|
178
|
+
proc.on('exit', () => {
|
|
179
|
+
clearTimeout(t);
|
|
180
|
+
resolvePromise();
|
|
181
|
+
});
|
|
182
|
+
proc.on('error', () => {
|
|
183
|
+
clearTimeout(t);
|
|
184
|
+
resolvePromise();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
118
189
|
function getEnvVal(env, key, legacyKey) {
|
|
119
190
|
return getEnvValueAny(env, [key, legacyKey]) || '';
|
|
120
191
|
}
|
|
@@ -294,6 +365,7 @@ async function main() {
|
|
|
294
365
|
const forwarded = argv;
|
|
295
366
|
|
|
296
367
|
const stackName = inferStackNameFromForwardedArgs(forwarded);
|
|
368
|
+
const { envPath: stackEnvPath } = resolveStackEnvPath(stackName);
|
|
297
369
|
|
|
298
370
|
const panes = [
|
|
299
371
|
mkPane('orch', 'orchestration', { visible: true, kind: 'log' }),
|
|
@@ -334,24 +406,38 @@ async function main() {
|
|
|
334
406
|
pushLine(panes[paneIndexById.get('orch')], `[${nowTs()}] ${msg}`);
|
|
335
407
|
};
|
|
336
408
|
|
|
409
|
+
// Preflight Yarn/Corepack for this stack before spawning the pty child.
|
|
410
|
+
// This prevents Corepack "download yarn? [Y/n]" prompts from deadlocking the TUI.
|
|
411
|
+
await preflightCorepackYarnForStack({ envPath: stackEnvPath });
|
|
412
|
+
|
|
337
413
|
let layout = 'columns'; // single | split | columns
|
|
338
414
|
let focused = paneIndexById.get('local'); // default focus
|
|
339
415
|
let paused = false;
|
|
340
416
|
let renderScheduled = false;
|
|
341
417
|
|
|
342
418
|
const wantsPty = process.platform !== 'win32' && (await commandExists('script', { cwd: rootDir }));
|
|
419
|
+
// In TUI mode, we intentionally do not forward keyboard input to the child process (stdin is ignored),
|
|
420
|
+
// so any interactive prompts inside the child would deadlock.
|
|
421
|
+
// Mark the child env so dependency installers can auto-approve safe prompts (Corepack yarn downloads).
|
|
422
|
+
const childEnv = {
|
|
423
|
+
...process.env,
|
|
424
|
+
HAPPY_STACKS_TUI: '1',
|
|
425
|
+
HAPPY_LOCAL_TUI: '1',
|
|
426
|
+
// Avoid Corepack mutating package.json automatically.
|
|
427
|
+
COREPACK_ENABLE_AUTO_PIN: '0',
|
|
428
|
+
};
|
|
343
429
|
const child = wantsPty
|
|
344
430
|
? // Use a pseudo-terminal so tools like Expo print QR/status output that they hide in non-TTY mode.
|
|
345
431
|
// `script` is available by default on macOS (and common on Linux).
|
|
346
432
|
spawn('script', ['-q', '/dev/null', process.execPath, happysBin, ...forwarded], {
|
|
347
433
|
cwd: rootDir,
|
|
348
|
-
env:
|
|
434
|
+
env: childEnv,
|
|
349
435
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
350
436
|
detached: process.platform !== 'win32',
|
|
351
437
|
})
|
|
352
438
|
: spawn(process.execPath, [happysBin, ...forwarded], {
|
|
353
439
|
cwd: rootDir,
|
|
354
|
-
env:
|
|
440
|
+
env: childEnv,
|
|
355
441
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
356
442
|
detached: process.platform !== 'win32',
|
|
357
443
|
});
|
|
@@ -34,7 +34,7 @@ export function getHappysRegistry() {
|
|
|
34
34
|
aliases: ['setupPR', 'setuppr'],
|
|
35
35
|
kind: 'node',
|
|
36
36
|
scriptRelPath: 'scripts/setup_pr.mjs',
|
|
37
|
-
rootUsage: 'happys setup-pr --happy=<pr-url|number> [--happy-
|
|
37
|
+
rootUsage: 'happys setup-pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>] [--dev|--start] [--json] [-- ...]',
|
|
38
38
|
description: 'One-shot: set up + run a PR stack (maintainer-friendly)',
|
|
39
39
|
},
|
|
40
40
|
{
|
|
@@ -42,7 +42,7 @@ export function getHappysRegistry() {
|
|
|
42
42
|
aliases: ['reviewPR', 'reviewpr'],
|
|
43
43
|
kind: 'node',
|
|
44
44
|
scriptRelPath: 'scripts/review_pr.mjs',
|
|
45
|
-
rootUsage: 'happys review-pr --happy=<pr-url|number> [--happy-
|
|
45
|
+
rootUsage: 'happys review-pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>] [--dev|--start] [--json] [-- ...]',
|
|
46
46
|
description: 'Run setup-pr in a temporary sandbox (auto-cleaned)',
|
|
47
47
|
},
|
|
48
48
|
{
|
|
@@ -54,12 +54,18 @@ export function getHappysRegistry() {
|
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
name: 'where',
|
|
57
|
-
aliases: ['env'],
|
|
58
57
|
kind: 'node',
|
|
59
58
|
scriptRelPath: 'scripts/where.mjs',
|
|
60
|
-
rootUsage: 'happys where [--json]
|
|
59
|
+
rootUsage: 'happys where [--json]',
|
|
61
60
|
description: 'Show resolved paths and env sources',
|
|
62
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: 'env',
|
|
64
|
+
kind: 'node',
|
|
65
|
+
scriptRelPath: 'scripts/env.mjs',
|
|
66
|
+
rootUsage: 'happys env set KEY=VALUE [KEY2=VALUE2...] (defaults to main stack)',
|
|
67
|
+
description: 'Set per-stack env vars (defaults to main)',
|
|
68
|
+
},
|
|
63
69
|
{
|
|
64
70
|
name: 'bootstrap',
|
|
65
71
|
kind: 'node',
|
|
@@ -140,6 +146,13 @@ export function getHappysRegistry() {
|
|
|
140
146
|
rootUsage: 'happys migrate light-to-server --from-stack=<name> --to-stack=<name> [--include-files] [--force] [--json]',
|
|
141
147
|
description: 'Migrate data between server flavors (experimental)',
|
|
142
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'monorepo',
|
|
151
|
+
kind: 'node',
|
|
152
|
+
scriptRelPath: 'scripts/monorepo.mjs',
|
|
153
|
+
rootUsage: 'happys monorepo port --target=/abs/path/to/monorepo [--branch=port/<name>] [--dry-run] [--3way] [--json]',
|
|
154
|
+
description: 'Port split-repo commits into monorepo (experimental)',
|
|
155
|
+
},
|
|
143
156
|
{
|
|
144
157
|
name: 'mobile',
|
|
145
158
|
kind: 'node',
|
|
@@ -347,6 +360,9 @@ export function renderHappysRootHelp() {
|
|
|
347
360
|
'usage:',
|
|
348
361
|
...usageLines.map((l) => ` ${l}`),
|
|
349
362
|
'',
|
|
363
|
+
'stack shorthand:',
|
|
364
|
+
' happys <stack> <command> ... (equivalent to: happys stack <command> <stack> ...)',
|
|
365
|
+
'',
|
|
350
366
|
'commands:',
|
|
351
367
|
...commandsLines,
|
|
352
368
|
'',
|
|
@@ -354,4 +370,3 @@ export function renderHappysRootHelp() {
|
|
|
354
370
|
' happys help [command]',
|
|
355
371
|
].join('\n');
|
|
356
372
|
}
|
|
357
|
-
|
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { getWorktreesRoot } from '../git/worktrees.mjs';
|
|
5
|
-
import { getComponentsDir } from '../paths/paths.mjs';
|
|
5
|
+
import { getComponentsDir, isHappyMonorepoRoot } from '../paths/paths.mjs';
|
|
6
6
|
|
|
7
7
|
export function getInvokedCwd(env = process.env) {
|
|
8
8
|
return String(env.HAPPY_STACKS_INVOKED_CWD ?? env.HAPPY_LOCAL_INVOKED_CWD ?? env.PWD ?? '').trim();
|
|
@@ -45,6 +45,24 @@ function findGitRoot(startDir, stopAtDir) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function resolveHappyMonorepoComponentFromPath({ monorepoRoot, absPath }) {
|
|
49
|
+
const root = resolve(monorepoRoot);
|
|
50
|
+
const abs = resolve(absPath);
|
|
51
|
+
const map = [
|
|
52
|
+
{ component: 'happy', dir: join(root, 'expo-app') },
|
|
53
|
+
{ component: 'happy-cli', dir: join(root, 'cli') },
|
|
54
|
+
{ component: 'happy-server', dir: join(root, 'server') },
|
|
55
|
+
];
|
|
56
|
+
for (const m of map) {
|
|
57
|
+
if (isPathInside(abs, m.dir)) {
|
|
58
|
+
// We return the shared git root so callers can safely use it as an env override
|
|
59
|
+
// for any of the monorepo components.
|
|
60
|
+
return { component: m.component, repoDir: root };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
export function inferComponentFromCwd({ rootDir, invokedCwd, components }) {
|
|
49
67
|
const cwd = String(invokedCwd ?? '').trim();
|
|
50
68
|
const list = Array.isArray(components) ? components : [];
|
|
@@ -56,6 +74,43 @@ export function inferComponentFromCwd({ rootDir, invokedCwd, components }) {
|
|
|
56
74
|
const componentsDir = getComponentsDir(rootDir);
|
|
57
75
|
const worktreesRoot = getWorktreesRoot(rootDir);
|
|
58
76
|
|
|
77
|
+
// Monorepo-aware inference:
|
|
78
|
+
// If we're inside a happy monorepo checkout/worktree, infer which "logical component"
|
|
79
|
+
// (expo-app/cli/server) the user is working in and return that package dir.
|
|
80
|
+
//
|
|
81
|
+
// This enables workflows like:
|
|
82
|
+
// - running `happys dev` from inside components/happy/cli (should infer happy-cli)
|
|
83
|
+
// - running from inside components/.worktrees/happy/<owner>/<branch>/server (should infer happy-server)
|
|
84
|
+
{
|
|
85
|
+
const monorepoScopes = [
|
|
86
|
+
resolve(join(componentsDir, 'happy')),
|
|
87
|
+
resolve(join(worktreesRoot, 'happy')),
|
|
88
|
+
];
|
|
89
|
+
for (const scope of monorepoScopes) {
|
|
90
|
+
if (!isPathInside(abs, scope)) continue;
|
|
91
|
+
const repoRoot = findGitRoot(abs, scope);
|
|
92
|
+
if (!repoRoot) continue;
|
|
93
|
+
if (!isHappyMonorepoRoot(repoRoot)) continue;
|
|
94
|
+
|
|
95
|
+
const inferred = resolveHappyMonorepoComponentFromPath({ monorepoRoot: repoRoot, absPath: abs });
|
|
96
|
+
if (inferred) {
|
|
97
|
+
// Only return components the caller asked us to consider.
|
|
98
|
+
if (list.includes(inferred.component)) {
|
|
99
|
+
return inferred;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If we are inside the monorepo root but not inside a known package dir, default to `happy`
|
|
105
|
+
// (the UI) when the caller allows it. This keeps legacy behavior where running from the
|
|
106
|
+
// repo root still "belongs" to the UI component.
|
|
107
|
+
if (list.includes('happy')) {
|
|
108
|
+
return { component: 'happy', repoDir: repoRoot };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
59
114
|
for (const component of list) {
|
|
60
115
|
const c = String(component ?? '').trim();
|
|
61
116
|
if (!c) continue;
|
|
@@ -79,4 +134,3 @@ export function inferComponentFromCwd({ rootDir, invokedCwd, components }) {
|
|
|
79
134
|
|
|
80
135
|
return null;
|
|
81
136
|
}
|
|
82
|
-
|
|
@@ -35,7 +35,37 @@ test('inferComponentFromCwd resolves components/<component> repo root', async (t
|
|
|
35
35
|
assert.deepEqual(inferred, { component: 'happy', repoDir: repoRoot });
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
test('inferComponentFromCwd resolves
|
|
38
|
+
test('inferComponentFromCwd resolves happy monorepo subpackages under components/happy', async (t) => {
|
|
39
|
+
const rootDir = await withTempRoot(t);
|
|
40
|
+
const prevWorkspace = process.env.HAPPY_STACKS_WORKSPACE_DIR;
|
|
41
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = rootDir;
|
|
42
|
+
t.after(() => {
|
|
43
|
+
if (prevWorkspace == null) {
|
|
44
|
+
delete process.env.HAPPY_STACKS_WORKSPACE_DIR;
|
|
45
|
+
} else {
|
|
46
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = prevWorkspace;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const monoRoot = join(rootDir, 'components', 'happy');
|
|
51
|
+
await mkdir(join(monoRoot, 'expo-app'), { recursive: true });
|
|
52
|
+
await mkdir(join(monoRoot, 'cli', 'src'), { recursive: true });
|
|
53
|
+
await mkdir(join(monoRoot, 'server'), { recursive: true });
|
|
54
|
+
await writeFile(join(monoRoot, 'expo-app', 'package.json'), '{}\n', 'utf-8');
|
|
55
|
+
await writeFile(join(monoRoot, 'cli', 'package.json'), '{}\n', 'utf-8');
|
|
56
|
+
await writeFile(join(monoRoot, 'server', 'package.json'), '{}\n', 'utf-8');
|
|
57
|
+
await writeFile(join(monoRoot, '.git'), 'gitdir: /tmp/fake\n', 'utf-8');
|
|
58
|
+
|
|
59
|
+
const invokedCwd = join(monoRoot, 'cli', 'src');
|
|
60
|
+
const inferred = inferComponentFromCwd({
|
|
61
|
+
rootDir,
|
|
62
|
+
invokedCwd,
|
|
63
|
+
components: ['happy', 'happy-cli', 'happy-server'],
|
|
64
|
+
});
|
|
65
|
+
assert.deepEqual(inferred, { component: 'happy-cli', repoDir: monoRoot });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('inferComponentFromCwd resolves happy monorepo worktree roots under components/.worktrees/happy', async (t) => {
|
|
39
69
|
const rootDir = await withTempRoot(t);
|
|
40
70
|
const prevWorkspace = process.env.HAPPY_STACKS_WORKSPACE_DIR;
|
|
41
71
|
process.env.HAPPY_STACKS_WORKSPACE_DIR = rootDir;
|
|
@@ -47,14 +77,18 @@ test('inferComponentFromCwd resolves components/.worktrees/<component>/<owner>/<
|
|
|
47
77
|
}
|
|
48
78
|
});
|
|
49
79
|
|
|
50
|
-
const wtRoot = join(rootDir, 'components', '.worktrees', 'happy', 'slopus', 'pr', '123-fix', 'nested');
|
|
51
|
-
await mkdir(wtRoot, { recursive: true });
|
|
52
80
|
const repoRoot = join(rootDir, 'components', '.worktrees', 'happy', 'slopus', 'pr', '123-fix');
|
|
81
|
+
await mkdir(join(repoRoot, 'expo-app'), { recursive: true });
|
|
82
|
+
await mkdir(join(repoRoot, 'cli', 'nested'), { recursive: true });
|
|
83
|
+
await mkdir(join(repoRoot, 'server'), { recursive: true });
|
|
84
|
+
await writeFile(join(repoRoot, 'expo-app', 'package.json'), '{}\n', 'utf-8');
|
|
85
|
+
await writeFile(join(repoRoot, 'cli', 'package.json'), '{}\n', 'utf-8');
|
|
86
|
+
await writeFile(join(repoRoot, 'server', 'package.json'), '{}\n', 'utf-8');
|
|
53
87
|
await writeFile(join(repoRoot, '.git'), 'gitdir: /tmp/fake\n', 'utf-8');
|
|
54
88
|
|
|
55
|
-
const invokedCwd = join(repoRoot, 'nested');
|
|
56
|
-
const inferred = inferComponentFromCwd({ rootDir, invokedCwd, components: ['happy'] });
|
|
57
|
-
assert.deepEqual(inferred, { component: 'happy', repoDir: repoRoot });
|
|
89
|
+
const invokedCwd = join(repoRoot, 'cli', 'nested');
|
|
90
|
+
const inferred = inferComponentFromCwd({ rootDir, invokedCwd, components: ['happy', 'happy-cli', 'happy-server'] });
|
|
91
|
+
assert.deepEqual(inferred, { component: 'happy-cli', repoDir: repoRoot });
|
|
58
92
|
});
|
|
59
93
|
|
|
60
94
|
test('inferComponentFromCwd returns null outside known component roots', async (t) => {
|
|
@@ -74,4 +108,3 @@ test('inferComponentFromCwd returns null outside known component roots', async (
|
|
|
74
108
|
const inferred = inferComponentFromCwd({ rootDir, invokedCwd, components: ['happy'] });
|
|
75
109
|
assert.equal(inferred, null);
|
|
76
110
|
});
|
|
77
|
-
|
|
@@ -24,11 +24,15 @@ export async function assertCliPrereqs({ git = false, pnpm = false, codex = fals
|
|
|
24
24
|
|
|
25
25
|
if (pnpm) {
|
|
26
26
|
const hasPnpm = await commandExists('pnpm');
|
|
27
|
-
|
|
27
|
+
const hasYarn = await commandExists('yarn');
|
|
28
|
+
if (!hasPnpm && !hasYarn) {
|
|
28
29
|
missing.push({
|
|
29
|
-
name: 'pnpm',
|
|
30
|
-
why: 'required to install dependencies for Happy Stacks components',
|
|
31
|
-
install: [
|
|
30
|
+
name: 'yarn/pnpm',
|
|
31
|
+
why: 'required to install dependencies for Happy Stacks components (varies per component)',
|
|
32
|
+
install: [
|
|
33
|
+
'Enable Corepack (recommended): `corepack enable`',
|
|
34
|
+
'Or install pnpm: `corepack prepare pnpm@latest --activate`',
|
|
35
|
+
],
|
|
32
36
|
});
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -69,4 +73,3 @@ export async function assertCliPrereqs({ git = false, pnpm = false, codex = fals
|
|
|
69
73
|
`[prereqs] After installing, re-run the command.`
|
|
70
74
|
);
|
|
71
75
|
}
|
|
72
|
-
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { assertCliPrereqs } from './prereqs.mjs';
|
|
8
|
+
|
|
9
|
+
test('assertCliPrereqs({pnpm:true}) accepts yarn when pnpm is missing', async () => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'hs-prereqs-yarn-'));
|
|
11
|
+
const oldPath = process.env.PATH;
|
|
12
|
+
try {
|
|
13
|
+
const yarnBin = join(root, 'yarn');
|
|
14
|
+
await writeFile(yarnBin, '#!/bin/sh\nexit 0\n', 'utf-8');
|
|
15
|
+
await chmod(yarnBin, 0o755);
|
|
16
|
+
process.env.PATH = `/bin:${root}`;
|
|
17
|
+
|
|
18
|
+
await assertCliPrereqs({ pnpm: true });
|
|
19
|
+
} finally {
|
|
20
|
+
process.env.PATH = oldPath;
|
|
21
|
+
await rm(root, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('assertCliPrereqs({pnpm:true}) throws when neither pnpm nor yarn is available', async () => {
|
|
26
|
+
const oldPath = process.env.PATH;
|
|
27
|
+
try {
|
|
28
|
+
process.env.PATH = '/bin';
|
|
29
|
+
await assert.rejects(() => assertCliPrereqs({ pnpm: true }), /pnpm|yarn/i);
|
|
30
|
+
} finally {
|
|
31
|
+
process.env.PATH = oldPath;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
@@ -2,6 +2,9 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import { listWorktreeSpecs } from '../git/worktrees.mjs';
|
|
3
3
|
|
|
4
4
|
export function isTty() {
|
|
5
|
+
if (process.env.HAPPY_STACKS_TEST_TTY === '1') {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
5
8
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6
9
|
}
|
|
7
10
|
|
|
@@ -24,6 +27,8 @@ export async function promptSelect(rl, { title, options, defaultIndex = 0 }) {
|
|
|
24
27
|
throw new Error('[wizard] no options to select from');
|
|
25
28
|
}
|
|
26
29
|
// eslint-disable-next-line no-console
|
|
30
|
+
console.log('');
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
27
32
|
console.log(title);
|
|
28
33
|
for (let i = 0; i < options.length; i++) {
|
|
29
34
|
// eslint-disable-next-line no-console
|
|
@@ -35,22 +40,26 @@ export async function promptSelect(rl, { title, options, defaultIndex = 0 }) {
|
|
|
35
40
|
return options[idx].value;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
export async function promptWorktreeSource({ rl, rootDir, component, stackName, createRemote = 'upstream' }) {
|
|
39
|
-
const
|
|
43
|
+
export async function promptWorktreeSource({ rl, rootDir, component, stackName, createRemote = 'upstream', deps = {} }) {
|
|
44
|
+
const promptFn = deps.prompt ?? prompt;
|
|
45
|
+
const promptSelectFn = deps.promptSelect ?? promptSelect;
|
|
46
|
+
const listWorktreeSpecsFn = deps.listWorktreeSpecs ?? listWorktreeSpecs;
|
|
40
47
|
|
|
41
48
|
const baseOptions = [{ label: `default (components/${component})`, value: 'default' }];
|
|
42
|
-
|
|
43
|
-
baseOptions.push({ label: 'pick existing worktree', value: 'pick' });
|
|
44
|
-
}
|
|
49
|
+
baseOptions.push({ label: 'pick existing worktree', value: 'pick' });
|
|
45
50
|
baseOptions.push({ label: `create new worktree (${createRemote})`, value: 'create' });
|
|
46
51
|
|
|
47
|
-
const kind = await
|
|
52
|
+
const kind = await promptSelectFn(rl, { title: `Select ${component}:`, options: baseOptions, defaultIndex: 0 });
|
|
48
53
|
|
|
49
54
|
if (kind === 'default') {
|
|
50
55
|
return 'default';
|
|
51
56
|
}
|
|
52
57
|
if (kind === 'pick') {
|
|
53
|
-
const
|
|
58
|
+
const specs = await listWorktreeSpecsFn({ rootDir, component });
|
|
59
|
+
if (!specs.length) {
|
|
60
|
+
return 'default';
|
|
61
|
+
}
|
|
62
|
+
const picked = await promptSelectFn(rl, {
|
|
54
63
|
title: `Available ${component} worktrees:`,
|
|
55
64
|
options: specs.map((s) => ({ label: s, value: s })),
|
|
56
65
|
defaultIndex: 0,
|
|
@@ -58,7 +67,7 @@ export async function promptWorktreeSource({ rl, rootDir, component, stackName,
|
|
|
58
67
|
return picked;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
const slug = await
|
|
70
|
+
const slug = await promptFn(rl, `New worktree slug for ${component} (example: pr/${stackName}/${component}): `, {
|
|
62
71
|
defaultValue: '',
|
|
63
72
|
});
|
|
64
73
|
if (!slug) {
|
|
@@ -66,4 +75,3 @@ export async function promptWorktreeSource({ rl, rootDir, component, stackName,
|
|
|
66
75
|
}
|
|
67
76
|
return { create: true, slug, remote: createRemote };
|
|
68
77
|
}
|
|
69
|
-
|