@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.9

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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * `pugi worktree <op>` — α7.7 Phase 1.
3
+ *
4
+ * Manual control over the scratch worktree primitive. Three subcommands:
5
+ *
6
+ * pugi worktree create [branch] # spawns `.pugi/worktrees/<uuid>`
7
+ * pugi worktree promote <path> # applies the worktree's diff back to cwd
8
+ * pugi worktree drop <path> # removes the worktree (idempotent)
9
+ *
10
+ * Output: human-readable by default, structured JSON under --json so
11
+ * scripted callers can chain (`pugi worktree create --json | jq .path`).
12
+ *
13
+ * The same primitives are used by the `pugi build` and
14
+ * `pugi review --consensus` paths internally; this surface is the
15
+ * operator escape hatch for debugging / manual experimentation.
16
+ *
17
+ * Brand voice: ASCII only, no emoji, no banned words.
18
+ */
19
+ import { spawnSync } from 'node:child_process';
20
+ import { resolve, sep } from 'node:path';
21
+ import { createWorktree, dropWorktree, promoteWorktree } from '../../core/edits/worktree.js';
22
+ /**
23
+ * R1 fix (2026-05-26, PR #413 r1, P2 #10): operator-facing path
24
+ * validation. The core `promoteWorktree` / `dropWorktree` primitives
25
+ * already gate their inputs, but mirroring the check at the CLI surface
26
+ * gives the operator a clean error message before we even attempt the
27
+ * git invocation (which would otherwise leak `git rev-parse HEAD` stderr).
28
+ */
29
+ function isUnderScratchRoot(cwd, candidate) {
30
+ const scratchRoot = resolve(cwd, '.pugi', 'worktrees');
31
+ const abs = resolve(cwd, candidate);
32
+ return abs.startsWith(scratchRoot + sep) && abs !== scratchRoot;
33
+ }
34
+ export async function runWorktreeCommand(args, opts) {
35
+ const [op, ...rest] = args;
36
+ if (!op)
37
+ return usage();
38
+ if (op === 'create') {
39
+ const branch = rest[0];
40
+ const result = createWorktree({
41
+ cwd: opts.cwd,
42
+ ...(branch ? { branch } : {}),
43
+ });
44
+ if (!result.ok) {
45
+ return {
46
+ ok: false,
47
+ text: opts.json
48
+ ? JSON.stringify(result, null, 2)
49
+ : `worktree create failed: ${result.reason}: ${result.detail}`,
50
+ exitCode: 1,
51
+ };
52
+ }
53
+ const handle = result.value;
54
+ return {
55
+ ok: true,
56
+ text: opts.json
57
+ ? JSON.stringify({ path: handle.path, baseSha: handle.baseSha }, null, 2)
58
+ : `worktree created: ${handle.path}\nbase: ${handle.baseSha}`,
59
+ exitCode: 0,
60
+ };
61
+ }
62
+ if (op === 'promote') {
63
+ const worktreePath = rest[0];
64
+ if (!worktreePath) {
65
+ return {
66
+ ok: false,
67
+ text: 'Usage: pugi worktree promote <path>',
68
+ exitCode: 2,
69
+ };
70
+ }
71
+ if (!isUnderScratchRoot(opts.cwd, worktreePath)) {
72
+ return {
73
+ ok: false,
74
+ text: opts.json
75
+ ? JSON.stringify({
76
+ ok: false,
77
+ reason: 'invalid_worktree_path',
78
+ detail: `worktree path must live under <cwd>/.pugi/worktrees/`,
79
+ }, null, 2)
80
+ : `promote failed: invalid_worktree_path: ${worktreePath} is not under .pugi/worktrees/`,
81
+ exitCode: 3,
82
+ };
83
+ }
84
+ // Resolve the worktree path's base SHA from its own git HEAD so
85
+ // the operator never has to remember it after `worktree create`.
86
+ const abs = resolve(opts.cwd, worktreePath);
87
+ const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: abs, encoding: 'utf8' });
88
+ if (head.status !== 0) {
89
+ return {
90
+ ok: false,
91
+ text: `cannot read HEAD of ${abs}: ${head.stderr.trim() || 'git rev-parse failed'}`,
92
+ exitCode: 1,
93
+ };
94
+ }
95
+ const baseSha = head.stdout.trim();
96
+ const result = promoteWorktree({
97
+ cwd: opts.cwd,
98
+ worktreePath: abs,
99
+ baseSha,
100
+ ...(opts.dryRun ? { dryRun: true } : {}),
101
+ });
102
+ if (!result.ok) {
103
+ return {
104
+ ok: false,
105
+ text: opts.json
106
+ ? JSON.stringify(result, null, 2)
107
+ : `promote failed: ${result.reason}: ${result.detail}`,
108
+ exitCode: result.reason === 'protected_file_in_worktree' ? 3 : 1,
109
+ };
110
+ }
111
+ const prefix = opts.dryRun ? 'dry-run: would promote' : 'promoted';
112
+ return {
113
+ ok: true,
114
+ text: opts.json
115
+ ? JSON.stringify({ filesChanged: result.value.filesChanged, dryRun: opts.dryRun ?? false }, null, 2)
116
+ : `${prefix} ${result.value.filesChanged} files from ${abs}`,
117
+ exitCode: 0,
118
+ };
119
+ }
120
+ if (op === 'drop') {
121
+ const worktreePath = rest[0];
122
+ if (!worktreePath) {
123
+ return {
124
+ ok: false,
125
+ text: 'Usage: pugi worktree drop <path>',
126
+ exitCode: 2,
127
+ };
128
+ }
129
+ if (!isUnderScratchRoot(opts.cwd, worktreePath)) {
130
+ return {
131
+ ok: false,
132
+ text: opts.json
133
+ ? JSON.stringify({
134
+ ok: false,
135
+ reason: 'invalid_worktree_path',
136
+ detail: `worktree path must live under <cwd>/.pugi/worktrees/`,
137
+ }, null, 2)
138
+ : `drop failed: invalid_worktree_path: ${worktreePath} is not under .pugi/worktrees/`,
139
+ exitCode: 3,
140
+ };
141
+ }
142
+ const abs = resolve(opts.cwd, worktreePath);
143
+ const result = dropWorktree(abs, opts.cwd);
144
+ if (!result.ok) {
145
+ return {
146
+ ok: false,
147
+ text: opts.json
148
+ ? JSON.stringify(result, null, 2)
149
+ : `drop failed: ${result.reason}: ${result.detail}`,
150
+ exitCode: result.reason === 'invalid_worktree_path' ? 3 : 1,
151
+ };
152
+ }
153
+ return {
154
+ ok: true,
155
+ text: opts.json
156
+ ? JSON.stringify({ dropped: abs }, null, 2)
157
+ : `worktree dropped: ${abs}`,
158
+ exitCode: 0,
159
+ };
160
+ }
161
+ return {
162
+ ok: false,
163
+ text: `unknown worktree operation: ${op}. Supported: create, promote, drop`,
164
+ exitCode: 2,
165
+ };
166
+ }
167
+ function usage() {
168
+ return {
169
+ ok: false,
170
+ text: 'Usage: pugi worktree <op>\n' +
171
+ ' pugi worktree create [branch]\n' +
172
+ ' pugi worktree promote <path>\n' +
173
+ ' pugi worktree drop <path>',
174
+ exitCode: 2,
175
+ };
176
+ }
177
+ //# sourceMappingURL=worktree.js.map