happy-stacks 0.1.2 → 0.2.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 +121 -83
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +560 -112
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +130 -20
- package/scripts/dev.mjs +201 -133
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +43 -20
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +25 -15
- package/scripts/mobile.mjs +13 -7
- package/scripts/run.mjs +114 -27
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +15 -2
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +1792 -254
- package/scripts/stop.mjs +6 -3
- package/scripts/tailscale.mjs +17 -2
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +2 -2
- package/scripts/ui_gateway.mjs +2 -2
- package/scripts/uninstall.mjs +18 -10
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +6 -2
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +60 -11
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +4 -2
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +100 -46
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +121 -20
- package/scripts/utils/proc.mjs +29 -2
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +24 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +79 -30
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +82 -8
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getLegacyStorageRoot, getStacksStorageRoot } from './paths.mjs';
|
|
6
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './sandbox.mjs';
|
|
7
|
+
|
|
8
|
+
export async function listAllStackNames() {
|
|
9
|
+
const names = new Set(['main']);
|
|
10
|
+
const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
|
|
11
|
+
const roots = [
|
|
12
|
+
// New layout: ~/.happy/stacks/<name>/env
|
|
13
|
+
getStacksStorageRoot(),
|
|
14
|
+
// Legacy layout: ~/.happy/local/stacks/<name>/env
|
|
15
|
+
...(allowLegacy ? [join(getLegacyStorageRoot(), 'stacks')] : []),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const root of roots) {
|
|
19
|
+
let entries = [];
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line no-await-in-loop
|
|
22
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
entries = [];
|
|
25
|
+
}
|
|
26
|
+
for (const ent of entries) {
|
|
27
|
+
if (!ent.isDirectory()) continue;
|
|
28
|
+
const name = ent.name;
|
|
29
|
+
if (!name || name.startsWith('.')) continue;
|
|
30
|
+
const envPath = join(root, name, 'env');
|
|
31
|
+
if (existsSync(envPath)) {
|
|
32
|
+
names.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Array.from(names).sort();
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
function safeWatch(path, handler) {
|
|
4
|
+
try {
|
|
5
|
+
// Node supports recursive watching on macOS and Windows. On Linux this may throw; we fail closed by returning null.
|
|
6
|
+
return watch(path, { recursive: true }, handler);
|
|
7
|
+
} catch {
|
|
8
|
+
try {
|
|
9
|
+
return watch(path, {}, handler);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Very small, dependency-free debounced watcher.
|
|
18
|
+
* Intended for dev ergonomics (rebuild/restart), not for correctness-critical logic.
|
|
19
|
+
*/
|
|
20
|
+
export function watchDebounced({ paths, debounceMs = 500, onChange } = {}) {
|
|
21
|
+
const list = Array.isArray(paths) ? paths.filter(Boolean) : [];
|
|
22
|
+
if (!list.length) return null;
|
|
23
|
+
if (typeof onChange !== 'function') return null;
|
|
24
|
+
|
|
25
|
+
let closed = false;
|
|
26
|
+
let t = null;
|
|
27
|
+
const watchers = [];
|
|
28
|
+
|
|
29
|
+
const trigger = (eventType, filename) => {
|
|
30
|
+
if (closed) return;
|
|
31
|
+
if (t) clearTimeout(t);
|
|
32
|
+
t = setTimeout(() => {
|
|
33
|
+
t = null;
|
|
34
|
+
try {
|
|
35
|
+
onChange({ eventType, filename });
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
}, debounceMs);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const p of list) {
|
|
43
|
+
const w = safeWatch(p, trigger);
|
|
44
|
+
if (w) watchers.push(w);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!watchers.length) return null;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
close() {
|
|
51
|
+
closed = true;
|
|
52
|
+
if (t) clearTimeout(t);
|
|
53
|
+
for (const w of watchers) {
|
|
54
|
+
try {
|
|
55
|
+
w.close();
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import { isAbsolute, join } from 'node:path';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { getComponentsDir } from './paths.mjs';
|
|
4
4
|
import { pathExists } from './fs.mjs';
|
|
5
5
|
import { run, runCapture } from './proc.mjs';
|
|
@@ -23,6 +23,62 @@ export function componentRepoDir(rootDir, component) {
|
|
|
23
23
|
return join(getComponentsDir(rootDir), component);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function isComponentWorktreePath({ rootDir, component, dir }) {
|
|
27
|
+
const raw = String(dir ?? '').trim();
|
|
28
|
+
if (!raw) return false;
|
|
29
|
+
const abs = resolve(raw);
|
|
30
|
+
const root = resolve(join(getWorktreesRoot(rootDir), component)) + '/';
|
|
31
|
+
return abs.startsWith(root);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function worktreeSpecFromDir({ rootDir, component, dir }) {
|
|
35
|
+
const raw = String(dir ?? '').trim();
|
|
36
|
+
if (!raw) return null;
|
|
37
|
+
if (!isComponentWorktreePath({ rootDir, component, dir: raw })) return null;
|
|
38
|
+
const abs = resolve(raw);
|
|
39
|
+
const root = resolve(join(getWorktreesRoot(rootDir), component)) + '/';
|
|
40
|
+
const rel = abs.slice(root.length).split('/').filter(Boolean);
|
|
41
|
+
if (rel.length < 2) return null;
|
|
42
|
+
// rel = [owner, ...branchParts]
|
|
43
|
+
return rel.join('/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function inferRemoteNameForOwner({ repoDir, owner }) {
|
|
47
|
+
const want = String(owner ?? '').trim();
|
|
48
|
+
if (!want) return 'upstream';
|
|
49
|
+
|
|
50
|
+
const candidates = ['upstream', 'origin', 'fork'];
|
|
51
|
+
for (const remoteName of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
const url = (await runCapture('git', ['remote', 'get-url', remoteName], { cwd: repoDir })).trim();
|
|
54
|
+
const o = parseGithubOwner(url);
|
|
55
|
+
if (o && o === want) {
|
|
56
|
+
return remoteName;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore missing remote
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 'upstream';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createWorktreeFromBaseWorktree({
|
|
66
|
+
rootDir,
|
|
67
|
+
component,
|
|
68
|
+
slug,
|
|
69
|
+
baseWorktreeSpec,
|
|
70
|
+
remoteName = 'upstream',
|
|
71
|
+
depsMode = '',
|
|
72
|
+
}) {
|
|
73
|
+
const args = ['wt', 'new', component, slug, `--remote=${remoteName}`, `--base-worktree=${baseWorktreeSpec}`];
|
|
74
|
+
if (depsMode) args.push(`--deps=${depsMode}`);
|
|
75
|
+
await run(process.execPath, [join(rootDir, 'bin', 'happys.mjs'), ...args], { cwd: rootDir });
|
|
76
|
+
|
|
77
|
+
const repoDir = componentRepoDir(rootDir, component);
|
|
78
|
+
const owner = await getRemoteOwner({ repoDir, remoteName });
|
|
79
|
+
return join(getWorktreesRoot(rootDir), component, owner, ...slug.split('/'));
|
|
80
|
+
}
|
|
81
|
+
|
|
26
82
|
export function resolveComponentSpecToDir({ rootDir, component, spec }) {
|
|
27
83
|
const raw = (spec ?? '').trim();
|
|
28
84
|
if (!raw || raw === 'default') {
|
package/scripts/where.mjs
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
5
|
|
|
7
|
-
import { parseArgs } from './utils/args.mjs';
|
|
6
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
7
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
8
8
|
import { getComponentsDir, getComponentDir, getHappyStacksHomeDir, getRootDir, getStackLabel, getStackName, getWorkspaceDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
9
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
9
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
10
|
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
14
|
-
}
|
|
11
|
+
import { getCanonicalHomeDir, getCanonicalHomeEnvPath } from './utils/config.mjs';
|
|
12
|
+
import { getSandboxDir } from './utils/sandbox.mjs';
|
|
15
13
|
|
|
16
14
|
function getHomeEnvPaths() {
|
|
17
15
|
const homeDir = getHappyStacksHomeDir();
|
|
@@ -36,6 +34,9 @@ async function main() {
|
|
|
36
34
|
|
|
37
35
|
const rootDir = getRootDir(import.meta.url);
|
|
38
36
|
const homeDir = getHappyStacksHomeDir();
|
|
37
|
+
const canonicalHomeDir = getCanonicalHomeDir();
|
|
38
|
+
const canonicalEnv = getCanonicalHomeEnvPath();
|
|
39
|
+
const sandboxDir = getSandboxDir();
|
|
39
40
|
const runtimeDir = getRuntimeDir();
|
|
40
41
|
const workspaceDir = getWorkspaceDir(rootDir);
|
|
41
42
|
const componentsDir = getComponentsDir(rootDir);
|
|
@@ -60,12 +61,15 @@ async function main() {
|
|
|
60
61
|
data: {
|
|
61
62
|
ok: true,
|
|
62
63
|
rootDir,
|
|
64
|
+
sandbox: sandboxDir ? { enabled: true, dir: sandboxDir } : { enabled: false },
|
|
63
65
|
homeDir,
|
|
66
|
+
canonicalHomeDir,
|
|
64
67
|
runtimeDir,
|
|
65
68
|
workspaceDir,
|
|
66
69
|
componentsDir,
|
|
67
70
|
stack: { name: stackName, label: stackLabel },
|
|
68
71
|
envFiles: {
|
|
72
|
+
canonical: { path: canonicalEnv, exists: existsSync(canonicalEnv) },
|
|
69
73
|
homeEnv: { path: homeEnv, exists: existsSync(homeEnv) },
|
|
70
74
|
homeLocal: { path: homeLocal, exists: existsSync(homeLocal) },
|
|
71
75
|
active: resolvedActiveEnv ? { path: resolvedActiveEnv.envPath, exists: existsSync(resolvedActiveEnv.envPath) } : null,
|
|
@@ -80,12 +84,15 @@ async function main() {
|
|
|
80
84
|
},
|
|
81
85
|
text: [
|
|
82
86
|
`[where] root: ${rootDir}`,
|
|
87
|
+
sandboxDir ? `[where] sandbox: ${sandboxDir}` : null,
|
|
88
|
+
`[where] canonical: ${canonicalHomeDir}`,
|
|
83
89
|
`[where] home: ${homeDir}`,
|
|
84
90
|
`[where] runtime: ${runtimeDir}`,
|
|
85
91
|
`[where] workspace: ${workspaceDir}`,
|
|
86
92
|
`[where] components:${componentsDir}`,
|
|
87
93
|
'',
|
|
88
94
|
`[where] stack: ${stackName} (${stackLabel})`,
|
|
95
|
+
`[where] env (canonical pointer): ${existsSync(canonicalEnv) ? canonicalEnv : `${canonicalEnv} (missing)`}`,
|
|
89
96
|
`[where] env (home defaults): ${existsSync(homeEnv) ? homeEnv : `${homeEnv} (missing)`}`,
|
|
90
97
|
`[where] env (home overrides): ${existsSync(homeLocal) ? homeLocal : `${homeLocal} (missing)`}`,
|
|
91
98
|
`[where] env (active): ${resolvedActiveEnv?.envPath ? resolvedActiveEnv.envPath : '(none)'}`,
|
package/scripts/worktrees.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
2
|
import { mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import { parseArgs } from './utils/args.mjs';
|
|
4
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
5
5
|
import { pathExists } from './utils/fs.mjs';
|
|
6
6
|
import { run, runCapture } from './utils/proc.mjs';
|
|
7
|
-
import { componentDirEnvKey, getComponentDir, getComponentsDir, getRootDir, getWorkspaceDir } from './utils/paths.mjs';
|
|
8
|
-
import { parseGithubOwner } from './utils/worktrees.mjs';
|
|
9
|
-
import { isTty, prompt, promptSelect, withRl } from './utils/wizard.mjs';
|
|
10
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
7
|
+
import { componentDirEnvKey, getComponentDir, getComponentsDir, getHappyStacksHomeDir, getRootDir, getWorkspaceDir } from './utils/paths.mjs';
|
|
8
|
+
import { inferRemoteNameForOwner, parseGithubOwner } from './utils/worktrees.mjs';
|
|
9
|
+
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
10
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
11
11
|
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
12
12
|
import { ensureEnvFileUpdated } from './utils/env_file.mjs';
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
@@ -369,6 +369,21 @@ async function migrateComponentWorktrees({ rootDir, component }) {
|
|
|
369
369
|
let renamed = 0;
|
|
370
370
|
|
|
371
371
|
const componentsDir = getComponentsDir(rootDir);
|
|
372
|
+
// NOTE: getWorkspaceDir() is influenced by HAPPY_STACKS_WORKSPACE_DIR, which for this repo
|
|
373
|
+
// points at the current workspace. For migration we specifically want to consider the
|
|
374
|
+
// historical home workspace at: <home>/workspace/components
|
|
375
|
+
const legacyHomeWorkspaceComponentsDir = join(getHappyStacksHomeDir(), 'workspace', 'components');
|
|
376
|
+
const allowedComponentRoots = [componentsDir];
|
|
377
|
+
try {
|
|
378
|
+
if (
|
|
379
|
+
existsSync(legacyHomeWorkspaceComponentsDir) &&
|
|
380
|
+
resolve(legacyHomeWorkspaceComponentsDir) !== resolve(componentsDir)
|
|
381
|
+
) {
|
|
382
|
+
allowedComponentRoots.push(legacyHomeWorkspaceComponentsDir);
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// ignore
|
|
386
|
+
}
|
|
372
387
|
|
|
373
388
|
for (const wt of worktrees) {
|
|
374
389
|
const wtPath = wt.path;
|
|
@@ -381,8 +396,14 @@ async function migrateComponentWorktrees({ rootDir, component }) {
|
|
|
381
396
|
continue;
|
|
382
397
|
}
|
|
383
398
|
|
|
384
|
-
// Only migrate worktrees living under
|
|
385
|
-
|
|
399
|
+
// Only migrate worktrees living under either:
|
|
400
|
+
// - current workspace components folder, or
|
|
401
|
+
// - legacy home workspace components folder (~/.happy-stacks/workspace/components)
|
|
402
|
+
// This is necessary when users switch HAPPY_STACKS_WORKSPACE_DIR, otherwise git will keep
|
|
403
|
+
// worktrees "stuck" in the old workspace and branches can't be re-used in the new workspace.
|
|
404
|
+
const resolvedWt = resolve(wtPath);
|
|
405
|
+
const okRoot = allowedComponentRoots.some((d) => resolvedWt.startsWith(resolve(d) + '/'));
|
|
406
|
+
if (!okRoot) {
|
|
386
407
|
continue;
|
|
387
408
|
}
|
|
388
409
|
|
|
@@ -675,7 +696,13 @@ async function cmdNew({ rootDir, argv }) {
|
|
|
675
696
|
await git(repoRoot, ['branch', '--set-upstream-to', `${remoteName}/${defaultBranch}`, mirrorBranch]).catch(() => {});
|
|
676
697
|
}
|
|
677
698
|
|
|
678
|
-
|
|
699
|
+
// If the branch already exists (common when migrating between workspaces),
|
|
700
|
+
// attach a new worktree to that branch instead of failing.
|
|
701
|
+
if (await gitOk(repoRoot, ['show-ref', '--verify', `refs/heads/${branchName}`])) {
|
|
702
|
+
await git(repoRoot, ['worktree', 'add', destPath, branchName]);
|
|
703
|
+
} else {
|
|
704
|
+
await git(repoRoot, ['worktree', 'add', '-b', branchName, destPath, base]);
|
|
705
|
+
}
|
|
679
706
|
|
|
680
707
|
const depsMode = parseDepsMode(kv.get('--deps'));
|
|
681
708
|
const deps = await maybeSetupDeps({ repoRoot, baseDir: baseWorktreeDir || '', worktreeDir: destPath, depsMode });
|
|
@@ -705,6 +732,43 @@ async function cmdNew({ rootDir, argv }) {
|
|
|
705
732
|
return { component, branch: branchName, path: destPath, base, used: shouldUse, deps };
|
|
706
733
|
}
|
|
707
734
|
|
|
735
|
+
async function cmdDuplicate({ rootDir, argv }) {
|
|
736
|
+
const { flags, kv } = parseArgs(argv);
|
|
737
|
+
const json = wantsJson(argv, { flags });
|
|
738
|
+
|
|
739
|
+
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
740
|
+
const component = positionals[1];
|
|
741
|
+
const fromSpec = positionals[2];
|
|
742
|
+
const slug = positionals[3];
|
|
743
|
+
if (!component || !fromSpec || !slug) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
'[wt] usage: happys wt duplicate <component> <fromWorktreeSpec|path|active|default> <newSlug> [--remote=<name>] [--deps=none|link|install|link-or-install] [--use] [--json]'
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Prefer inferring the remote from the source spec's owner when possible (owner/<branch...>).
|
|
750
|
+
const remoteOverride = (kv.get('--remote') ?? '').trim();
|
|
751
|
+
let remoteName = remoteOverride;
|
|
752
|
+
if (!remoteName && !isAbsolute(fromSpec)) {
|
|
753
|
+
const owner = String(fromSpec).trim().split('/')[0];
|
|
754
|
+
if (owner && owner !== 'active' && owner !== 'default' && owner !== 'main') {
|
|
755
|
+
const repoRoot = getComponentRepoRoot(rootDir, component);
|
|
756
|
+
remoteName = await normalizeRemoteName(repoRoot, await inferRemoteNameForOwner({ repoDir: repoRoot, owner }));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const depsMode = (kv.get('--deps') ?? '').trim();
|
|
761
|
+
const forwarded = ['new', component, slug, `--base-worktree=${fromSpec}`];
|
|
762
|
+
if (remoteName) forwarded.push(`--remote=${remoteName}`);
|
|
763
|
+
if (depsMode) forwarded.push(`--deps=${depsMode}`);
|
|
764
|
+
if (flags.has('--use')) forwarded.push('--use');
|
|
765
|
+
if (flags.has('--force')) forwarded.push('--force');
|
|
766
|
+
if (json) forwarded.push('--json');
|
|
767
|
+
|
|
768
|
+
// Delegate to cmdNew for the actual implementation (single source of truth).
|
|
769
|
+
return await cmdNew({ rootDir, argv: forwarded });
|
|
770
|
+
}
|
|
771
|
+
|
|
708
772
|
async function cmdPr({ rootDir, argv }) {
|
|
709
773
|
const { flags, kv } = parseArgs(argv);
|
|
710
774
|
const json = wantsJson(argv, { flags });
|
|
@@ -1582,6 +1646,7 @@ async function main() {
|
|
|
1582
1646
|
' happys wt sync-all [--remote=<name>] [--json]',
|
|
1583
1647
|
' happys wt list <component> [--json]',
|
|
1584
1648
|
' 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]',
|
|
1649
|
+
' happys wt duplicate <component> <fromWorktreeSpec|path|active|default> <newSlug> [--remote=<name>] [--deps=none|link|install|link-or-install] [--use] [--json]',
|
|
1585
1650
|
' 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]',
|
|
1586
1651
|
' happys wt use <component> <owner/branch|path|default|main> [--force] [--interactive|-i] [--json]',
|
|
1587
1652
|
' happys wt status <component> [worktreeSpec|default|path] [--json]',
|
|
@@ -1633,6 +1698,15 @@ async function main() {
|
|
|
1633
1698
|
}
|
|
1634
1699
|
return;
|
|
1635
1700
|
}
|
|
1701
|
+
if (cmd === 'duplicate') {
|
|
1702
|
+
const res = await cmdDuplicate({ rootDir, argv });
|
|
1703
|
+
printResult({
|
|
1704
|
+
json,
|
|
1705
|
+
data: res,
|
|
1706
|
+
text: `[wt] duplicated ${res.component} worktree: ${res.path} (${res.branch} based on ${res.base})`,
|
|
1707
|
+
});
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1636
1710
|
if (cmd === 'pr') {
|
|
1637
1711
|
const res = await cmdPr({ rootDir, argv });
|
|
1638
1712
|
printResult({
|
|
File without changes
|
|
File without changes
|
|
File without changes
|