@pugi/cli 0.1.0-beta.88 → 0.1.0-beta.89
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/dist/core/auth/env-provider.js +1 -1
- package/dist/core/context/markdown-traverse.js +1 -1
- package/dist/core/credentials.js +1 -1
- package/dist/core/engine/anvil-client.js +63 -0
- package/dist/core/engine/native-pugi.js +1 -1
- package/dist/core/engine/tool-bridge.js +436 -0
- package/dist/core/hooks/events.js +3 -1
- package/dist/core/hooks/registry.js +3 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/lsp/client.js +453 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +296 -4
- package/dist/core/mcp/server.js +1 -1
- package/dist/core/repl/ask.js +1 -1
- package/dist/core/repl/session.js +3 -3
- package/dist/core/repl/slash-commands.js +1 -1
- package/dist/core/settings.js +26 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/runtime/cli.js +108 -8
- package/dist/runtime/commands/agents.js +1 -1
- package/dist/runtime/commands/hooks.js +3 -0
- package/dist/runtime/commands/review-consensus.js +1 -1
- package/dist/runtime/version.js +1 -1
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/registry.js +23 -0
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/render.js +1 -1
- package/dist/tui/repl.js +1 -1
- package/dist/tui/status-bar.js +1 -1
- package/dist/tui/update-banner.js +1 -1
- package/package.json +3 -3
- package/test/scenarios/compact-force.scenario.txt +3 -2
- package/test/scenarios/identity.scenario.txt +6 -5
- package/test/scenarios/persona-handoff.scenario.txt +2 -1
- package/test/scenarios/walkback.scenario.txt +6 -6
|
@@ -2019,7 +2019,7 @@ export class ReplSession {
|
|
|
2019
2019
|
// Surface the operator's choice as a transcript row so the
|
|
2020
2020
|
// conversation reads linearly. The label of the chosen option
|
|
2021
2021
|
// (or the literal custom input) is more readable than the bare
|
|
2022
|
-
// value -
|
|
2022
|
+
// value - peer CLI's "you chose: Vercel" pattern.
|
|
2023
2023
|
const humanLabel = humanLabelForVerdict(tag, sanitisedVerdict);
|
|
2024
2024
|
this.appendOperatorLine(humanLabel);
|
|
2025
2025
|
// Local-origin modals (operator typed `/ask`) never need an
|
|
@@ -4105,7 +4105,7 @@ export function colorizeQuotaRow(row, pct) {
|
|
|
4105
4105
|
* string and emit a synthesised `ToolCallEntry`. Returns null when no
|
|
4106
4106
|
* known tool pattern matches.
|
|
4107
4107
|
*
|
|
4108
|
-
* The grammar mirrors the way the upstream tool,
|
|
4108
|
+
* The grammar mirrors the way the upstream tool, peer CLI, and Gemini CLI
|
|
4109
4109
|
* display tool calls in their tool stream panes:
|
|
4110
4110
|
*
|
|
4111
4111
|
* Read(path)
|
|
@@ -4293,7 +4293,7 @@ function encodePlanReviewVerdictLocal(result) {
|
|
|
4293
4293
|
}
|
|
4294
4294
|
/**
|
|
4295
4295
|
* Compose the human-readable transcript line that records the
|
|
4296
|
-
* operator's ask verdict. Mirrors
|
|
4296
|
+
* operator's ask verdict. Mirrors peer CLI's "you chose: <label>"
|
|
4297
4297
|
* pattern so the conversation reads linearly.
|
|
4298
4298
|
*/
|
|
4299
4299
|
function humanLabelForVerdict(tag, verdict) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The REPL input box surfaces a palette of slash commands the operator
|
|
5
5
|
* can run from inside a persistent session. The wave-2 expansion (CEO
|
|
6
6
|
*) grows the surface from 6 to 20 commands so the `/help`
|
|
7
|
-
* overlay matches the breadth the upstream tool /
|
|
7
|
+
* overlay matches the breadth the upstream tool / peer CLI operators expect.
|
|
8
8
|
*
|
|
9
9
|
* The registry is pure: each `parseSlashCommand` call returns a
|
|
10
10
|
* `SlashCommandResult` describing what the REPL session should do next.
|
package/dist/core/settings.js
CHANGED
|
@@ -177,6 +177,32 @@ const pugiSettingsSchema = z.object({
|
|
|
177
177
|
tier: z.enum(['1m', 'standard']).optional(),
|
|
178
178
|
})
|
|
179
179
|
.optional(),
|
|
180
|
+
// PUGI-487 - `pugi --worktree` flag governance.
|
|
181
|
+
//
|
|
182
|
+
// Two knobs control the user-facing --worktree flag introduced for
|
|
183
|
+
// parity with parallel-agent isolation patterns in other coding
|
|
184
|
+
// CLIs:
|
|
185
|
+
//
|
|
186
|
+
// - `baseRef`: which ref the new worktree branches FROM.
|
|
187
|
+
// `'fresh'` (default) resolves origin/<default-branch> so each
|
|
188
|
+
// parallel session starts from a clean trunk.
|
|
189
|
+
// `'head'` carries the operator's current local HEAD (including
|
|
190
|
+
// unpushed work) into the new tree.
|
|
191
|
+
//
|
|
192
|
+
// - `cleanupPeriodDays`: integer days. The daily sweep removes
|
|
193
|
+
// user-facing worktrees older than N days that have no
|
|
194
|
+
// uncommitted, untracked, or unpushed state. Default 7 mirrors
|
|
195
|
+
// a one-work-week window. Set to 0 to disable auto-cleanup.
|
|
196
|
+
//
|
|
197
|
+
// Both knobs are optional - the consumers (`bootstrapWorktree`,
|
|
198
|
+
// `runUserWorktreeCleanup`) carry their own defaults so a missing
|
|
199
|
+
// section produces standard behaviour.
|
|
200
|
+
worktree: z
|
|
201
|
+
.object({
|
|
202
|
+
baseRef: z.enum(['fresh', 'head']).optional(),
|
|
203
|
+
cleanupPeriodDays: z.number().int().min(0).max(365).optional(),
|
|
204
|
+
})
|
|
205
|
+
.optional(),
|
|
180
206
|
});
|
|
181
207
|
/**
|
|
182
208
|
* #20 — the upstream tool drop-in compat ingest.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.worktreeinclude` parser - PUGI-487.
|
|
3
|
+
*
|
|
4
|
+
* The `.worktreeinclude` file lives at the repository root and carries
|
|
5
|
+
* gitignore-syntax patterns. Files that:
|
|
6
|
+
*
|
|
7
|
+
* 1. Match at least one pattern in `.worktreeinclude`, AND
|
|
8
|
+
* 2. Are git-ignored (untracked-and-ignored OR explicitly ignored),
|
|
9
|
+
*
|
|
10
|
+
* get COPIED into a fresh `pugi --worktree <name>` tree right after
|
|
11
|
+
* `git worktree add` finishes. Tracked files are NEVER copied because
|
|
12
|
+
* git already brings them along on its own, and double-writing would
|
|
13
|
+
* corrupt the worktree index.
|
|
14
|
+
*
|
|
15
|
+
* Why a separate include file instead of `.gitignore` negation
|
|
16
|
+
* patterns?
|
|
17
|
+
*
|
|
18
|
+
* - The common case is "I have an untracked `.env` with API keys that
|
|
19
|
+
* every parallel session needs". Listing it in `.gitignore` with
|
|
20
|
+
* a negation would force-add it to the repo, defeating the point.
|
|
21
|
+
* - Operators want a single-file, copy-pasteable manifest of WHAT
|
|
22
|
+
* leaves the main tree, not a derived view across `.gitignore`
|
|
23
|
+
* semantics.
|
|
24
|
+
*
|
|
25
|
+
* Pattern grammar (subset of gitignore):
|
|
26
|
+
*
|
|
27
|
+
* - Lines starting with `#` are comments. Blank lines are skipped.
|
|
28
|
+
* - Trailing `/` means directory-only and applies recursively to
|
|
29
|
+
* every file under the directory.
|
|
30
|
+
* - Leading `!` is a negation pattern that REMOVES matches from
|
|
31
|
+
* prior includes. Multiple negations are processed in order.
|
|
32
|
+
* - `*` matches any character except `/`.
|
|
33
|
+
* - `**` matches any number of path segments.
|
|
34
|
+
* - `?` matches a single non-`/` character.
|
|
35
|
+
* - Patterns with NO `/` match anywhere in the tree (e.g. `.env`
|
|
36
|
+
* matches both `./.env` and `./apps/foo/.env`).
|
|
37
|
+
* - Patterns starting with `/` are anchored to the repository root.
|
|
38
|
+
*
|
|
39
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
40
|
+
*/
|
|
41
|
+
import { spawnSync } from 'node:child_process';
|
|
42
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
43
|
+
import { resolve, sep, posix } from 'node:path';
|
|
44
|
+
/** Default file name. */
|
|
45
|
+
export const WORKTREE_INCLUDE_FILENAME = '.worktreeinclude';
|
|
46
|
+
/**
|
|
47
|
+
* Parse the textual contents of a `.worktreeinclude` file into a rule
|
|
48
|
+
* list. Pure function - does not touch the filesystem. Exposed for
|
|
49
|
+
* spec coverage and the cleanup/dry-run path.
|
|
50
|
+
*/
|
|
51
|
+
export function parseWorktreeIncludeText(text, source = WORKTREE_INCLUDE_FILENAME) {
|
|
52
|
+
const rules = [];
|
|
53
|
+
const lines = text.split(/\r?\n/);
|
|
54
|
+
for (const raw of lines) {
|
|
55
|
+
const line = raw.trim();
|
|
56
|
+
if (line.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
if (line.startsWith('#'))
|
|
59
|
+
continue;
|
|
60
|
+
let body = line;
|
|
61
|
+
let negate = false;
|
|
62
|
+
if (body.startsWith('!')) {
|
|
63
|
+
negate = true;
|
|
64
|
+
body = body.slice(1);
|
|
65
|
+
}
|
|
66
|
+
if (body.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
let directoryOnly = false;
|
|
69
|
+
if (body.endsWith('/')) {
|
|
70
|
+
directoryOnly = true;
|
|
71
|
+
body = body.slice(0, -1);
|
|
72
|
+
}
|
|
73
|
+
let anchored = false;
|
|
74
|
+
if (body.startsWith('/')) {
|
|
75
|
+
anchored = true;
|
|
76
|
+
body = body.slice(1);
|
|
77
|
+
}
|
|
78
|
+
else if (body.includes('/')) {
|
|
79
|
+
// a non-leading slash also anchors per gitignore semantics
|
|
80
|
+
anchored = true;
|
|
81
|
+
}
|
|
82
|
+
if (body.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
rules.push({
|
|
85
|
+
raw: line,
|
|
86
|
+
pattern: body,
|
|
87
|
+
negate,
|
|
88
|
+
directoryOnly,
|
|
89
|
+
anchored,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { rules, source };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Load + parse `.worktreeinclude` from a repository root. Returns an
|
|
96
|
+
* empty rule set when the file is absent or unreadable - a missing
|
|
97
|
+
* include file is the normal case (no extras to copy).
|
|
98
|
+
*/
|
|
99
|
+
export function loadWorktreeInclude(repoRoot) {
|
|
100
|
+
const path = resolve(repoRoot, WORKTREE_INCLUDE_FILENAME);
|
|
101
|
+
if (!existsSync(path)) {
|
|
102
|
+
return { rules: [], source: path };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const text = readFileSync(path, 'utf8');
|
|
106
|
+
return parseWorktreeIncludeText(text, path);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { rules: [], source: path };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const REGEX_ESCAPE = /[.+^${}()|[\]\\]/;
|
|
113
|
+
/**
|
|
114
|
+
* Convert a gitignore-style glob pattern into a regex source string.
|
|
115
|
+
* Subset implementation: covers `*`, `**`, `?`, and escapes the rest.
|
|
116
|
+
*
|
|
117
|
+
* Uses index-based scanning so `**` is detected and consumed before a
|
|
118
|
+
* single `*` substitution could eat the doubled form.
|
|
119
|
+
*/
|
|
120
|
+
function globToRegexSource(pattern) {
|
|
121
|
+
let out = '';
|
|
122
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
123
|
+
const ch = pattern.charAt(i);
|
|
124
|
+
if (ch === '*') {
|
|
125
|
+
if (pattern.charAt(i + 1) === '*') {
|
|
126
|
+
out += '.*';
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
out += '[^/]*';
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === '?') {
|
|
134
|
+
out += '[^/]';
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (REGEX_ESCAPE.test(ch)) {
|
|
138
|
+
out += '\\' + ch;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out += ch;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Decide whether `relPath` (a forward-slash, no-leading-slash path
|
|
147
|
+
* relative to the repo root) is matched by the given rule list. The
|
|
148
|
+
* last matching rule wins so a negation after an include drops the
|
|
149
|
+
* entry and an include after a negation re-adds it.
|
|
150
|
+
*/
|
|
151
|
+
export function matchesWorktreeInclude(relPath, rules, isDirectory = false) {
|
|
152
|
+
const normalized = relPath.replace(/\\+/g, '/');
|
|
153
|
+
let matched = false;
|
|
154
|
+
for (const rule of rules) {
|
|
155
|
+
if (rule.directoryOnly && !isDirectory) {
|
|
156
|
+
// directory-only rule still matches files UNDER the dir (handled
|
|
157
|
+
// below). Falls through to the file-match attempt.
|
|
158
|
+
}
|
|
159
|
+
const source = buildRegex(rule);
|
|
160
|
+
const re = new RegExp('^' + source + '$');
|
|
161
|
+
if (re.test(normalized)) {
|
|
162
|
+
matched = !rule.negate;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (rule.directoryOnly) {
|
|
166
|
+
const reUnder = new RegExp('^' + source + '/.*$');
|
|
167
|
+
if (reUnder.test(normalized)) {
|
|
168
|
+
matched = !rule.negate;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return matched;
|
|
173
|
+
}
|
|
174
|
+
function buildRegex(rule) {
|
|
175
|
+
const body = globToRegexSource(rule.pattern);
|
|
176
|
+
if (rule.anchored) {
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
// Unanchored pattern matches anywhere in the tree. Allow a
|
|
180
|
+
// segment-aligned match by accepting either start-of-string or a
|
|
181
|
+
// `/` boundary before the pattern body.
|
|
182
|
+
return '(?:.*/)?' + body;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Walk `repoRoot` and return every git-ignored file path that matches
|
|
186
|
+
* the include rules. Tracked files are excluded because they ride
|
|
187
|
+
* along with git automatically.
|
|
188
|
+
*
|
|
189
|
+
* Implementation note: rather than re-implement gitignore evaluation
|
|
190
|
+
* the default lister shells out to
|
|
191
|
+
* `git ls-files --others --ignored --exclude-standard` to get the
|
|
192
|
+
* authoritative ignored-untracked list. Tracked files never appear in
|
|
193
|
+
* that output, so the result is exactly the safe-to-copy candidate
|
|
194
|
+
* pool.
|
|
195
|
+
*/
|
|
196
|
+
export function collectWorktreeIncludeFiles(repoRoot, rules, options = {}) {
|
|
197
|
+
if (rules.length === 0)
|
|
198
|
+
return [];
|
|
199
|
+
const lister = options.listIgnored ?? defaultListIgnored;
|
|
200
|
+
const stat = options.isDirectory ?? defaultIsDirectory;
|
|
201
|
+
const candidates = lister(resolve(repoRoot));
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const rel = candidate.replace(/\\+/g, '/').replace(/^\.\//, '');
|
|
205
|
+
if (rel.length === 0)
|
|
206
|
+
continue;
|
|
207
|
+
if (rel.startsWith('.pugi/') || rel.startsWith('.claude/')) {
|
|
208
|
+
// Never copy Pugi or .claude state into the worktree. Those
|
|
209
|
+
// dirs are owned by the runtime and the include file should
|
|
210
|
+
// not be able to leak audit logs or stale session state.
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const absPath = resolve(repoRoot, rel);
|
|
214
|
+
const isDir = stat(absPath);
|
|
215
|
+
if (matchesWorktreeInclude(rel, rules, isDir)) {
|
|
216
|
+
out.push(rel);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function defaultListIgnored(repoRoot) {
|
|
222
|
+
// `-z` emits NUL-separated paths, the only safe form when filenames
|
|
223
|
+
// may contain spaces or newlines.
|
|
224
|
+
const result = spawnSync('git', ['ls-files', '--others', '--ignored', '--exclude-standard', '-z'], {
|
|
225
|
+
cwd: repoRoot,
|
|
226
|
+
encoding: 'buffer',
|
|
227
|
+
shell: false,
|
|
228
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
229
|
+
});
|
|
230
|
+
if (result.status !== 0 || !result.stdout)
|
|
231
|
+
return [];
|
|
232
|
+
const text = result.stdout.toString('utf8');
|
|
233
|
+
return text.split('\u0000').filter((s) => s.length > 0);
|
|
234
|
+
}
|
|
235
|
+
function defaultIsDirectory(absPath) {
|
|
236
|
+
try {
|
|
237
|
+
return statSync(absPath).isDirectory();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Cross-platform path-join in posix form (for matching semantics). */
|
|
244
|
+
export function joinRel(parent, child) {
|
|
245
|
+
return posix.join(parent.replace(/\\+/g, '/'), child);
|
|
246
|
+
}
|
|
247
|
+
/** Re-export the system separator so consumers do not have to import it. */
|
|
248
|
+
export const PATH_SEP = sep;
|
|
249
|
+
//# sourceMappingURL=include-parser.js.map
|
package/dist/runtime/cli.js
CHANGED
|
@@ -1596,9 +1596,75 @@ export async function runCli(argv) {
|
|
|
1596
1596
|
// because the welcome banner is the authoritative surface — a stray
|
|
1597
1597
|
// stderr line above the alt-screen flicker would race against the
|
|
1598
1598
|
// banner paint on slow terminals.
|
|
1599
|
+
// PUGI-487 - `pugi --worktree [<name>]` dispatch. When the flag is
|
|
1600
|
+
// present on a bare invocation we create an isolated git worktree at
|
|
1601
|
+
// `.claude/worktrees/<name>/` and chdir into it BEFORE the REPL mounts.
|
|
1602
|
+
// The flag is mutually exclusive with `--print` and `--headless`
|
|
1603
|
+
// (already short-circuited above) and with `--bare` (which disables
|
|
1604
|
+
// every project-discovery surface the worktree relies on). We refuse
|
|
1605
|
+
// those combinations with a clear error rather than silently dropping
|
|
1606
|
+
// the flag.
|
|
1607
|
+
if (typeof flags.worktree === 'string' && isBareInvocation) {
|
|
1608
|
+
if (flags.bare) {
|
|
1609
|
+
process.stderr.write('--worktree cannot be combined with --bare; bare mode disables project discovery.\n');
|
|
1610
|
+
process.exitCode = 2;
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const { bootstrapWorktree } = await import('./worktree-bootstrap.js');
|
|
1614
|
+
// PUGI-487 codex P2: honour `worktree.baseRef` from .pugi/settings.json
|
|
1615
|
+
// so an operator who set `head` (carry unpushed local work into the
|
|
1616
|
+
// new tree) actually gets that behaviour. The schema is optional;
|
|
1617
|
+
// `loadSettings` never throws so a bad/missing file degrades to the
|
|
1618
|
+
// default `fresh` branching.
|
|
1619
|
+
let configuredBaseRef;
|
|
1620
|
+
try {
|
|
1621
|
+
const { loadSettings } = await import('../core/settings.js');
|
|
1622
|
+
const settings = loadSettings(process.cwd());
|
|
1623
|
+
configuredBaseRef = settings.worktree?.baseRef;
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
// best-effort: a malformed settings file falls back to the
|
|
1627
|
+
// bootstrap's own default rather than aborting the worktree.
|
|
1628
|
+
}
|
|
1629
|
+
const envelope = await bootstrapWorktree({
|
|
1630
|
+
repoRoot: process.cwd(),
|
|
1631
|
+
nameArg: flags.worktree.length === 0 ? undefined : flags.worktree,
|
|
1632
|
+
...(configuredBaseRef ? { baseRef: configuredBaseRef } : {}),
|
|
1633
|
+
});
|
|
1634
|
+
if (!envelope.ok) {
|
|
1635
|
+
if (flags.json) {
|
|
1636
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
process.stderr.write(`pugi --worktree failed: ${envelope.message ?? envelope.reason ?? 'unknown error'}\n`);
|
|
1640
|
+
}
|
|
1641
|
+
process.exitCode = envelope.reason === 'untrusted_workspace' ? 3 : 1;
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
// chdir into the new tree so the REPL boot below treats it as the
|
|
1645
|
+
// workspace root. The flag does not change the rest of the bare-
|
|
1646
|
+
// invocation flow except for the cwd.
|
|
1647
|
+
try {
|
|
1648
|
+
process.chdir(envelope.directory);
|
|
1649
|
+
}
|
|
1650
|
+
catch (error) {
|
|
1651
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1652
|
+
process.stderr.write(`failed to enter worktree ${envelope.directory}: ${message}\n`);
|
|
1653
|
+
process.exitCode = 1;
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (flags.json) {
|
|
1657
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
process.stderr.write(`pugi --worktree ${envelope.name} -> ${envelope.directory} (branch ${envelope.branch}, copied ${envelope.copiedFiles.length} include file(s))\n`);
|
|
1661
|
+
}
|
|
1662
|
+
// Fall through to the bare-REPL branch below so the REPL mounts
|
|
1663
|
+
// inside the freshly-checked-out worktree.
|
|
1664
|
+
}
|
|
1599
1665
|
// Bare `pugi` on a TTY enters the REPL-by-default agentic session
|
|
1600
1666
|
// (Sprint , ). The REPL is the customer-facing surface
|
|
1601
|
-
// that brings Pugi to parity with the upstream tool /
|
|
1667
|
+
// that brings Pugi to parity with the upstream tool / peer CLI. When the
|
|
1602
1668
|
// operator has no credentials yet, we fall back to the splash
|
|
1603
1669
|
// so the install-time `pugi` surface still shows the wordmark +
|
|
1604
1670
|
// quick-start hints. Non-TTY (CI, pipes, `--no-tty`) also falls
|
|
@@ -2134,6 +2200,25 @@ function parseArgs(argv) {
|
|
|
2134
2200
|
else if (arg.startsWith('--filter=')) {
|
|
2135
2201
|
flags.smokeFilter = arg.slice('--filter='.length);
|
|
2136
2202
|
}
|
|
2203
|
+
else if (arg === '--worktree') {
|
|
2204
|
+
// PUGI-487 - user-facing parallel-isolation flag. Consumes
|
|
2205
|
+
// the next token as the name unless it starts with `--`
|
|
2206
|
+
// (treat as missing => auto-generate). The empty string is
|
|
2207
|
+
// the sentinel for "flag present without value".
|
|
2208
|
+
const next = argv[index + 1];
|
|
2209
|
+
if (typeof next === 'string' && next.length > 0 && !next.startsWith('--')) {
|
|
2210
|
+
flags.worktree = next;
|
|
2211
|
+
index += 1;
|
|
2212
|
+
}
|
|
2213
|
+
else {
|
|
2214
|
+
flags.worktree = '';
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
else if (arg.startsWith('--worktree=')) {
|
|
2218
|
+
// PUGI-487 - `--worktree=<name>` form. Empty value also
|
|
2219
|
+
// triggers the auto-name path.
|
|
2220
|
+
flags.worktree = arg.slice('--worktree='.length);
|
|
2221
|
+
}
|
|
2137
2222
|
else {
|
|
2138
2223
|
args.push(arg);
|
|
2139
2224
|
}
|
|
@@ -5864,7 +5949,7 @@ function parseProviderFlag(args) {
|
|
|
5864
5949
|
* `--json` / `--no-tty` / CI markers were not supplied. We only
|
|
5865
5950
|
* prompt or render Ink surfaces when a human is plausibly watching
|
|
5866
5951
|
* the screen. The multi-condition gate matches the upstream tool, gh CLI,
|
|
5867
|
-
*
|
|
5952
|
+
* peer CLI, and the npm CLI conventions.
|
|
5868
5953
|
*/
|
|
5869
5954
|
function isInteractive(flags) {
|
|
5870
5955
|
if (flags.json)
|
|
@@ -6023,7 +6108,7 @@ async function login(args, flags, _session) {
|
|
|
6023
6108
|
* Render the interactive Ink picker shown when `pugi login` runs on
|
|
6024
6109
|
* a TTY with no token args. Returns the chosen provider, or `null`
|
|
6025
6110
|
* when the user dismisses the picker via Esc / q. Mirrors the
|
|
6026
|
-
* the upstream tool /
|
|
6111
|
+
* the upstream tool / peer CLI auth picker UX.
|
|
6027
6112
|
*
|
|
6028
6113
|
* The Ink import is dynamic so a non-interactive `pugi <anything>`
|
|
6029
6114
|
* never pays the React+Ink module-load cost. ESM dynamic-import is
|
|
@@ -7154,21 +7239,36 @@ function notImplemented(command) {
|
|
|
7154
7239
|
}
|
|
7155
7240
|
function ensurePugiGitIgnore(cwd, created, skipped) {
|
|
7156
7241
|
const gitignorePath = resolve(cwd, '.gitignore');
|
|
7157
|
-
|
|
7242
|
+
// PUGI-487 - also ensure `.claude/worktrees/` is git-ignored so the
|
|
7243
|
+
// user-facing `pugi --worktree` flag does not surface its created
|
|
7244
|
+
// trees as untracked status noise.
|
|
7245
|
+
const markers = ['.pugi/', '.claude/worktrees/'];
|
|
7158
7246
|
if (!existsSync(gitignorePath)) {
|
|
7159
|
-
writeFileSync(gitignorePath, `${
|
|
7247
|
+
writeFileSync(gitignorePath, `${markers.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
7160
7248
|
created.push(gitignorePath);
|
|
7161
7249
|
return;
|
|
7162
7250
|
}
|
|
7163
7251
|
const current = readFileSync(gitignorePath, 'utf8');
|
|
7164
7252
|
const lines = current.split('\n').map((line) => line.trim());
|
|
7165
|
-
|
|
7253
|
+
const equivalents = {
|
|
7254
|
+
'.pugi/': ['.pugi/', '/.pugi/', '.pugi'],
|
|
7255
|
+
'.claude/worktrees/': ['.claude/worktrees/', '/.claude/worktrees/', '.claude/worktrees'],
|
|
7256
|
+
};
|
|
7257
|
+
const toAppend = [];
|
|
7258
|
+
for (const marker of markers) {
|
|
7259
|
+
const eq = equivalents[marker] ?? [marker];
|
|
7260
|
+
const present = eq.some((variant) => lines.includes(variant));
|
|
7261
|
+
if (!present)
|
|
7262
|
+
toAppend.push(marker);
|
|
7263
|
+
}
|
|
7264
|
+
if (toAppend.length === 0) {
|
|
7166
7265
|
skipped.push(gitignorePath);
|
|
7167
7266
|
return;
|
|
7168
7267
|
}
|
|
7169
|
-
const
|
|
7268
|
+
const trailing = current.endsWith('\n') ? '' : '\n';
|
|
7269
|
+
const next = `${current}${trailing}${toAppend.join('\n')}\n`;
|
|
7170
7270
|
writeFileSync(gitignorePath, next, { encoding: 'utf8' });
|
|
7171
|
-
created.push(`${gitignorePath} (+${
|
|
7271
|
+
created.push(`${gitignorePath} (+${toAppend.join(', ')})`);
|
|
7172
7272
|
}
|
|
7173
7273
|
/**
|
|
7174
7274
|
* Compute the workspace label surfaced in the REPL header bar
|
|
@@ -94,7 +94,7 @@ async function runAgentsList(args, ctx) {
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
if (verdicts.length === 0) {
|
|
97
|
-
ctx.writeOutput({ command: 'agents.list', agents: [] }, 'No agents installed. Try `pugi agents install gh:
|
|
97
|
+
ctx.writeOutput({ command: 'agents.list', agents: [] }, 'No agents installed. Try `pugi agents install gh:example-org/coding-agents/code-reviewer.md@main`.');
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
const lines = ['Installed agents:'];
|
|
@@ -101,6 +101,9 @@ function runList(ctx, flags) {
|
|
|
101
101
|
SubagentStop: [],
|
|
102
102
|
PreCompact: [],
|
|
103
103
|
Notification: [],
|
|
104
|
+
// PUGI-487 - worktree lifecycle events.
|
|
105
|
+
WorktreeCreate: [],
|
|
106
|
+
WorktreeRemove: [],
|
|
104
107
|
};
|
|
105
108
|
for (const event of ALL_HOOK_EVENTS_V2) {
|
|
106
109
|
perEvent[event] = config.list(event).map((entry) => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `pugi review --consensus` — customer-facing triple-review .
|
|
3
3
|
*
|
|
4
|
-
* The differentiator: the upstream tool ships single-Claude review,
|
|
4
|
+
* The differentiator: the upstream tool ships single-Claude review, peer CLI
|
|
5
5
|
* ships single-GPT review, Gemini CLI ships single-Gemini review. Pugi
|
|
6
6
|
* ships a 3-model consensus gate as a first-class command so customers
|
|
7
7
|
* get the same production-readiness signal we use internally - without the
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.89');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|