@cod3vil/trunk 0.1.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +69 -0
  4. package/dist/commands/clone.d.ts +6 -0
  5. package/dist/commands/clone.js +114 -0
  6. package/dist/commands/init.d.ts +6 -0
  7. package/dist/commands/init.js +235 -0
  8. package/dist/commands/new.d.ts +6 -0
  9. package/dist/commands/new.js +357 -0
  10. package/dist/core/adopt.d.ts +14 -0
  11. package/dist/core/adopt.js +157 -0
  12. package/dist/core/agents.d.ts +30 -0
  13. package/dist/core/agents.js +31 -0
  14. package/dist/core/arguments.d.ts +92 -0
  15. package/dist/core/arguments.js +93 -0
  16. package/dist/core/detect.d.ts +28 -0
  17. package/dist/core/detect.js +169 -0
  18. package/dist/core/diff.d.ts +37 -0
  19. package/dist/core/diff.js +140 -0
  20. package/dist/core/env.d.ts +63 -0
  21. package/dist/core/env.js +140 -0
  22. package/dist/core/generate/aliases.d.ts +7 -0
  23. package/dist/core/generate/aliases.js +45 -0
  24. package/dist/core/generate/header.d.ts +2 -0
  25. package/dist/core/generate/header.js +81 -0
  26. package/dist/core/generate/index.d.ts +8 -0
  27. package/dist/core/generate/index.js +74 -0
  28. package/dist/core/generate/proxy.d.ts +8 -0
  29. package/dist/core/generate/proxy.js +56 -0
  30. package/dist/core/generate/steps.d.ts +7 -0
  31. package/dist/core/generate/steps.js +88 -0
  32. package/dist/core/generate/tmux.d.ts +4 -0
  33. package/dist/core/generate/tmux.js +98 -0
  34. package/dist/core/generate/toml.d.ts +16 -0
  35. package/dist/core/generate/toml.js +68 -0
  36. package/dist/core/gh.d.ts +60 -0
  37. package/dist/core/gh.js +101 -0
  38. package/dist/core/git.d.ts +79 -0
  39. package/dist/core/git.js +211 -0
  40. package/dist/core/journal.d.ts +54 -0
  41. package/dist/core/journal.js +147 -0
  42. package/dist/core/log.d.ts +8 -0
  43. package/dist/core/log.js +38 -0
  44. package/dist/core/pipeline.d.ts +119 -0
  45. package/dist/core/pipeline.js +473 -0
  46. package/dist/core/platform.d.ts +10 -0
  47. package/dist/core/platform.js +26 -0
  48. package/dist/core/prefix.d.ts +25 -0
  49. package/dist/core/prefix.js +59 -0
  50. package/dist/core/process.d.ts +27 -0
  51. package/dist/core/process.js +43 -0
  52. package/dist/core/repo.d.ts +79 -0
  53. package/dist/core/repo.js +294 -0
  54. package/dist/core/resolve.d.ts +127 -0
  55. package/dist/core/resolve.js +488 -0
  56. package/dist/core/result.d.ts +27 -0
  57. package/dist/core/result.js +32 -0
  58. package/dist/core/settings.d.ts +51 -0
  59. package/dist/core/settings.js +83 -0
  60. package/dist/core/tmuxRename.d.ts +36 -0
  61. package/dist/core/tmuxRename.js +79 -0
  62. package/dist/core/validate.d.ts +22 -0
  63. package/dist/core/validate.js +111 -0
  64. package/dist/core/version.d.ts +2 -0
  65. package/dist/core/version.js +35 -0
  66. package/dist/core/words.d.ts +16 -0
  67. package/dist/core/words.js +198 -0
  68. package/dist/core/wt.d.ts +41 -0
  69. package/dist/core/wt.js +59 -0
  70. package/dist/ui/SetupForm.d.ts +55 -0
  71. package/dist/ui/SetupForm.js +354 -0
  72. package/dist/ui/Summary.d.ts +15 -0
  73. package/dist/ui/Summary.js +74 -0
  74. package/dist/ui/fields/MultiSelect.d.ts +17 -0
  75. package/dist/ui/fields/MultiSelect.js +66 -0
  76. package/dist/ui/fields/Select.d.ts +17 -0
  77. package/dist/ui/fields/Select.js +37 -0
  78. package/dist/ui/fields/TextInput.d.ts +13 -0
  79. package/dist/ui/fields/TextInput.js +50 -0
  80. package/package.json +76 -0
  81. package/readme.md +147 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Renaming live tmux sessions after a prefix change. Sessions are named
3
+ * `<prefix>_<branch>`, so changing the prefix without renaming them leaves
4
+ * sessions that `wt remove` can no longer find.
5
+ *
6
+ * Every target is matched exactly. A session belonging to another project that
7
+ * merely starts with the same letters is never touched, and neither is one
8
+ * whose name matches the prefix but has no branch after it.
9
+ */
10
+ import { type CommandRunner } from './process.js';
11
+ export type SessionRename = Readonly<{
12
+ from: string;
13
+ to: string;
14
+ }>;
15
+ export type RenameOptions = Readonly<{
16
+ tmuxPath?: string;
17
+ run?: CommandRunner;
18
+ env?: NodeJS.ProcessEnv;
19
+ /** For tests: an isolated tmux server, as in `tmux -L trunk-test`. */
20
+ socketName?: string;
21
+ }>;
22
+ export type RenameResult = Readonly<{
23
+ renamed: readonly SessionRename[];
24
+ failed: readonly SessionRename[];
25
+ }>;
26
+ /**
27
+ * Which sessions a prefix change affects. Pure, so the list can be shown to the
28
+ * user before anything is renamed.
29
+ */
30
+ export declare function planRenames(sessionNames: readonly string[], oldPrefix: string, newPrefix: string): readonly SessionRename[];
31
+ /** The sessions tmux currently knows about, or none when tmux is not running. */
32
+ export declare function listSessions(options?: RenameOptions): Promise<readonly string[]>;
33
+ /** Renames each session, reporting which ones tmux refused. */
34
+ export declare function renameSessions(renames: readonly SessionRename[], options?: RenameOptions): Promise<RenameResult>;
35
+ /** What to run by hand when a rename failed and the session is stranded. */
36
+ export declare function manualKillCommands(failed: readonly SessionRename[]): readonly string[];
@@ -0,0 +1,79 @@
1
+ /* eslint-disable unicorn/filename-case -- Phase 5 specifies tmuxRename.ts. */
2
+ /**
3
+ * Renaming live tmux sessions after a prefix change. Sessions are named
4
+ * `<prefix>_<branch>`, so changing the prefix without renaming them leaves
5
+ * sessions that `wt remove` can no longer find.
6
+ *
7
+ * Every target is matched exactly. A session belonging to another project that
8
+ * merely starts with the same letters is never touched, and neither is one
9
+ * whose name matches the prefix but has no branch after it.
10
+ */
11
+ import { runCommand } from './process.js';
12
+ /**
13
+ * Which sessions a prefix change affects. Pure, so the list can be shown to the
14
+ * user before anything is renamed.
15
+ */
16
+ export function planRenames(sessionNames, oldPrefix, newPrefix) {
17
+ if (!oldPrefix || oldPrefix === newPrefix) {
18
+ return Object.freeze([]);
19
+ }
20
+ const start = `${oldPrefix}_`;
21
+ return Object.freeze(sessionNames
22
+ .filter(name => name.startsWith(start) && name.length > start.length)
23
+ .map(name => Object.freeze({
24
+ from: name,
25
+ to: `${newPrefix}_${name.slice(start.length)}`,
26
+ })));
27
+ }
28
+ /** The sessions tmux currently knows about, or none when tmux is not running. */
29
+ export async function listSessions(options = {}) {
30
+ const result = await run(options)(tmuxPath(options), [...socket(options), 'list-sessions', '-F', '#{session_name}'], { env: options.env });
31
+ if (result.code !== 0) {
32
+ return Object.freeze([]);
33
+ }
34
+ return Object.freeze(result.stdout
35
+ .split(/\r?\n/)
36
+ .map(line => line.trim())
37
+ .filter(Boolean));
38
+ }
39
+ /** Renames each session, reporting which ones tmux refused. */
40
+ export async function renameSessions(renames, options = {}) {
41
+ const renamed = [];
42
+ const failed = [];
43
+ for (const rename of renames) {
44
+ // Renames must not race each other on the same tmux server.
45
+ // eslint-disable-next-line no-await-in-loop
46
+ const result = await run(options)(tmuxPath(options), [
47
+ ...socket(options),
48
+ 'rename-session',
49
+ '-t',
50
+ // The `=` prefix is an exact match; without it tmux would accept a
51
+ // unique prefix and could rename the wrong session.
52
+ `=${rename.from}`,
53
+ rename.to,
54
+ ], { env: options.env });
55
+ if (result.code === 0) {
56
+ renamed.push(rename);
57
+ }
58
+ else {
59
+ failed.push(rename);
60
+ }
61
+ }
62
+ return Object.freeze({
63
+ renamed: Object.freeze(renamed),
64
+ failed: Object.freeze(failed),
65
+ });
66
+ }
67
+ /** What to run by hand when a rename failed and the session is stranded. */
68
+ export function manualKillCommands(failed) {
69
+ return Object.freeze(failed.map(rename => `tmux kill-session -t '=${rename.from}'`));
70
+ }
71
+ function socket(options) {
72
+ return options.socketName ? ['-L', options.socketName] : [];
73
+ }
74
+ function tmuxPath(options) {
75
+ return options.tmuxPath ?? 'tmux';
76
+ }
77
+ function run(options) {
78
+ return options.run ?? runCommand;
79
+ }
@@ -0,0 +1,22 @@
1
+ import { type CommandRunner } from './process.js';
2
+ import type { Settings } from './settings.js';
3
+ export type ValidationPreview = Readonly<{
4
+ session?: string;
5
+ port?: number;
6
+ url?: string;
7
+ }>;
8
+ export type ValidationResult = Readonly<{
9
+ preview: ValidationPreview;
10
+ configOutput: string;
11
+ hookOutput: string;
12
+ }>;
13
+ export type ValidationOptions = Readonly<{
14
+ wtPath?: string;
15
+ run?: CommandRunner;
16
+ env?: NodeJS.ProcessEnv;
17
+ }>;
18
+ export declare class GeneratedConfigError extends Error {
19
+ readonly output: string;
20
+ constructor(message: string, output: string);
21
+ }
22
+ export declare function validateGeneratedConfig(worktreePath: string, settings: Settings, options?: ValidationOptions): Promise<ValidationResult>;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Read-only validation of a generated project config through Worktrunk itself.
3
+ * The raw diagnostics stay attached to failures because they are usually the
4
+ * fastest way for a user to fix a local version or template mismatch.
5
+ */
6
+ import { expectedHooks } from './generate/index.js';
7
+ import { runCommand } from './process.js';
8
+ export class GeneratedConfigError extends Error {
9
+ output;
10
+ constructor(message, output) {
11
+ super(`${message}${output.trim() ? `\n${output.trim()}` : ''}`);
12
+ this.name = 'GeneratedConfigError';
13
+ this.output = output;
14
+ }
15
+ }
16
+ export async function validateGeneratedConfig(worktreePath, settings, options = {}) {
17
+ const run = options.run ?? runCommand;
18
+ const wtPath = options.wtPath ?? 'wt';
19
+ const commandOptions = { env: options.env };
20
+ const config = await run(wtPath, ['-C', worktreePath, 'config', 'show'], commandOptions);
21
+ const configOutput = joinOutput(config.stdout, config.stderr);
22
+ if (config.code !== 0 || hasDiagnostic(configOutput)) {
23
+ throw new GeneratedConfigError('Worktrunk rejected the generated config.', configOutput);
24
+ }
25
+ const hooks = await run(wtPath, ['-C', worktreePath, 'hook', 'show', '--expanded', '--format', 'json'], commandOptions);
26
+ const hookOutput = joinOutput(hooks.stdout, hooks.stderr);
27
+ if (hooks.code !== 0 || hasDiagnostic(hookOutput)) {
28
+ throw new GeneratedConfigError('Worktrunk could not expand the generated hooks.', hookOutput);
29
+ }
30
+ const records = parseExpandedHooks(hooks.stdout, hookOutput);
31
+ assertExpectedHooks(records, settings, hookOutput);
32
+ return Object.freeze({
33
+ preview: Object.freeze(extractPreview(records)),
34
+ configOutput,
35
+ hookOutput,
36
+ });
37
+ }
38
+ function parseExpandedHooks(stdout, rawOutput) {
39
+ let value;
40
+ try {
41
+ value = JSON.parse(stdout);
42
+ }
43
+ catch {
44
+ throw new GeneratedConfigError('Worktrunk returned invalid hook JSON.', rawOutput);
45
+ }
46
+ if (!Array.isArray(value) || !value.every(entry => isExpandedHook(entry))) {
47
+ throw new GeneratedConfigError('Worktrunk returned unexpected hook data.', rawOutput);
48
+ }
49
+ return value;
50
+ }
51
+ function isExpandedHook(value) {
52
+ return (typeof value === 'object' &&
53
+ value !== null &&
54
+ 'expanded' in value &&
55
+ typeof value.expanded === 'string' &&
56
+ 'name' in value &&
57
+ typeof value.name === 'string' &&
58
+ 'source' in value &&
59
+ typeof value.source === 'string' &&
60
+ 'type' in value &&
61
+ typeof value.type === 'string');
62
+ }
63
+ function assertExpectedHooks(records, settings, rawOutput) {
64
+ const configured = new Set(records
65
+ .filter(record => record.source === 'project')
66
+ .map(record => `${record.type}:${record.name}`));
67
+ const missing = expectedHooks(settings)
68
+ .map(hook => `${hook.type}:${hook.name}`)
69
+ .filter(hook => !configured.has(hook));
70
+ if (missing.length > 0) {
71
+ throw new GeneratedConfigError(`Worktrunk omitted generated hooks: ${missing.join(', ')}.`, rawOutput);
72
+ }
73
+ }
74
+ function extractPreview(records) {
75
+ const tmux = findHook(records, 'pre-start', 'tmux');
76
+ const server = findHook(records, 'post-start', 'server');
77
+ const proxy = findHook(records, 'post-start', 'proxy');
78
+ const prefix = tmux ? assignment(tmux, 'P') : undefined;
79
+ const branch = tmux ? assignment(tmux, 'B') : undefined;
80
+ const port = server ? /--port\s+(\d+)/.exec(server)?.[1] : undefined;
81
+ const host = proxy ? assignment(proxy, 'HOST') : undefined;
82
+ return {
83
+ session: prefix && branch ? `${prefix}_${branch}` : undefined,
84
+ port: port ? Number(port) : undefined,
85
+ url: host ? `http://${host}:8080` : undefined,
86
+ };
87
+ }
88
+ function findHook(records, type, name) {
89
+ return records.find(record => record.source === 'project' &&
90
+ record.type === type &&
91
+ record.name === name)?.expanded;
92
+ }
93
+ function assignment(body, name) {
94
+ const match = new RegExp(`^${name}=(.+)$`, 'm').exec(body);
95
+ return match ? unquote(match[1].trim()) : undefined;
96
+ }
97
+ function unquote(value) {
98
+ if ((value.startsWith("'") && value.endsWith("'")) ||
99
+ (value.startsWith('"') && value.endsWith('"'))) {
100
+ return value.slice(1, -1);
101
+ }
102
+ return value;
103
+ }
104
+ function hasDiagnostic(output) {
105
+ return output
106
+ .split(/\r?\n/)
107
+ .some(line => /^\s*[▲⚠✗]\s+.*(?:unknown field|invalid|error|warning|failed|rejected)/i.test(line) || /^\s*(?:warning|error)(?:\b|:)/i.test(line));
108
+ }
109
+ function joinOutput(stdout, stderr) {
110
+ return [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join('\n');
111
+ }
@@ -0,0 +1,2 @@
1
+ /** The package version, or `0.0.0` when package.json cannot be read. */
2
+ export declare function trunkVersion(): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Trunk's own version, for the header of every generated config.
3
+ *
4
+ * Read from package.json rather than generated into the source: npm always
5
+ * includes package.json in the published tarball, so it is there at run time,
6
+ * and a generated file would have to be either committed and kept in sync or
7
+ * gitignored — and a gitignored file is invisible to type-aware linting, which
8
+ * is exactly how this broke before.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
12
+ let cached;
13
+ /** The package version, or `0.0.0` when package.json cannot be read. */
14
+ export function trunkVersion() {
15
+ cached ??= readVersion();
16
+ return cached;
17
+ }
18
+ function readVersion() {
19
+ // `source/core/` and the built `dist/core/` both sit two levels below the
20
+ // package root, so one path serves the tests and the published CLI.
21
+ const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url));
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
24
+ if (typeof parsed === 'object' &&
25
+ parsed !== null &&
26
+ 'version' in parsed &&
27
+ typeof parsed.version === 'string') {
28
+ return parsed.version;
29
+ }
30
+ }
31
+ catch {
32
+ // Falls through to the placeholder below.
33
+ }
34
+ return '0.0.0';
35
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A word list for suggesting a prefix when a repository name gives a poor one.
3
+ * The words are short, concrete and unambiguous to type.
4
+ */
5
+ /**
6
+ * Words that would be confusing as a session prefix because they already name a
7
+ * command or a branch. Kept out of the list below, and asserted in tests.
8
+ */
9
+ export declare const reservedWords: readonly string[];
10
+ /** Three to seven lowercase letters each: no digits, hyphens or ambiguity. */
11
+ export declare const words: readonly string[];
12
+ /**
13
+ * Picks a word, skipping any already in use. The random source is a parameter so
14
+ * tests can seed it and get the same word every run.
15
+ */
16
+ export declare function randomWord(exclude?: ReadonlySet<string>, random?: () => number): string;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * A word list for suggesting a prefix when a repository name gives a poor one.
3
+ * The words are short, concrete and unambiguous to type.
4
+ */
5
+ /**
6
+ * Words that would be confusing as a session prefix because they already name a
7
+ * command or a branch. Kept out of the list below, and asserted in tests.
8
+ */
9
+ export const reservedWords = Object.freeze([
10
+ 'bash',
11
+ 'brew',
12
+ 'bun',
13
+ 'caddy',
14
+ 'cargo',
15
+ 'code',
16
+ 'deno',
17
+ 'dev',
18
+ 'docker',
19
+ 'fish',
20
+ 'gh',
21
+ 'git',
22
+ 'java',
23
+ 'just',
24
+ 'less',
25
+ 'main',
26
+ 'make',
27
+ 'more',
28
+ 'nano',
29
+ 'next',
30
+ 'node',
31
+ 'npm',
32
+ 'perl',
33
+ 'pnpm',
34
+ 'python',
35
+ 'ruby',
36
+ 'rustc',
37
+ 'shell',
38
+ 'task',
39
+ 'tmux',
40
+ 'vim',
41
+ 'wt',
42
+ 'yarn',
43
+ 'zsh',
44
+ ]);
45
+ /** Three to seven lowercase letters each: no digits, hyphens or ambiguity. */
46
+ export const words = Object.freeze([
47
+ 'acorn',
48
+ 'anchor',
49
+ 'anvil',
50
+ 'apple',
51
+ 'apron',
52
+ 'badger',
53
+ 'bamboo',
54
+ 'barrel',
55
+ 'beacon',
56
+ 'beaver',
57
+ 'birch',
58
+ 'bison',
59
+ 'blade',
60
+ 'bloom',
61
+ 'board',
62
+ 'brook',
63
+ 'brush',
64
+ 'cabin',
65
+ 'cactus',
66
+ 'candle',
67
+ 'canoe',
68
+ 'cedar',
69
+ 'cello',
70
+ 'chalk',
71
+ 'cherry',
72
+ 'cliff',
73
+ 'cloud',
74
+ 'clover',
75
+ 'coral',
76
+ 'crane',
77
+ 'creek',
78
+ 'crown',
79
+ 'crystal',
80
+ 'daisy',
81
+ 'delta',
82
+ 'dune',
83
+ 'eagle',
84
+ 'earth',
85
+ 'ember',
86
+ 'falcon',
87
+ 'fern',
88
+ 'field',
89
+ 'finch',
90
+ 'flame',
91
+ 'flint',
92
+ 'forest',
93
+ 'fox',
94
+ 'frost',
95
+ 'garden',
96
+ 'gecko',
97
+ 'globe',
98
+ 'grove',
99
+ 'harbor',
100
+ 'hazel',
101
+ 'heron',
102
+ 'hill',
103
+ 'honey',
104
+ 'horse',
105
+ 'iris',
106
+ 'island',
107
+ 'ivory',
108
+ 'jade',
109
+ 'kettle',
110
+ 'koala',
111
+ 'lake',
112
+ 'lantern',
113
+ 'laurel',
114
+ 'lemon',
115
+ 'lilac',
116
+ 'linen',
117
+ 'lotus',
118
+ 'maple',
119
+ 'marsh',
120
+ 'meadow',
121
+ 'melon',
122
+ 'mint',
123
+ 'moose',
124
+ 'moss',
125
+ 'moth',
126
+ 'ocean',
127
+ 'olive',
128
+ 'onion',
129
+ 'orchid',
130
+ 'otter',
131
+ 'owl',
132
+ 'panda',
133
+ 'pearl',
134
+ 'pebble',
135
+ 'pine',
136
+ 'plum',
137
+ 'pond',
138
+ 'poppy',
139
+ 'quartz',
140
+ 'raven',
141
+ 'reed',
142
+ 'reef',
143
+ 'ridge',
144
+ 'robin',
145
+ 'rose',
146
+ 'sable',
147
+ 'sage',
148
+ 'sail',
149
+ 'salmon',
150
+ 'sand',
151
+ 'seal',
152
+ 'shark',
153
+ 'slate',
154
+ 'snail',
155
+ 'snow',
156
+ 'sparrow',
157
+ 'spruce',
158
+ 'star',
159
+ 'stone',
160
+ 'storm',
161
+ 'summit',
162
+ 'swan',
163
+ 'thyme',
164
+ 'tiger',
165
+ 'topaz',
166
+ 'torch',
167
+ 'trout',
168
+ 'tulip',
169
+ 'turtle',
170
+ 'vale',
171
+ 'valley',
172
+ 'velvet',
173
+ 'vine',
174
+ 'violet',
175
+ 'walnut',
176
+ 'wave',
177
+ 'whale',
178
+ 'willow',
179
+ 'wolf',
180
+ 'wren',
181
+ 'yarrow',
182
+ 'zebra',
183
+ ]);
184
+ /**
185
+ * Picks a word, skipping any already in use. The random source is a parameter so
186
+ * tests can seed it and get the same word every run.
187
+ */
188
+ export function randomWord(exclude = new Set(), random = Math.random) {
189
+ const candidates = words.filter(word => !exclude.has(word));
190
+ if (candidates.length === 0) {
191
+ throw new RangeError('No words remain after applying exclusions.');
192
+ }
193
+ const value = random();
194
+ if (value < 0 || value >= 1 || !Number.isFinite(value)) {
195
+ throw new RangeError('The random source must return a number from 0 up to 1.');
196
+ }
197
+ return candidates[Math.floor(value * candidates.length)];
198
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Worktrunk wrappers. Two kinds live here: captured calls, whose output trunk
3
+ * reads, and attached calls, which borrow trunk's terminal so wt can ask its
4
+ * own questions.
5
+ *
6
+ * trunk never writes to the user's worktrunk config. When wt needs to know
7
+ * something, such as where worktrees belong for a project, wt asks for it
8
+ * itself through an attached call.
9
+ */
10
+ import { type AttachedRunner, type CommandResult, type CommandRunner } from './process.js';
11
+ export type WtOptions = Readonly<{
12
+ wtPath?: string;
13
+ run?: CommandRunner;
14
+ attach?: AttachedRunner;
15
+ env?: NodeJS.ProcessEnv;
16
+ }>;
17
+ export type HookType = 'pre-start' | 'post-start';
18
+ /**
19
+ * The first `wt switch` in a project, with the terminal attached so wt's own
20
+ * worktree-path question reaches the user.
21
+ */
22
+ export declare function switchAttached(projectDirectory: string, branch: string, options?: WtOptions): Promise<number>;
23
+ /**
24
+ * Creates the setup worktree. `--no-hooks` is mandatory: the config being
25
+ * written has not been approved yet, and the hooks would otherwise run against
26
+ * a half-configured repository.
27
+ */
28
+ export declare function switchCreateAttached(projectDirectory: string, branch: string, options?: WtOptions): Promise<number>;
29
+ /** Undoes {@link switchCreateAttached}; also the rollback path's first step. */
30
+ export declare function remove(projectDirectory: string, branch: string, options?: WtOptions): Promise<CommandResult>;
31
+ /** Interactive by design: the user is approving commands that will run. */
32
+ export declare function approvalsAdd(worktreePath: string, options?: WtOptions): Promise<number>;
33
+ /**
34
+ * Runs one hook as the smoke test. Attached, because post-start starts a real
35
+ * dev server and prints where it is listening.
36
+ */
37
+ export declare function runHook(worktreePath: string, hook: HookType, options?: WtOptions): Promise<number>;
38
+ /** Where wt writes a failed hook's output, for the message after a failure. */
39
+ export declare function hookLogPath(projectDirectory: string, branch: string, hook: HookType): string;
40
+ /** Worktrunk's own branch-to-path rule, used for log paths and warnings. */
41
+ export declare function sanitizeBranch(branch: string): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Worktrunk wrappers. Two kinds live here: captured calls, whose output trunk
3
+ * reads, and attached calls, which borrow trunk's terminal so wt can ask its
4
+ * own questions.
5
+ *
6
+ * trunk never writes to the user's worktrunk config. When wt needs to know
7
+ * something, such as where worktrees belong for a project, wt asks for it
8
+ * itself through an attached call.
9
+ */
10
+ import { runAttached, runCommand, } from './process.js';
11
+ /**
12
+ * The first `wt switch` in a project, with the terminal attached so wt's own
13
+ * worktree-path question reaches the user.
14
+ */
15
+ export async function switchAttached(projectDirectory, branch, options = {}) {
16
+ return attach(options)(wtPath(options), ['-C', projectDirectory, 'switch', branch], { env: options.env });
17
+ }
18
+ /**
19
+ * Creates the setup worktree. `--no-hooks` is mandatory: the config being
20
+ * written has not been approved yet, and the hooks would otherwise run against
21
+ * a half-configured repository.
22
+ */
23
+ export async function switchCreateAttached(projectDirectory, branch, options = {}) {
24
+ return attach(options)(wtPath(options), ['-C', projectDirectory, 'switch', '--create', branch, '--no-hooks'], { env: options.env });
25
+ }
26
+ /** Undoes {@link switchCreateAttached}; also the rollback path's first step. */
27
+ export async function remove(projectDirectory, branch, options = {}) {
28
+ return run(options)(wtPath(options), ['-C', projectDirectory, 'remove', branch, '--no-hooks', '--yes'], { env: options.env });
29
+ }
30
+ /** Interactive by design: the user is approving commands that will run. */
31
+ export async function approvalsAdd(worktreePath, options = {}) {
32
+ return attach(options)(wtPath(options), ['-C', worktreePath, 'config', 'approvals', 'add'], { env: options.env });
33
+ }
34
+ /**
35
+ * Runs one hook as the smoke test. Attached, because post-start starts a real
36
+ * dev server and prints where it is listening.
37
+ */
38
+ export async function runHook(worktreePath, hook, options = {}) {
39
+ return attach(options)(wtPath(options), ['-C', worktreePath, 'hook', hook], {
40
+ env: options.env,
41
+ });
42
+ }
43
+ /** Where wt writes a failed hook's output, for the message after a failure. */
44
+ export function hookLogPath(projectDirectory, branch, hook) {
45
+ return `${projectDirectory}/.git/wt/logs/${sanitizeBranch(branch)}/project/${hook}/`;
46
+ }
47
+ /** Worktrunk's own branch-to-path rule, used for log paths and warnings. */
48
+ export function sanitizeBranch(branch) {
49
+ return branch.replaceAll('/', '-');
50
+ }
51
+ function wtPath(options) {
52
+ return options.wtPath ?? 'wt';
53
+ }
54
+ function run(options) {
55
+ return options.run ?? runCommand;
56
+ }
57
+ function attach(options) {
58
+ return options.attach ?? runAttached;
59
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { resolve, type ResolveOptions } from '../core/resolve.js';
3
+ import { type Outcome } from '../core/result.js';
4
+ export type SetupFormProperties = Readonly<{
5
+ folder: string;
6
+ options: ResolveOptions;
7
+ randomPrefix?: () => string;
8
+ onSubmit: (settings: SetupValuesResult) => void;
9
+ onAbort: () => void;
10
+ }>;
11
+ type SetupValuesResult = Extract<ReturnType<typeof resolve>, {
12
+ kind: 'complete';
13
+ }>['settings'];
14
+ export type SetupFormOutcome = Readonly<{
15
+ kind: 'settings';
16
+ settings: SetupValuesResult;
17
+ }> | Readonly<{
18
+ kind: 'aborted';
19
+ }>
20
+ /** The terminal could not run the form, so the caller falls back to flags. */
21
+ | Readonly<{
22
+ kind: 'unavailable';
23
+ reason: string;
24
+ }>;
25
+ export type CollectSettingsResult = Readonly<{
26
+ kind: 'settings';
27
+ settings: SetupValuesResult;
28
+ }> | Readonly<{
29
+ kind: 'outcome';
30
+ outcome: Outcome;
31
+ }>;
32
+ export type CollectSettingsOptions = Readonly<{
33
+ folder: string;
34
+ resolveOptions: ResolveOptions;
35
+ invocation: Readonly<{
36
+ executable: string;
37
+ arguments: readonly string[];
38
+ }>;
39
+ yes?: boolean;
40
+ interactive?: boolean;
41
+ randomPrefix?: () => string;
42
+ installCaddy?: (executable: string, arguments_: readonly string[]) => Promise<boolean>;
43
+ report?: (line: string) => void;
44
+ /** Injectable so the abort and submit paths are testable without a TTY. */
45
+ runForm?: (properties: Omit<SetupFormProperties, 'onSubmit' | 'onAbort'>) => Promise<SetupFormOutcome>;
46
+ /** Injectable for the same reason as {@link CollectSettingsOptions.runForm}. */
47
+ askInstall?: () => Promise<InstallAnswer>;
48
+ }>;
49
+ export default function SetupForm({ folder, options, randomPrefix, onSubmit, onAbort, }: SetupFormProperties): React.ReactElement;
50
+ /** Mounts Ink with Ctrl+C routed through the form's normal abort result. */
51
+ export declare function runSetupForm(properties: Omit<SetupFormProperties, 'onSubmit' | 'onAbort'>): Promise<SetupFormOutcome>;
52
+ /** Chooses the headless path or mounts the form, including the Caddy branch. */
53
+ export declare function collectSettings({ folder, resolveOptions, invocation, yes, interactive, randomPrefix, installCaddy, report, runForm, askInstall, }: CollectSettingsOptions): Promise<CollectSettingsResult>;
54
+ export type InstallAnswer = 'yes' | 'no' | 'abort';
55
+ export {};