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.
Files changed (91) hide show
  1. package/README.md +121 -83
  2. package/bin/happys.mjs +70 -10
  3. package/docs/edison.md +381 -0
  4. package/docs/happy-development.md +733 -0
  5. package/docs/menubar.md +54 -0
  6. package/docs/paths-and-env.md +141 -0
  7. package/docs/stacks.md +39 -0
  8. package/extras/swiftbar/auth-login.sh +5 -2
  9. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  10. package/extras/swiftbar/happy-stacks.5s.sh +131 -81
  11. package/extras/swiftbar/happys-term.sh +15 -38
  12. package/extras/swiftbar/happys.sh +15 -32
  13. package/extras/swiftbar/install.sh +99 -13
  14. package/extras/swiftbar/lib/git.sh +309 -1
  15. package/extras/swiftbar/lib/icons.sh +2 -2
  16. package/extras/swiftbar/lib/render.sh +209 -80
  17. package/extras/swiftbar/lib/system.sh +27 -4
  18. package/extras/swiftbar/lib/utils.sh +311 -28
  19. package/extras/swiftbar/pnpm.sh +2 -1
  20. package/extras/swiftbar/set-interval.sh +10 -5
  21. package/extras/swiftbar/set-server-flavor.sh +11 -2
  22. package/extras/swiftbar/wt-pr.sh +9 -2
  23. package/package.json +2 -1
  24. package/scripts/auth.mjs +560 -112
  25. package/scripts/build.mjs +24 -4
  26. package/scripts/cli-link.mjs +3 -3
  27. package/scripts/completion.mjs +15 -8
  28. package/scripts/daemon.mjs +130 -20
  29. package/scripts/dev.mjs +201 -133
  30. package/scripts/doctor.mjs +26 -21
  31. package/scripts/edison.mjs +1828 -0
  32. package/scripts/happy.mjs +3 -7
  33. package/scripts/init.mjs +43 -20
  34. package/scripts/install.mjs +14 -8
  35. package/scripts/lint.mjs +145 -0
  36. package/scripts/menubar.mjs +81 -8
  37. package/scripts/migrate.mjs +25 -15
  38. package/scripts/mobile.mjs +13 -7
  39. package/scripts/run.mjs +114 -27
  40. package/scripts/self.mjs +3 -7
  41. package/scripts/server_flavor.mjs +3 -3
  42. package/scripts/service.mjs +15 -2
  43. package/scripts/setup.mjs +790 -0
  44. package/scripts/setup_pr.mjs +182 -0
  45. package/scripts/stack.mjs +1792 -254
  46. package/scripts/stop.mjs +6 -3
  47. package/scripts/tailscale.mjs +17 -2
  48. package/scripts/test.mjs +144 -0
  49. package/scripts/tui.mjs +556 -0
  50. package/scripts/typecheck.mjs +2 -2
  51. package/scripts/ui_gateway.mjs +2 -2
  52. package/scripts/uninstall.mjs +18 -10
  53. package/scripts/utils/auth_files.mjs +58 -0
  54. package/scripts/utils/auth_login_ux.mjs +76 -0
  55. package/scripts/utils/auth_sources.mjs +12 -0
  56. package/scripts/utils/browser.mjs +22 -0
  57. package/scripts/utils/canonical_home.mjs +20 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  60. package/scripts/utils/config.mjs +6 -2
  61. package/scripts/utils/dev_auth_key.mjs +169 -0
  62. package/scripts/utils/dev_daemon.mjs +104 -0
  63. package/scripts/utils/dev_expo_web.mjs +112 -0
  64. package/scripts/utils/dev_server.mjs +183 -0
  65. package/scripts/utils/env.mjs +60 -11
  66. package/scripts/utils/env_file.mjs +36 -0
  67. package/scripts/utils/expo.mjs +4 -2
  68. package/scripts/utils/handy_master_secret.mjs +94 -0
  69. package/scripts/utils/happy_server_infra.mjs +100 -46
  70. package/scripts/utils/localhost_host.mjs +17 -0
  71. package/scripts/utils/ownership.mjs +135 -0
  72. package/scripts/utils/paths.mjs +5 -2
  73. package/scripts/utils/pm.mjs +121 -20
  74. package/scripts/utils/proc.mjs +29 -2
  75. package/scripts/utils/runtime.mjs +1 -3
  76. package/scripts/utils/sandbox.mjs +14 -0
  77. package/scripts/utils/server.mjs +24 -0
  78. package/scripts/utils/server_port.mjs +9 -0
  79. package/scripts/utils/server_urls.mjs +54 -0
  80. package/scripts/utils/stack_context.mjs +23 -0
  81. package/scripts/utils/stack_runtime_state.mjs +104 -0
  82. package/scripts/utils/stack_startup.mjs +208 -0
  83. package/scripts/utils/stack_stop.mjs +79 -30
  84. package/scripts/utils/stacks.mjs +38 -0
  85. package/scripts/utils/watch.mjs +63 -0
  86. package/scripts/utils/worktrees.mjs +57 -1
  87. package/scripts/where.mjs +14 -7
  88. package/scripts/worktrees.mjs +82 -8
  89. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  90. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  91. /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
- function expandHome(p) {
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)'}`,
@@ -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 this happy-stacks components folder.
385
- if (!resolve(wtPath).startsWith(resolve(componentsDir) + '/')) {
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
- await git(repoRoot, ['worktree', 'add', '-b', branchName, destPath, base]);
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