happy-stacks 0.0.0 → 0.1.2
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 +22 -4
- package/bin/happys.mjs +76 -5
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +16 -4
- package/extras/swiftbar/auth-login.sh +5 -5
- package/extras/swiftbar/happy-stacks.5s.sh +83 -41
- package/extras/swiftbar/happys-term.sh +151 -0
- package/extras/swiftbar/happys.sh +52 -0
- package/extras/swiftbar/lib/render.sh +74 -56
- package/extras/swiftbar/lib/system.sh +37 -6
- package/extras/swiftbar/lib/utils.sh +180 -4
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +2 -13
- package/extras/swiftbar/set-server-flavor.sh +8 -8
- package/extras/swiftbar/wt-pr.sh +1 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +374 -3
- package/scripts/daemon.mjs +78 -11
- package/scripts/dev.mjs +122 -17
- package/scripts/init.mjs +238 -32
- package/scripts/migrate.mjs +292 -0
- package/scripts/mobile.mjs +51 -19
- package/scripts/run.mjs +118 -26
- package/scripts/service.mjs +176 -37
- package/scripts/stack.mjs +665 -22
- package/scripts/stop.mjs +157 -0
- package/scripts/tailscale.mjs +147 -21
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/utils/cli_registry.mjs +23 -0
- package/scripts/utils/config.mjs +9 -1
- package/scripts/utils/env.mjs +37 -15
- package/scripts/utils/expo.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +430 -0
- package/scripts/utils/pm.mjs +11 -2
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +46 -5
- package/scripts/utils/server.mjs +37 -0
- package/scripts/utils/stack_stop.mjs +206 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/worktrees.mjs +53 -7
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getComponentDir } from './paths.mjs';
|
|
6
|
+
import { killPortListeners } from './ports.mjs';
|
|
7
|
+
import { isPidAlive, killPid, readPidState } from './expo.mjs';
|
|
8
|
+
import { stopLocalDaemon } from '../daemon.mjs';
|
|
9
|
+
import { stopHappyServerManagedInfra } from './happy_server_infra.mjs';
|
|
10
|
+
|
|
11
|
+
function parseIntOrNull(raw) {
|
|
12
|
+
const s = String(raw ?? '').trim();
|
|
13
|
+
if (!s) return null;
|
|
14
|
+
const n = Number(s);
|
|
15
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveServerComponentFromStackEnv(env) {
|
|
19
|
+
const v =
|
|
20
|
+
(env.HAPPY_STACKS_SERVER_COMPONENT ?? env.HAPPY_LOCAL_SERVER_COMPONENT ?? '').toString().trim() || 'happy-server-light';
|
|
21
|
+
return v === 'happy-server' ? 'happy-server' : 'happy-server-light';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function daemonControlPost({ httpPort, path, body = {} }) {
|
|
25
|
+
const ctl = new AbortController();
|
|
26
|
+
const t = setTimeout(() => ctl.abort(), 1500);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`http://127.0.0.1:${httpPort}${path}`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
signal: ctl.signal,
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`daemon control ${path} failed (http ${res.status}): ${text.trim()}`);
|
|
37
|
+
}
|
|
38
|
+
return text.trim() ? JSON.parse(text) : null;
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(t);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function stopDaemonTrackedSessions({ cliHomeDir, json }) {
|
|
45
|
+
// Read daemon state file written by happy-cli; needed to call control server (/list, /stop-session).
|
|
46
|
+
const statePath = join(cliHomeDir, 'daemon.state.json');
|
|
47
|
+
if (!existsSync(statePath)) {
|
|
48
|
+
return { ok: true, skipped: true, reason: 'missing_state', stoppedSessionIds: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let state = null;
|
|
52
|
+
try {
|
|
53
|
+
state = JSON.parse(await readFile(statePath, 'utf-8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return { ok: false, skipped: true, reason: 'bad_state', stoppedSessionIds: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const httpPort = Number(state?.httpPort);
|
|
59
|
+
const pid = Number(state?.pid);
|
|
60
|
+
if (!Number.isFinite(httpPort) || httpPort <= 0) {
|
|
61
|
+
return { ok: false, skipped: true, reason: 'missing_http_port', stoppedSessionIds: [] };
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(pid) || pid <= 1) {
|
|
64
|
+
return { ok: false, skipped: true, reason: 'missing_pid', stoppedSessionIds: [] };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: true, skipped: true, reason: 'daemon_not_running', stoppedSessionIds: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const listed = await daemonControlPost({ httpPort, path: '/list' }).catch((e) => {
|
|
73
|
+
if (!json) console.warn(`[stack] failed to list daemon sessions: ${e instanceof Error ? e.message : String(e)}`);
|
|
74
|
+
return null;
|
|
75
|
+
});
|
|
76
|
+
const children = Array.isArray(listed?.children) ? listed.children : [];
|
|
77
|
+
|
|
78
|
+
const stoppedSessionIds = [];
|
|
79
|
+
for (const child of children) {
|
|
80
|
+
const sid = String(child?.happySessionId ?? '').trim();
|
|
81
|
+
if (!sid) continue;
|
|
82
|
+
// eslint-disable-next-line no-await-in-loop
|
|
83
|
+
const res = await daemonControlPost({ httpPort, path: '/stop-session', body: { sessionId: sid } }).catch(() => null);
|
|
84
|
+
if (res?.success) {
|
|
85
|
+
stoppedSessionIds.push(sid);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ok: true, skipped: false, stoppedSessionIds };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function stopExpoStateDir({ stackName, baseDir, kind, stateFileName, json }) {
|
|
93
|
+
const root = join(baseDir, kind);
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
entries = [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const killed = [];
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (!e.isDirectory()) continue;
|
|
104
|
+
const statePath = join(root, e.name, stateFileName);
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
const state = await readPidState(statePath);
|
|
107
|
+
if (!state) continue;
|
|
108
|
+
const pid = Number(state.pid);
|
|
109
|
+
const port = parseIntOrNull(state.port);
|
|
110
|
+
|
|
111
|
+
if (!Number.isFinite(pid) || pid <= 1) continue;
|
|
112
|
+
if (!isPidAlive(pid)) continue;
|
|
113
|
+
|
|
114
|
+
if (!json) {
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log(`[stack] stopping ${kind} (pid=${pid}${port ? ` port=${port}` : ''}) for ${stackName}`);
|
|
117
|
+
}
|
|
118
|
+
if (port) {
|
|
119
|
+
// eslint-disable-next-line no-await-in-loop
|
|
120
|
+
await killPortListeners(port, { label: `${stackName} ${kind}` });
|
|
121
|
+
}
|
|
122
|
+
// eslint-disable-next-line no-await-in-loop
|
|
123
|
+
await killPid(pid);
|
|
124
|
+
killed.push({ pid, port, statePath });
|
|
125
|
+
}
|
|
126
|
+
return killed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function stopStackWithEnv({ rootDir, stackName, baseDir, env, json, noDocker = false, aggressive = false }) {
|
|
130
|
+
const actions = {
|
|
131
|
+
stackName,
|
|
132
|
+
baseDir,
|
|
133
|
+
aggressive,
|
|
134
|
+
daemonSessionsStopped: null,
|
|
135
|
+
daemonStopped: false,
|
|
136
|
+
killedPorts: [],
|
|
137
|
+
uiDev: [],
|
|
138
|
+
mobile: [],
|
|
139
|
+
infra: null,
|
|
140
|
+
errors: [],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const serverComponent = resolveServerComponentFromStackEnv(env);
|
|
144
|
+
const port = parseIntOrNull(env.HAPPY_STACKS_SERVER_PORT ?? env.HAPPY_LOCAL_SERVER_PORT);
|
|
145
|
+
const backendPort = parseIntOrNull(env.HAPPY_STACKS_HAPPY_SERVER_BACKEND_PORT ?? env.HAPPY_LOCAL_HAPPY_SERVER_BACKEND_PORT);
|
|
146
|
+
const cliHomeDir = (env.HAPPY_STACKS_CLI_HOME_DIR ?? env.HAPPY_LOCAL_CLI_HOME_DIR ?? join(baseDir, 'cli')).toString();
|
|
147
|
+
const cliBin = join(getComponentDir(rootDir, 'happy-cli'), 'bin', 'happy.mjs');
|
|
148
|
+
|
|
149
|
+
if (aggressive) {
|
|
150
|
+
try {
|
|
151
|
+
actions.daemonSessionsStopped = await stopDaemonTrackedSessions({ cliHomeDir, json });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
actions.errors.push({ step: 'daemon-sessions', error: e instanceof Error ? e.message : String(e) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const internalServerUrl = port ? `http://127.0.0.1:${port}` : 'http://127.0.0.1:3005';
|
|
159
|
+
await stopLocalDaemon({ cliBin, internalServerUrl, cliHomeDir });
|
|
160
|
+
actions.daemonStopped = true;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
actions.errors.push({ step: 'daemon', error: e instanceof Error ? e.message : String(e) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
actions.uiDev = await stopExpoStateDir({ stackName, baseDir, kind: 'ui-dev', stateFileName: 'ui.state.json', json });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
actions.errors.push({ step: 'expo-ui', error: e instanceof Error ? e.message : String(e) });
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
actions.mobile = await stopExpoStateDir({ stackName, baseDir, kind: 'mobile', stateFileName: 'expo.state.json', json });
|
|
172
|
+
} catch (e) {
|
|
173
|
+
actions.errors.push({ step: 'expo-mobile', error: e instanceof Error ? e.message : String(e) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (backendPort) {
|
|
177
|
+
try {
|
|
178
|
+
const pids = await killPortListeners(backendPort, { label: `${stackName} happy-server-backend` });
|
|
179
|
+
actions.killedPorts.push({ port: backendPort, pids, label: 'happy-server-backend' });
|
|
180
|
+
} catch (e) {
|
|
181
|
+
actions.errors.push({ step: 'backend-port', error: e instanceof Error ? e.message : String(e) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (port) {
|
|
185
|
+
try {
|
|
186
|
+
const pids = await killPortListeners(port, { label: `${stackName} server` });
|
|
187
|
+
actions.killedPorts.push({ port, pids, label: 'server' });
|
|
188
|
+
} catch (e) {
|
|
189
|
+
actions.errors.push({ step: 'server-port', error: e instanceof Error ? e.message : String(e) });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const managed = (env.HAPPY_STACKS_MANAGED_INFRA ?? env.HAPPY_LOCAL_MANAGED_INFRA ?? '1').toString().trim() !== '0';
|
|
194
|
+
if (!noDocker && serverComponent === 'happy-server' && managed) {
|
|
195
|
+
try {
|
|
196
|
+
actions.infra = await stopHappyServerManagedInfra({ stackName, baseDir, removeVolumes: false });
|
|
197
|
+
} catch (e) {
|
|
198
|
+
actions.errors.push({ step: 'infra', error: e instanceof Error ? e.message : String(e) });
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
actions.infra = { ok: true, skipped: true, reason: noDocker ? 'no_docker' : 'not_managed_or_not_happy_server' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return actions;
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
2
3
|
import { getComponentsDir } from './paths.mjs';
|
|
3
4
|
|
|
4
5
|
function isInside(path, dir) {
|
|
@@ -45,3 +46,43 @@ export function assertServerComponentDirMatches({ rootDir, serverComponentName,
|
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function detectPrismaProvider(schemaText) {
|
|
50
|
+
// Best-effort parse of:
|
|
51
|
+
// datasource db { provider = "sqlite" ... }
|
|
52
|
+
const m = schemaText.match(/datasource\s+db\s*\{[\s\S]*?\bprovider\s*=\s*\"([a-zA-Z0-9_-]+)\"/m);
|
|
53
|
+
return m?.[1] ?? '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertServerPrismaProviderMatches({ serverComponentName, serverDir }) {
|
|
57
|
+
const schemaPath = join(serverDir, 'prisma', 'schema.prisma');
|
|
58
|
+
let schemaText = '';
|
|
59
|
+
try {
|
|
60
|
+
schemaText = readFileSync(schemaPath, 'utf-8');
|
|
61
|
+
} catch {
|
|
62
|
+
// If it doesn't exist, skip validation; not every server component necessarily uses Prisma.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const provider = detectPrismaProvider(schemaText);
|
|
67
|
+
if (!provider) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (serverComponentName === 'happy-server-light' && provider !== 'sqlite') {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[server] happy-server-light expects Prisma datasource provider \"sqlite\", but found \"${provider}\" in:\n` +
|
|
74
|
+
`- ${schemaPath}\n` +
|
|
75
|
+
`This usually means you're pointing happy-server-light at an upstream happy-server checkout/PR (Postgres).\n` +
|
|
76
|
+
`Fix: either switch server flavor to happy-server, or point happy-server-light at a fork checkout that keeps sqlite support.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (serverComponentName === 'happy-server' && provider === 'sqlite') {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[server] happy-server expects Prisma datasource provider \"postgresql\", but found \"sqlite\" in:\n` +
|
|
83
|
+
`- ${schemaPath}\n` +
|
|
84
|
+
`Fix: either switch server flavor to happy-server-light, or point happy-server at the full-server checkout.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
package/scripts/worktrees.mjs
CHANGED
|
@@ -14,6 +14,14 @@ import { existsSync } from 'node:fs';
|
|
|
14
14
|
import { getHomeEnvLocalPath, getHomeEnvPath, resolveUserConfigEnvPath } from './utils/config.mjs';
|
|
15
15
|
import { detectServerComponentDirMismatch } from './utils/validate.mjs';
|
|
16
16
|
|
|
17
|
+
function getActiveStackName() {
|
|
18
|
+
return (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isMainStack() {
|
|
22
|
+
return getActiveStackName() === 'main';
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
function getWorktreesRoot(rootDir) {
|
|
18
26
|
return join(getComponentsDir(rootDir), '.worktrees');
|
|
19
27
|
}
|
|
@@ -142,6 +150,7 @@ async function ensureWorktreeExclude(worktreeDir, patterns) {
|
|
|
142
150
|
const want = patterns.map((p) => p.trim()).filter(Boolean).filter((p) => !existingLines.has(p));
|
|
143
151
|
if (!want.length) return;
|
|
144
152
|
const next = (existing ? existing.replace(/\s*$/, '') + '\n' : '') + want.join('\n') + '\n';
|
|
153
|
+
await mkdir(dirname(excludePath), { recursive: true });
|
|
145
154
|
await writeFile(excludePath, next, 'utf-8');
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -475,13 +484,33 @@ async function cmdMigrate({ rootDir }) {
|
|
|
475
484
|
return { moved: totalMoved, branchesRenamed: totalRenamed };
|
|
476
485
|
}
|
|
477
486
|
|
|
478
|
-
async function cmdUse({ rootDir, args }) {
|
|
487
|
+
async function cmdUse({ rootDir, args, flags }) {
|
|
479
488
|
const component = args[0];
|
|
480
489
|
const spec = args[1];
|
|
481
490
|
if (!component || !spec) {
|
|
482
491
|
throw new Error('[wt] usage: happys wt use <component> <owner/branch|path|default>');
|
|
483
492
|
}
|
|
484
493
|
|
|
494
|
+
// Safety: main stack should not be repointed to arbitrary worktrees by default.
|
|
495
|
+
// This is the most common “oops, the main stack now runs my PR checkout” footgun (especially for agents).
|
|
496
|
+
const force = Boolean(flags?.has('--force'));
|
|
497
|
+
if (!force && isMainStack() && spec !== 'default' && spec !== 'main') {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`[wt] refusing to change main stack component override by default.\n` +
|
|
500
|
+
`- stack: main\n` +
|
|
501
|
+
`- component: ${component}\n` +
|
|
502
|
+
`- requested: ${spec}\n` +
|
|
503
|
+
`\n` +
|
|
504
|
+
`Recommendation:\n` +
|
|
505
|
+
`- Create a new isolated stack and switch that stack instead:\n` +
|
|
506
|
+
` happys stack new exp1 --interactive\n` +
|
|
507
|
+
` happys stack wt exp1 -- use ${component} ${spec}\n` +
|
|
508
|
+
`\n` +
|
|
509
|
+
`If you really intend to repoint the main stack, re-run with --force:\n` +
|
|
510
|
+
` happys wt use ${component} ${spec} --force\n`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
485
514
|
const key = componentDirEnvKey(component);
|
|
486
515
|
const worktreesRoot = getWorktreesRoot(rootDir);
|
|
487
516
|
const envPath = process.env.HAPPY_STACKS_ENV_FILE?.trim()
|
|
@@ -577,10 +606,10 @@ async function cmdUseInteractive({ rootDir }) {
|
|
|
577
606
|
options: specs.map((s) => ({ label: s, value: s })),
|
|
578
607
|
defaultIndex: 0,
|
|
579
608
|
});
|
|
580
|
-
await cmdUse({ rootDir, args: [component, picked] });
|
|
609
|
+
await cmdUse({ rootDir, args: [component, picked], flags: new Set(['--force']) });
|
|
581
610
|
return;
|
|
582
611
|
}
|
|
583
|
-
await cmdUse({ rootDir, args: [component, 'default'] });
|
|
612
|
+
await cmdUse({ rootDir, args: [component, 'default'], flags: new Set(['--force']) });
|
|
584
613
|
});
|
|
585
614
|
}
|
|
586
615
|
|
|
@@ -652,7 +681,24 @@ async function cmdNew({ rootDir, argv }) {
|
|
|
652
681
|
const deps = await maybeSetupDeps({ repoRoot, baseDir: baseWorktreeDir || '', worktreeDir: destPath, depsMode });
|
|
653
682
|
|
|
654
683
|
const shouldUse = flags.has('--use');
|
|
684
|
+
const force = flags.has('--force');
|
|
655
685
|
if (shouldUse) {
|
|
686
|
+
if (isMainStack() && !force) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`[wt] refusing to set main stack component override via --use by default.\n` +
|
|
689
|
+
`- stack: main\n` +
|
|
690
|
+
`- component: ${component}\n` +
|
|
691
|
+
`- new worktree: ${destPath}\n` +
|
|
692
|
+
`\n` +
|
|
693
|
+
`Recommendation:\n` +
|
|
694
|
+
`- Use an isolated stack instead:\n` +
|
|
695
|
+
` happys stack new exp1 --interactive\n` +
|
|
696
|
+
` happys stack wt exp1 -- use ${component} ${owner}/${slug}\n` +
|
|
697
|
+
`\n` +
|
|
698
|
+
`If you really intend to repoint the main stack, re-run with --force:\n` +
|
|
699
|
+
` happys wt new ${component} ${slug} --use --force\n`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
656
702
|
const key = componentDirEnvKey(component);
|
|
657
703
|
await ensureEnvLocalUpdated({ rootDir, updates: [{ key, value: destPath }] });
|
|
658
704
|
}
|
|
@@ -776,7 +822,7 @@ async function cmdPr({ rootDir, argv }) {
|
|
|
776
822
|
const shouldUse = flags.has('--use');
|
|
777
823
|
if (shouldUse) {
|
|
778
824
|
// Reuse cmdUse so it writes to env.local or stack env file depending on context.
|
|
779
|
-
await cmdUse({ rootDir, args: [component, destPath] });
|
|
825
|
+
await cmdUse({ rootDir, args: [component, destPath], flags });
|
|
780
826
|
}
|
|
781
827
|
|
|
782
828
|
const newHead = (await git(destPath, ['rev-parse', 'HEAD'])).trim();
|
|
@@ -1535,9 +1581,9 @@ async function main() {
|
|
|
1535
1581
|
' happys wt sync <component> [--remote=<name>] [--json]',
|
|
1536
1582
|
' happys wt sync-all [--remote=<name>] [--json]',
|
|
1537
1583
|
' happys wt list <component> [--json]',
|
|
1538
|
-
' happys wt new <component> <slug> [--from=upstream|origin] [--remote=<name>] [--base=<ref>|--base-worktree=<spec>] [--deps=none|link|install|link-or-install] [--use] [--interactive|-i] [--json]',
|
|
1584
|
+
' happys wt new <component> <slug> [--from=upstream|origin] [--remote=<name>] [--base=<ref>|--base-worktree=<spec>] [--deps=none|link|install|link-or-install] [--use] [--force] [--interactive|-i] [--json]',
|
|
1539
1585
|
' happys wt pr <component> <pr-url|number> [--remote=upstream] [--slug=<name>] [--deps=none|link|install|link-or-install] [--use] [--update] [--stash|--stash-keep] [--force] [--json]',
|
|
1540
|
-
' happys wt use <component> <owner/branch|path|default|main> [--interactive|-i] [--json]',
|
|
1586
|
+
' happys wt use <component> <owner/branch|path|default|main> [--force] [--interactive|-i] [--json]',
|
|
1541
1587
|
' happys wt status <component> [worktreeSpec|default|path] [--json]',
|
|
1542
1588
|
' happys wt update <component> [worktreeSpec|default|path] [--remote=upstream] [--base=<ref>] [--rebase|--merge] [--dry-run] [--stash|--stash-keep] [--force] [--json]',
|
|
1543
1589
|
' happys wt update-all [component] [--remote=upstream] [--base=<ref>] [--rebase|--merge] [--dry-run] [--stash|--stash-keep] [--force] [--json]',
|
|
@@ -1569,7 +1615,7 @@ async function main() {
|
|
|
1569
1615
|
if (interactive && isTty()) {
|
|
1570
1616
|
await cmdUseInteractive({ rootDir });
|
|
1571
1617
|
} else {
|
|
1572
|
-
const res = await cmdUse({ rootDir, args: positionals.slice(1) });
|
|
1618
|
+
const res = await cmdUse({ rootDir, args: positionals.slice(1), flags });
|
|
1573
1619
|
printResult({ json, data: res, text: `[wt] ${res.component}: active dir -> ${res.activeDir}` });
|
|
1574
1620
|
}
|
|
1575
1621
|
return;
|