@zenobius/pi-worktrees 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -67,779 +67,378 @@ function getConfiguredWorktreeRoot(settings) {
67
67
  return settings.worktreeRoot ?? settings.parentDir;
68
68
  }
69
69
 
70
- // src/services/config/config.ts
71
- import { createConfigService } from "@zenobius/pi-extension-config";
72
- import { Parse as Parse2 } from "typebox/value";
70
+ // src/services/glob.ts
71
+ function globToRegExp(pattern) {
72
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
73
+ const doubleStarReplaced = escaped.replace(/\*\*/g, "::DOUBLE_STAR::");
74
+ const singleStarReplaced = doubleStarReplaced.replace(/\*/g, "[^/]*");
75
+ const regexBody = singleStarReplaced.replace(/::DOUBLE_STAR::/g, ".*");
76
+ return new RegExp(`^${regexBody}$`, "i");
77
+ }
78
+ function globMatch(input, pattern) {
79
+ return globToRegExp(pattern).test(input);
80
+ }
73
81
 
74
- // src/services/config/migrations/01-flat-single.ts
75
- import {
76
- Array as TypeArray2,
77
- Object as TypeObject2,
78
- Optional as Optional2,
79
- String as TypeString2,
80
- Union as Union2
81
- } from "typebox";
82
- import { Parse } from "typebox/value";
83
- var LegacyOnCreateSchema = Union2([TypeString2(), TypeArray2(TypeString2())]);
84
- var LegacyWorktreeSettingsSchema = TypeObject2({
85
- parentDir: Optional2(TypeString2()),
86
- onCreate: Optional2(LegacyOnCreateSchema)
87
- }, {
88
- additionalProperties: true
89
- });
90
- var LegacyConfigSchema = TypeObject2({
91
- parentDir: Optional2(TypeString2()),
92
- onCreate: Optional2(LegacyOnCreateSchema),
93
- worktree: Optional2(LegacyWorktreeSettingsSchema)
94
- }, {
95
- additionalProperties: true
96
- });
97
- function toRecord(value) {
98
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
99
- return {};
82
+ // src/services/git.ts
83
+ function git(args, cwd) {
84
+ try {
85
+ return execSync(`git ${args.join(" ")}`, {
86
+ cwd,
87
+ encoding: "utf-8",
88
+ stdio: ["pipe", "pipe", "pipe"]
89
+ }).trim();
90
+ } catch (error) {
91
+ throw new Error(`git ${args[0]} failed: ${error.message}`);
100
92
  }
101
- return { ...value };
102
93
  }
103
- function getFallbackSettings(config) {
104
- const nested = config.worktree ?? {};
105
- const fallback = {};
106
- if (nested.parentDir !== undefined) {
107
- fallback.parentDir = nested.parentDir;
108
- } else if (config.parentDir !== undefined) {
109
- fallback.parentDir = config.parentDir;
94
+ function getRemoteUrl(cwd, remote = "origin") {
95
+ try {
96
+ return git(["remote", "get-url", remote], cwd);
97
+ } catch {
98
+ return null;
110
99
  }
111
- if (nested.onCreate !== undefined) {
112
- fallback.onCreate = nested.onCreate;
113
- } else if (config.onCreate !== undefined) {
114
- fallback.onCreate = config.onCreate;
100
+ }
101
+ function isGitRepo(cwd) {
102
+ try {
103
+ git(["rev-parse", "--git-dir"], cwd);
104
+ return true;
105
+ } catch {
106
+ return false;
115
107
  }
116
- return fallback;
117
108
  }
118
- var migration = {
119
- id: "legacy-flat-worktree-settings",
120
- up(config) {
121
- const record = toRecord(config);
122
- const parsed = Parse(LegacyConfigSchema, record);
123
- const fallback = getFallbackSettings(parsed);
124
- const next = { ...record };
125
- if (Object.keys(fallback).length > 0) {
126
- next.worktree = fallback;
127
- }
128
- delete next.parentDir;
129
- delete next.onCreate;
130
- return next;
131
- },
132
- down(config) {
133
- const record = toRecord(config);
134
- const parsed = Parse(LegacyConfigSchema, record);
135
- const worktree = toRecord(parsed.worktree);
136
- const next = { ...record };
137
- if (worktree.parentDir !== undefined) {
138
- next.parentDir = worktree.parentDir;
139
- }
140
- if (worktree.onCreate !== undefined) {
141
- next.onCreate = worktree.onCreate;
109
+ function getMainWorktreePath(cwd) {
110
+ const gitCommonDir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
111
+ return dirname(gitCommonDir);
112
+ }
113
+ function getProjectName(cwd) {
114
+ return basename(getMainWorktreePath(cwd));
115
+ }
116
+ function isWorktree(cwd) {
117
+ try {
118
+ const gitPath = join(cwd, ".git");
119
+ if (existsSync(gitPath)) {
120
+ const stat = statSync(gitPath);
121
+ return stat.isFile();
142
122
  }
143
- delete next.worktree;
144
- return next;
145
- }
146
- };
147
-
148
- // src/services/config/migrations/02-worktree-to-worktrees.ts
149
- var FALLBACK_WORKTREE_PATTERN = "**";
150
- function toRecord2(value) {
151
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
152
- return {};
123
+ return false;
124
+ } catch {
125
+ return false;
153
126
  }
154
- return { ...value };
155
127
  }
156
- function sanitizeLegacyWorktreeSettings(value) {
157
- const source = toRecord2(value);
158
- const next = {};
159
- if (source.parentDir !== undefined) {
160
- next.parentDir = source.parentDir;
161
- }
162
- if (source.onCreate !== undefined) {
163
- next.onCreate = source.onCreate;
128
+ function getCurrentBranch(cwd) {
129
+ try {
130
+ return git(["branch", "--show-current"], cwd) || "HEAD (detached)";
131
+ } catch {
132
+ return "unknown";
164
133
  }
165
- return next;
166
134
  }
167
- var migration2 = {
168
- id: "legacy-worktree-to-worktrees",
169
- up(config) {
170
- const record = toRecord2(config);
171
- const next = { ...record };
172
- const topLevel = toRecord2(record);
173
- const worktree = sanitizeLegacyWorktreeSettings(record.worktree);
174
- const hasLegacyWorktreeSettings = Object.keys(worktree).length > 0;
175
- if (!hasLegacyWorktreeSettings) {
176
- return next;
177
- }
178
- const existingWorktrees = toRecord2(record.worktrees);
179
- const mergedWorktrees = { ...existingWorktrees };
180
- const existingFallback = toRecord2(mergedWorktrees[FALLBACK_WORKTREE_PATTERN]);
181
- mergedWorktrees[FALLBACK_WORKTREE_PATTERN] = {
182
- ...existingFallback,
183
- ...worktree
184
- };
185
- next.worktrees = mergedWorktrees;
186
- if (topLevel.logfile !== undefined) {
187
- next.logfile = topLevel.logfile;
188
- }
189
- delete next.worktree;
190
- return next;
191
- },
192
- down(config) {
193
- const record = toRecord2(config);
194
- const next = { ...record };
195
- const topLevel = toRecord2(record);
196
- const worktrees = toRecord2(record.worktrees);
197
- const fallbackSettings = sanitizeLegacyWorktreeSettings(worktrees[FALLBACK_WORKTREE_PATTERN]);
198
- if (Object.keys(fallbackSettings).length > 0) {
199
- next.worktree = fallbackSettings;
200
- const remaining = { ...worktrees };
201
- delete remaining[FALLBACK_WORKTREE_PATTERN];
202
- if (Object.keys(remaining).length > 0) {
203
- next.worktrees = remaining;
204
- } else {
205
- delete next.worktrees;
135
+ function listWorktrees(cwd) {
136
+ const output = git(["worktree", "list", "--porcelain"], cwd);
137
+ const worktrees = [];
138
+ const currentPath = resolve(cwd);
139
+ const mainPath = getMainWorktreePath(cwd);
140
+ let current = {};
141
+ for (const line of output.split(`
142
+ `)) {
143
+ if (line.startsWith("worktree ")) {
144
+ current.path = line.slice(9);
145
+ } else if (line.startsWith("HEAD ")) {
146
+ current.head = line.slice(5);
147
+ } else if (line.startsWith("branch ")) {
148
+ current.branch = line.slice(7).replace("refs/heads/", "");
149
+ } else if (line === "detached") {
150
+ current.branch = "HEAD (detached)";
151
+ } else if (line === "") {
152
+ if (current.path) {
153
+ worktrees.push({
154
+ path: current.path,
155
+ branch: current.branch || "unknown",
156
+ head: current.head || "unknown",
157
+ isMain: current.path === mainPath,
158
+ isCurrent: current.path === currentPath
159
+ });
206
160
  }
161
+ current = {};
207
162
  }
208
- if (topLevel.logfile !== undefined) {
209
- next.logfile = topLevel.logfile;
210
- }
211
- return next;
212
163
  }
213
- };
214
-
215
- // src/services/config/migrations/03-parentDir-to-worktreeRoot.ts
216
- function toRecord3(value) {
217
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
218
- return {};
164
+ if (current.path) {
165
+ worktrees.push({
166
+ path: current.path,
167
+ branch: current.branch || "unknown",
168
+ head: current.head || "unknown",
169
+ isMain: current.path === mainPath,
170
+ isCurrent: current.path === currentPath
171
+ });
219
172
  }
220
- return { ...value };
173
+ return worktrees;
221
174
  }
222
- function migrateSettings(value) {
223
- const source = toRecord3(value);
224
- const next = {};
225
- if (source.worktreeRoot !== undefined) {
226
- next.worktreeRoot = source.worktreeRoot;
227
- } else if (source.parentDir !== undefined) {
228
- next.worktreeRoot = source.parentDir;
175
+ function isPathInsideRepo(repoPath, targetPath) {
176
+ const relPath = relative(repoPath, targetPath);
177
+ return !relPath.startsWith("..") && !relPath.startsWith("/");
178
+ }
179
+ function getWorktreeParentDir(cwd, repos, matchStrategy) {
180
+ const project = getProjectName(cwd);
181
+ const mainWorktree = getMainWorktreePath(cwd);
182
+ const repo = getRemoteUrl(cwd);
183
+ const repoReference = repo && repo.trim().length > 0 ? repo : "**";
184
+ const worktree = matchRepo(repoReference, repos, matchStrategy);
185
+ if (worktree.type === "tie-conflict") {
186
+ throw new Error(worktree.message);
229
187
  }
230
- if (source.onCreate !== undefined) {
231
- next.onCreate = source.onCreate;
188
+ const configuredRoot = getConfiguredWorktreeRoot(worktree.settings);
189
+ if (configuredRoot) {
190
+ return expandTemplate(configuredRoot, {
191
+ path: "",
192
+ name: "",
193
+ branch: "",
194
+ project,
195
+ mainWorktree
196
+ });
232
197
  }
233
- return next;
198
+ return `${mainWorktree}.worktrees`;
234
199
  }
235
- var migration3 = {
236
- id: "parentDir-to-worktreeRoot",
237
- up(config) {
238
- const record = toRecord3(config);
239
- const next = { ...record };
240
- const worktrees = toRecord3(record.worktrees);
241
- const migratedEntries = Object.entries(worktrees).map(([pattern, value]) => [
242
- pattern,
243
- migrateSettings(value)
244
- ]);
245
- if (migratedEntries.length > 0) {
246
- next.worktrees = Object.fromEntries(migratedEntries);
247
- }
248
- if (record.worktree !== undefined) {
249
- next.worktree = migrateSettings(record.worktree);
250
- }
251
- return next;
252
- },
253
- down(config) {
254
- const record = toRecord3(config);
255
- const next = { ...record };
256
- const worktrees = toRecord3(record.worktrees);
257
- const downgradedEntries = Object.entries(worktrees).map(([pattern, value]) => {
258
- const migrated = migrateSettings(value);
259
- const downSettings = {};
260
- if (migrated.worktreeRoot !== undefined) {
261
- downSettings.parentDir = migrated.worktreeRoot;
262
- }
263
- if (migrated.onCreate !== undefined) {
264
- downSettings.onCreate = migrated.onCreate;
265
- }
266
- return [pattern, downSettings];
267
- });
268
- if (downgradedEntries.length > 0) {
269
- next.worktrees = Object.fromEntries(downgradedEntries);
200
+ function ensureExcluded(cwd, worktreeParentDir) {
201
+ const mainWorktree = getMainWorktreePath(cwd);
202
+ if (!isPathInsideRepo(mainWorktree, worktreeParentDir)) {
203
+ return;
204
+ }
205
+ const excludePath = join(mainWorktree, ".git", "info", "exclude");
206
+ const relPath = relative(mainWorktree, worktreeParentDir);
207
+ const excludePattern = `/${relPath}/`;
208
+ try {
209
+ let content = "";
210
+ if (existsSync(excludePath)) {
211
+ content = readFileSync(excludePath, "utf-8");
270
212
  }
271
- if (record.worktree !== undefined) {
272
- const migrated = migrateSettings(record.worktree);
273
- const downSettings = {};
274
- if (migrated.worktreeRoot !== undefined) {
275
- downSettings.parentDir = migrated.worktreeRoot;
276
- }
277
- if (migrated.onCreate !== undefined) {
278
- downSettings.onCreate = migrated.onCreate;
279
- }
280
- next.worktree = downSettings;
213
+ if (content.includes(excludePattern) || content.includes(relPath)) {
214
+ return;
281
215
  }
282
- return next;
283
- }
284
- };
216
+ const newEntry = `
217
+ # Worktree directory (added by worktree extension)
218
+ ${excludePattern}
219
+ `;
220
+ appendFileSync(excludePath, newEntry);
221
+ } catch {}
222
+ }
285
223
 
286
- // src/services/config/migrations/04-oncreate-display-output-max-lines.ts
287
- var DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES = 5;
288
- function toRecord4(value) {
289
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
290
- return {};
224
+ class ConfiguredRepoKeyMismatchException extends Error {
225
+ constructor(winner) {
226
+ super();
227
+ this.message = `ConfiguredRepoKeyMismatch: expected ${winner} to resolve to WorktreeSettingsConfig`;
291
228
  }
292
- return { ...value };
293
229
  }
294
- var migration4 = {
295
- id: "oncreate-display-output-max-lines-default",
296
- up(config) {
297
- const record = toRecord4(config);
298
- if (record.onCreateDisplayOutputMaxLines !== undefined) {
299
- return record;
230
+ function normalizeRepoReference(value) {
231
+ const trimmed = value.trim();
232
+ const withoutProtocol = trimmed.replace(/^ssh:\/\//, "").replace(/^https?:\/\//, "").replace(/^git@([^:]+):/, "$1/");
233
+ return withoutProtocol.replace(/\.git$/, "").replace(/\/+$/, "");
234
+ }
235
+ function calculateSpecificity(normalizedPattern) {
236
+ const segments = normalizedPattern.split("/").filter(Boolean);
237
+ let score = 0;
238
+ for (const segment of segments) {
239
+ if (segment === "**" || segment === "*") {
240
+ continue;
241
+ }
242
+ if (segment.includes("*")) {
243
+ score += 0.5;
244
+ continue;
300
245
  }
246
+ score += 1;
247
+ }
248
+ return score;
249
+ }
250
+ function resolveTie(tiedMatches, url, repos, matchingStrategy) {
251
+ const patterns = tiedMatches.map((match) => match.pattern);
252
+ const strategy = matchingStrategy || "fail-on-tie";
253
+ if (strategy === "fail-on-tie") {
301
254
  return {
302
- ...record,
303
- onCreateDisplayOutputMaxLines: DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES
255
+ type: "tie-conflict",
256
+ patterns,
257
+ url,
258
+ message: `Multiple patterns match with equal specificity:
259
+ ${patterns.map((pattern) => ` - ${pattern}`).join(`
260
+ `)}
261
+
262
+ Refine patterns or set matchingStrategy to 'first-wins' or 'last-wins'.`
304
263
  };
305
- },
306
- down(config) {
307
- const record = toRecord4(config);
308
- const next = { ...record };
309
- delete next.onCreateDisplayOutputMaxLines;
310
- return next;
311
264
  }
312
- };
313
-
314
- // src/services/config/migrations/05-oncreate-command-display-format.ts
315
- var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING = "[ ] {{cmd}}";
316
- var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS = "[x] {{cmd}}";
317
- var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR = "[ ] {{cmd}} [ERROR]";
318
- var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR = "dim";
319
- var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR = "success";
320
- var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR = "error";
321
- function toRecord5(value) {
322
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
323
- return {};
265
+ const winner = strategy === "last-wins" ? patterns[patterns.length - 1] : patterns[0];
266
+ const settings = repos.get(winner);
267
+ if (!settings) {
268
+ throw new Error;
324
269
  }
325
- return { ...value };
270
+ return {
271
+ settings,
272
+ matchedPattern: winner,
273
+ type: strategy
274
+ };
326
275
  }
327
- var migration5 = {
328
- id: "oncreate-command-display-format-defaults",
329
- up(config) {
330
- const record = toRecord5(config);
331
- const next = { ...record };
332
- if (next.onCreateCmdDisplayPending === undefined) {
333
- next.onCreateCmdDisplayPending = DEFAULT_ONCREATE_CMD_DISPLAY_PENDING;
334
- }
335
- if (next.onCreateCmdDisplaySuccess === undefined) {
336
- next.onCreateCmdDisplaySuccess = DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS;
337
- }
338
- if (next.onCreateCmdDisplayError === undefined) {
339
- next.onCreateCmdDisplayError = DEFAULT_ONCREATE_CMD_DISPLAY_ERROR;
340
- }
341
- if (next.onCreateCmdDisplayPendingColor === undefined) {
342
- next.onCreateCmdDisplayPendingColor = DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR;
343
- }
344
- if (next.onCreateCmdDisplaySuccessColor === undefined) {
345
- next.onCreateCmdDisplaySuccessColor = DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR;
276
+ function matchRepo(url, repos, matchStrategy) {
277
+ const repoReference = url && url.trim().length > 0 ? url : "**";
278
+ const normalizedUrl = normalizeRepoReference(repoReference);
279
+ const scoredMatches = [];
280
+ for (const [pattern, settings2] of repos.entries()) {
281
+ const normalizedPattern = normalizeRepoReference(pattern);
282
+ if (normalizedUrl === normalizedPattern) {
283
+ return {
284
+ settings: settings2,
285
+ matchedPattern: pattern,
286
+ type: "exact"
287
+ };
346
288
  }
347
- if (next.onCreateCmdDisplayErrorColor === undefined) {
348
- next.onCreateCmdDisplayErrorColor = DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR;
289
+ if (globMatch(normalizedUrl, normalizedPattern)) {
290
+ scoredMatches.push({
291
+ pattern,
292
+ normalizedPattern,
293
+ specificity: calculateSpecificity(normalizedPattern)
294
+ });
349
295
  }
350
- return next;
351
- },
352
- down(config) {
353
- const record = toRecord5(config);
354
- const next = { ...record };
355
- delete next.onCreateCmdDisplayPending;
356
- delete next.onCreateCmdDisplaySuccess;
357
- delete next.onCreateCmdDisplayError;
358
- delete next.onCreateCmdDisplayPendingColor;
359
- delete next.onCreateCmdDisplaySuccessColor;
360
- delete next.onCreateCmdDisplayErrorColor;
361
- return next;
362
296
  }
363
- };
364
-
365
- // src/services/config/config.ts
366
- var DEFAULT_LOGFILE_TEMPLATE = "/tmp/pi-worktree-{sessionId}-{name}.log";
367
- var DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES2 = 5;
368
- var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING2 = "[ ] {{cmd}}";
369
- var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS2 = "[x] {{cmd}}";
370
- var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR2 = "[ ] {{cmd}} [ERROR]";
371
- var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR2 = "dim";
372
- var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR2 = "success";
373
- var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR2 = "error";
374
- async function createPiWorktreeConfigService() {
375
- const parse = (value) => {
376
- return Parse2(PiWorktreeConfigSchema, value);
297
+ if (scoredMatches.length === 0) {
298
+ throw new Error(`No matching worktree settings for repo: ${normalizedUrl}`);
299
+ }
300
+ scoredMatches.sort((left, right) => right.specificity - left.specificity);
301
+ const topSpecificity = scoredMatches[0].specificity;
302
+ const tiedMatches = scoredMatches.filter((match) => match.specificity === topSpecificity);
303
+ if (tiedMatches.length > 1) {
304
+ return resolveTie(tiedMatches, normalizedUrl, repos, matchStrategy);
305
+ }
306
+ const winner = scoredMatches[0].pattern;
307
+ const settings = repos.get(winner);
308
+ if (!settings) {
309
+ throw new ConfiguredRepoKeyMismatchException(winner);
310
+ }
311
+ return {
312
+ settings,
313
+ matchedPattern: winner,
314
+ type: "exact"
377
315
  };
378
- const store = await createConfigService("pi-worktrees", {
379
- defaults: {},
380
- parse,
381
- migrations: [migration, migration2, migration3, migration4, migration5]
382
- });
383
- await store.reload();
384
- const save = async (data) => {
385
- if (data.worktrees !== undefined) {
386
- await store.set("worktrees", data.worktrees, "home");
387
- }
388
- if (data.matchingStrategy !== undefined) {
389
- await store.set("matchingStrategy", data.matchingStrategy, "home");
390
- }
391
- if (data.logfile !== undefined) {
392
- await store.set("logfile", data.logfile, "home");
316
+ }
317
+
318
+ // src/cmds/cmdCd.ts
319
+ async function cmdCd(args, ctx, deps) {
320
+ const worktreeName = args.trim();
321
+ if (!isGitRepo(ctx.cwd)) {
322
+ ctx.ui.notify("Not in a git repository", "error");
323
+ return;
324
+ }
325
+ const worktrees = listWorktrees(ctx.cwd);
326
+ const current = deps.configService.current(ctx);
327
+ if (!worktreeName) {
328
+ const main = worktrees.find((worktree) => worktree.isMain);
329
+ if (main) {
330
+ ctx.ui.notify(`Main worktree: ${main.path}`, "info");
393
331
  }
394
- if (data.onCreateDisplayOutputMaxLines !== undefined) {
395
- await store.set("onCreateDisplayOutputMaxLines", data.onCreateDisplayOutputMaxLines, "home");
396
- }
397
- if (data.onCreateCmdDisplayPending !== undefined) {
398
- await store.set("onCreateCmdDisplayPending", data.onCreateCmdDisplayPending, "home");
399
- }
400
- if (data.onCreateCmdDisplaySuccess !== undefined) {
401
- await store.set("onCreateCmdDisplaySuccess", data.onCreateCmdDisplaySuccess, "home");
402
- }
403
- if (data.onCreateCmdDisplayError !== undefined) {
404
- await store.set("onCreateCmdDisplayError", data.onCreateCmdDisplayError, "home");
405
- }
406
- if (data.onCreateCmdDisplayPendingColor !== undefined) {
407
- await store.set("onCreateCmdDisplayPendingColor", data.onCreateCmdDisplayPendingColor, "home");
408
- }
409
- if (data.onCreateCmdDisplaySuccessColor !== undefined) {
410
- await store.set("onCreateCmdDisplaySuccessColor", data.onCreateCmdDisplaySuccessColor, "home");
411
- }
412
- if (data.onCreateCmdDisplayErrorColor !== undefined) {
413
- await store.set("onCreateCmdDisplayErrorColor", data.onCreateCmdDisplayErrorColor, "home");
414
- }
415
- await store.save("home");
416
- };
417
- const worktrees = new Map(Object.entries(store.config.worktrees || {}));
418
- const current = (ctx) => {
419
- const repo = getRemoteUrl(ctx.cwd);
420
- const resolution = matchRepo(repo, worktrees, store.config.matchingStrategy);
421
- if (resolution.type === "tie-conflict") {
422
- throw new Error(resolution.message);
423
- }
424
- const settings = resolution.settings;
425
- const project = getProjectName(ctx.cwd);
426
- const mainWorktree = getMainWorktreePath(ctx.cwd);
427
- const parentDir = getWorktreeParentDir(ctx.cwd, worktrees, store.config.matchingStrategy);
428
- return {
429
- ...settings,
430
- repo,
431
- project,
432
- mainWorktree,
433
- parentDir,
434
- logfile: store.config.logfile ?? DEFAULT_LOGFILE_TEMPLATE,
435
- onCreateDisplayOutputMaxLines: store.config.onCreateDisplayOutputMaxLines ?? DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES2,
436
- onCreateCmdDisplayPending: store.config.onCreateCmdDisplayPending ?? DEFAULT_ONCREATE_CMD_DISPLAY_PENDING2,
437
- onCreateCmdDisplaySuccess: store.config.onCreateCmdDisplaySuccess ?? DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS2,
438
- onCreateCmdDisplayError: store.config.onCreateCmdDisplayError ?? DEFAULT_ONCREATE_CMD_DISPLAY_ERROR2,
439
- onCreateCmdDisplayPendingColor: store.config.onCreateCmdDisplayPendingColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR2,
440
- onCreateCmdDisplaySuccessColor: store.config.onCreateCmdDisplaySuccessColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR2,
441
- onCreateCmdDisplayErrorColor: store.config.onCreateCmdDisplayErrorColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR2,
442
- matchedPattern: resolution.matchedPattern
443
- };
444
- };
445
- const service = {
446
- ...store,
447
- worktrees,
448
- current,
449
- save
450
- };
451
- return service;
332
+ return;
333
+ }
334
+ const target = worktrees.find((worktree) => basename2(worktree.path) === worktreeName || worktree.path === worktreeName || worktree.path === join2(current.parentDir, worktreeName));
335
+ if (!target) {
336
+ ctx.ui.notify(`Worktree not found: ${worktreeName}`, "error");
337
+ return;
338
+ }
339
+ ctx.ui.notify(`Worktree path: ${target.path}`, "info");
452
340
  }
453
- var DefaultWorktreeSettings = {
454
- worktreeRoot: "{{mainWorktree}}.worktrees",
455
- onCreate: "cd {cwd}"
456
- };
457
- var DefaultLogfileTemplate = DEFAULT_LOGFILE_TEMPLATE;
458
341
 
459
- // src/services/glob.ts
460
- function globToRegExp(pattern) {
461
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
462
- const doubleStarReplaced = escaped.replace(/\*\*/g, "::DOUBLE_STAR::");
463
- const singleStarReplaced = doubleStarReplaced.replace(/\*/g, "[^/]*");
464
- const regexBody = singleStarReplaced.replace(/::DOUBLE_STAR::/g, ".*");
465
- return new RegExp(`^${regexBody}$`, "i");
466
- }
467
- function globMatch(input, pattern) {
468
- return globToRegExp(pattern).test(input);
469
- }
342
+ // src/cmds/cmdCreate.ts
343
+ import { join as join3 } from "path";
470
344
 
471
- // src/services/git.ts
472
- function git(args, cwd) {
473
- try {
474
- return execSync(`git ${args.join(" ")}`, {
475
- cwd,
476
- encoding: "utf-8",
477
- stdio: ["pipe", "pipe", "pipe"]
478
- }).trim();
479
- } catch (error) {
480
- throw new Error(`git ${args[0]} failed: ${error.message}`);
481
- }
345
+ // src/cmds/shared.ts
346
+ import { appendFileSync as appendFileSync2, writeFileSync } from "fs";
347
+ import { spawn } from "child_process";
348
+ var ANSI = {
349
+ reset: "\x1B[0m",
350
+ gray: "\x1B[90m",
351
+ blue: "\x1B[34m",
352
+ green: "\x1B[32m",
353
+ red: "\x1B[31m",
354
+ yellow: "\x1B[33m"
355
+ };
356
+ function applyCommandTemplate(template, command) {
357
+ return template.replace(/\{\{cmd\}\}|\{cmd\}/g, command);
482
358
  }
483
- function getRemoteUrl(cwd, remote = "origin") {
484
- try {
485
- return git(["remote", "get-url", remote], cwd);
486
- } catch {
487
- return null;
359
+ function resolveAnsiColor(colorName) {
360
+ if (colorName === "dim") {
361
+ return ANSI.gray;
362
+ }
363
+ if (colorName === "accent" || colorName === "info") {
364
+ return ANSI.blue;
365
+ }
366
+ if (colorName === "success") {
367
+ return ANSI.green;
368
+ }
369
+ if (colorName === "error") {
370
+ return ANSI.red;
371
+ }
372
+ if (colorName === "warning") {
373
+ return ANSI.yellow;
488
374
  }
375
+ return "";
489
376
  }
490
- function isGitRepo(cwd) {
491
- try {
492
- git(["rev-parse", "--git-dir"], cwd);
493
- return true;
494
- } catch {
495
- return false;
377
+ function colorize(text, colorName) {
378
+ const ansi = resolveAnsiColor(colorName);
379
+ if (!ansi) {
380
+ return text;
496
381
  }
382
+ return `${ansi}${text}${ANSI.reset}`;
497
383
  }
498
- function getMainWorktreePath(cwd) {
499
- const gitCommonDir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
500
- return dirname(gitCommonDir);
384
+ function formatCommandLine(command, state, config) {
385
+ if (state === "success") {
386
+ return colorize(applyCommandTemplate(config.successTemplate, command), config.successColor);
387
+ }
388
+ if (state === "failed") {
389
+ return colorize(applyCommandTemplate(config.errorTemplate, command), config.errorColor);
390
+ }
391
+ return colorize(applyCommandTemplate(config.pendingTemplate, command), config.pendingColor);
501
392
  }
502
- function getProjectName(cwd) {
503
- return basename(getMainWorktreePath(cwd));
393
+ function toLines(text) {
394
+ return text.replace(/\r/g, "").split(`
395
+ `).map((line) => line.trimEnd()).filter((line) => line.length > 0);
504
396
  }
505
- function isWorktree(cwd) {
506
- try {
507
- const gitPath = join(cwd, ".git");
508
- if (existsSync(gitPath)) {
509
- const stat = statSync(gitPath);
510
- return stat.isFile();
511
- }
512
- return false;
513
- } catch {
514
- return false;
397
+ function formatOutputLine(stream, line, state) {
398
+ const prefix = stream === "stderr" ? "\u26A0" : "\u203A";
399
+ if (state === "running") {
400
+ return ` ${prefix} ${line}`;
515
401
  }
402
+ return `${ANSI.gray} ${prefix} ${line}${ANSI.reset}`;
516
403
  }
517
- function getCurrentBranch(cwd) {
518
- try {
519
- return git(["branch", "--show-current"], cwd) || "HEAD (detached)";
520
- } catch {
521
- return "unknown";
404
+ function getDisplayLines(text, maxLines) {
405
+ const lines = toLines(text);
406
+ if (maxLines < 0) {
407
+ return lines;
408
+ }
409
+ if (maxLines === 0) {
410
+ return [];
522
411
  }
412
+ return lines.slice(-maxLines);
523
413
  }
524
- function listWorktrees(cwd) {
525
- const output = git(["worktree", "list", "--porcelain"], cwd);
526
- const worktrees = [];
527
- const currentPath = resolve(cwd);
528
- const mainPath = getMainWorktreePath(cwd);
529
- let current = {};
530
- for (const line of output.split(`
531
- `)) {
532
- if (line.startsWith("worktree ")) {
533
- current.path = line.slice(9);
534
- } else if (line.startsWith("HEAD ")) {
535
- current.head = line.slice(5);
536
- } else if (line.startsWith("branch ")) {
537
- current.branch = line.slice(7).replace("refs/heads/", "");
538
- } else if (line === "detached") {
539
- current.branch = "HEAD (detached)";
540
- } else if (line === "") {
541
- if (current.path) {
542
- worktrees.push({
543
- path: current.path,
544
- branch: current.branch || "unknown",
545
- head: current.head || "unknown",
546
- isMain: current.path === mainPath,
547
- isCurrent: current.path === currentPath
548
- });
549
- }
550
- current = {};
414
+ function formatCommandList(commands, states, outputs, commandDisplay, logPath, displayOutputMaxLines = 5) {
415
+ const lines = ["onCreate steps:"];
416
+ for (const [index, command] of commands.entries()) {
417
+ const state = states[index];
418
+ lines.push(formatCommandLine(command, state, commandDisplay));
419
+ for (const line of getDisplayLines(outputs[index].stdout, displayOutputMaxLines)) {
420
+ lines.push(formatOutputLine("stdout", line, state));
421
+ }
422
+ for (const line of getDisplayLines(outputs[index].stderr, displayOutputMaxLines)) {
423
+ lines.push(formatOutputLine("stderr", line, state));
551
424
  }
552
425
  }
553
- if (current.path) {
554
- worktrees.push({
555
- path: current.path,
556
- branch: current.branch || "unknown",
557
- head: current.head || "unknown",
558
- isMain: current.path === mainPath,
559
- isCurrent: current.path === currentPath
560
- });
426
+ if (logPath) {
427
+ lines.push("");
428
+ lines.push(`${ANSI.gray}log: ${logPath}${ANSI.reset}`);
561
429
  }
562
- return worktrees;
563
- }
564
- function isPathInsideRepo(repoPath, targetPath) {
565
- const relPath = relative(repoPath, targetPath);
566
- return !relPath.startsWith("..") && !relPath.startsWith("/");
430
+ return lines.join(`
431
+ `);
567
432
  }
568
- function getWorktreeParentDir(cwd, repos, matchStrategy) {
569
- const project = getProjectName(cwd);
570
- const mainWorktree = getMainWorktreePath(cwd);
571
- const repo = getRemoteUrl(cwd);
572
- if (!repo) {
573
- throw new Error("Not a git repo");
433
+ function appendCommandLog(logPath, command, result) {
434
+ const lines = [`$ ${command}`];
435
+ if (result.stdout) {
436
+ lines.push("[stdout]");
437
+ lines.push(result.stdout.trimEnd());
574
438
  }
575
- const worktree = matchRepo(repo, repos, matchStrategy);
576
- if (worktree.type === "tie-conflict") {
577
- throw new Error(worktree.message);
578
- }
579
- const configuredRoot = getConfiguredWorktreeRoot(worktree.settings);
580
- if (configuredRoot) {
581
- return expandTemplate(configuredRoot, {
582
- path: "",
583
- name: "",
584
- branch: "",
585
- project,
586
- mainWorktree
587
- });
588
- }
589
- return `${mainWorktree}.worktrees`;
590
- }
591
- function ensureExcluded(cwd, worktreeParentDir) {
592
- const mainWorktree = getMainWorktreePath(cwd);
593
- if (!isPathInsideRepo(mainWorktree, worktreeParentDir)) {
594
- return;
595
- }
596
- const excludePath = join(mainWorktree, ".git", "info", "exclude");
597
- const relPath = relative(mainWorktree, worktreeParentDir);
598
- const excludePattern = `/${relPath}/`;
599
- try {
600
- let content = "";
601
- if (existsSync(excludePath)) {
602
- content = readFileSync(excludePath, "utf-8");
603
- }
604
- if (content.includes(excludePattern) || content.includes(relPath)) {
605
- return;
606
- }
607
- const newEntry = `
608
- # Worktree directory (added by worktree extension)
609
- ${excludePattern}
610
- `;
611
- appendFileSync(excludePath, newEntry);
612
- } catch {}
613
- }
614
-
615
- class ConfiguredRepoKeyMismatchException extends Error {
616
- constructor(winner) {
617
- super();
618
- this.message = `ConfiguredRepoKeyMismatch: expected ${winner} to resolve to WorktreeSettingsConfig`;
619
- }
620
- }
621
- function normalizeRepoReference(value) {
622
- const trimmed = value.trim();
623
- const withoutProtocol = trimmed.replace(/^ssh:\/\//, "").replace(/^https?:\/\//, "").replace(/^git@([^:]+):/, "$1/");
624
- return withoutProtocol.replace(/\.git$/, "").replace(/\/+$/, "");
625
- }
626
- function calculateSpecificity(normalizedPattern) {
627
- const segments = normalizedPattern.split("/").filter(Boolean);
628
- let score = 0;
629
- for (const segment of segments) {
630
- if (segment === "**" || segment === "*") {
631
- continue;
632
- }
633
- if (segment.includes("*")) {
634
- score += 0.5;
635
- continue;
636
- }
637
- score += 1;
638
- }
639
- return score;
640
- }
641
- function resolveTie(tiedMatches, url, repos, matchingStrategy) {
642
- const patterns = tiedMatches.map((match) => match.pattern);
643
- const strategy = matchingStrategy || "fail-on-tie";
644
- if (strategy === "fail-on-tie") {
645
- return {
646
- type: "tie-conflict",
647
- patterns,
648
- url,
649
- message: `Multiple patterns match with equal specificity:
650
- ${patterns.map((pattern) => ` - ${pattern}`).join(`
651
- `)}
652
-
653
- Refine patterns or set matchingStrategy to 'first-wins' or 'last-wins'.`
654
- };
655
- }
656
- const winner = strategy === "last-wins" ? patterns[patterns.length - 1] : patterns[0];
657
- const settings = repos.get(winner);
658
- if (!settings) {
659
- throw new Error;
660
- }
661
- return {
662
- settings,
663
- matchedPattern: winner,
664
- type: strategy
665
- };
666
- }
667
- function matchRepo(url, repos, matchStrategy) {
668
- if (!url || repos.size === 0) {
669
- return {
670
- settings: DefaultWorktreeSettings,
671
- matchedPattern: null,
672
- type: "default"
673
- };
674
- }
675
- const normalizedUrl = normalizeRepoReference(url);
676
- const scoredMatches = [];
677
- for (const [pattern, settings2] of repos.entries()) {
678
- const normalizedPattern = normalizeRepoReference(pattern);
679
- if (normalizedUrl === normalizedPattern) {
680
- return {
681
- settings: settings2,
682
- matchedPattern: pattern,
683
- type: "exact"
684
- };
685
- }
686
- if (globMatch(normalizedUrl, normalizedPattern)) {
687
- scoredMatches.push({
688
- pattern,
689
- normalizedPattern,
690
- specificity: calculateSpecificity(normalizedPattern)
691
- });
692
- }
693
- }
694
- if (scoredMatches.length === 0) {
695
- return {
696
- settings: DefaultWorktreeSettings,
697
- matchedPattern: null,
698
- type: "no-match"
699
- };
700
- }
701
- scoredMatches.sort((left, right) => right.specificity - left.specificity);
702
- const topSpecificity = scoredMatches[0].specificity;
703
- const tiedMatches = scoredMatches.filter((match) => match.specificity === topSpecificity);
704
- if (tiedMatches.length > 1) {
705
- return resolveTie(tiedMatches, normalizedUrl, repos, matchStrategy);
706
- }
707
- const winner = scoredMatches[0].pattern;
708
- const settings = repos.get(winner);
709
- if (!settings) {
710
- throw new ConfiguredRepoKeyMismatchException(winner);
711
- }
712
- return {
713
- settings,
714
- matchedPattern: winner,
715
- type: "exact"
716
- };
717
- }
718
-
719
- // src/cmds/cmdCd.ts
720
- async function cmdCd(args, ctx, deps) {
721
- const worktreeName = args.trim();
722
- if (!isGitRepo(ctx.cwd)) {
723
- ctx.ui.notify("Not in a git repository", "error");
724
- return;
725
- }
726
- const worktrees = listWorktrees(ctx.cwd);
727
- const current = deps.configService.current(ctx);
728
- if (!worktreeName) {
729
- const main = worktrees.find((worktree) => worktree.isMain);
730
- if (main) {
731
- ctx.ui.notify(`Main worktree: ${main.path}`, "info");
732
- }
733
- return;
734
- }
735
- const target = worktrees.find((worktree) => basename2(worktree.path) === worktreeName || worktree.path === worktreeName || worktree.path === join2(current.parentDir, worktreeName));
736
- if (!target) {
737
- ctx.ui.notify(`Worktree not found: ${worktreeName}`, "error");
738
- return;
739
- }
740
- ctx.ui.notify(`Worktree path: ${target.path}`, "info");
741
- }
742
-
743
- // src/cmds/cmdCreate.ts
744
- import { join as join3 } from "path";
745
-
746
- // src/cmds/shared.ts
747
- import { appendFileSync as appendFileSync2, writeFileSync } from "fs";
748
- import { spawn } from "child_process";
749
- var ANSI = {
750
- reset: "\x1B[0m",
751
- gray: "\x1B[90m",
752
- blue: "\x1B[34m",
753
- green: "\x1B[32m",
754
- red: "\x1B[31m",
755
- yellow: "\x1B[33m"
756
- };
757
- function applyCommandTemplate(template, command) {
758
- return template.replace(/\{\{cmd\}\}|\{cmd\}/g, command);
759
- }
760
- function resolveAnsiColor(colorName) {
761
- if (colorName === "dim") {
762
- return ANSI.gray;
763
- }
764
- if (colorName === "accent" || colorName === "info") {
765
- return ANSI.blue;
766
- }
767
- if (colorName === "success") {
768
- return ANSI.green;
769
- }
770
- if (colorName === "error") {
771
- return ANSI.red;
772
- }
773
- if (colorName === "warning") {
774
- return ANSI.yellow;
775
- }
776
- return "";
777
- }
778
- function colorize(text, colorName) {
779
- const ansi = resolveAnsiColor(colorName);
780
- if (!ansi) {
781
- return text;
782
- }
783
- return `${ansi}${text}${ANSI.reset}`;
784
- }
785
- function formatCommandLine(command, state, config) {
786
- if (state === "success") {
787
- return colorize(applyCommandTemplate(config.successTemplate, command), config.successColor);
788
- }
789
- if (state === "failed") {
790
- return colorize(applyCommandTemplate(config.errorTemplate, command), config.errorColor);
791
- }
792
- return colorize(applyCommandTemplate(config.pendingTemplate, command), config.pendingColor);
793
- }
794
- function toLines(text) {
795
- return text.replace(/\r/g, "").split(`
796
- `).map((line) => line.trimEnd()).filter((line) => line.length > 0);
797
- }
798
- function formatOutputLine(stream, line, state) {
799
- const prefix = stream === "stderr" ? "\u26A0" : "\u203A";
800
- if (state === "running") {
801
- return ` ${prefix} ${line}`;
802
- }
803
- return `${ANSI.gray} ${prefix} ${line}${ANSI.reset}`;
804
- }
805
- function getDisplayLines(text, maxLines) {
806
- const lines = toLines(text);
807
- if (maxLines < 0) {
808
- return lines;
809
- }
810
- if (maxLines === 0) {
811
- return [];
812
- }
813
- return lines.slice(-maxLines);
814
- }
815
- function formatCommandList(commands, states, outputs, commandDisplay, logPath, displayOutputMaxLines = 5) {
816
- const lines = ["onCreate steps:"];
817
- for (const [index, command] of commands.entries()) {
818
- const state = states[index];
819
- lines.push(formatCommandLine(command, state, commandDisplay));
820
- for (const line of getDisplayLines(outputs[index].stdout, displayOutputMaxLines)) {
821
- lines.push(formatOutputLine("stdout", line, state));
822
- }
823
- for (const line of getDisplayLines(outputs[index].stderr, displayOutputMaxLines)) {
824
- lines.push(formatOutputLine("stderr", line, state));
825
- }
826
- }
827
- if (logPath) {
828
- lines.push("");
829
- lines.push(`${ANSI.gray}log: ${logPath}${ANSI.reset}`);
830
- }
831
- return lines.join(`
832
- `);
833
- }
834
- function appendCommandLog(logPath, command, result) {
835
- const lines = [`$ ${command}`];
836
- if (result.stdout) {
837
- lines.push("[stdout]");
838
- lines.push(result.stdout.trimEnd());
839
- }
840
- if (result.stderr) {
841
- lines.push("[stderr]");
842
- lines.push(result.stderr.trimEnd());
439
+ if (result.stderr) {
440
+ lines.push("[stderr]");
441
+ lines.push(result.stderr.trimEnd());
843
442
  }
844
443
  lines.push(`[exit ${result.code}]`);
845
444
  lines.push("");
@@ -945,6 +544,411 @@ log: ${options.logPath}` : ""}`, "error");
945
544
  return { success: true, executed };
946
545
  }
947
546
 
547
+ // src/services/config/config.ts
548
+ import { createConfigService } from "@zenobius/pi-extension-config";
549
+ import { Parse as Parse2 } from "typebox/value";
550
+
551
+ // src/services/config/migrations/01-flat-single.ts
552
+ import {
553
+ Array as TypeArray2,
554
+ Object as TypeObject2,
555
+ Optional as Optional2,
556
+ String as TypeString2,
557
+ Union as Union2
558
+ } from "typebox";
559
+ import { Parse } from "typebox/value";
560
+ var LegacyOnCreateSchema = Union2([TypeString2(), TypeArray2(TypeString2())]);
561
+ var LegacyWorktreeSettingsSchema = TypeObject2({
562
+ parentDir: Optional2(TypeString2()),
563
+ onCreate: Optional2(LegacyOnCreateSchema)
564
+ }, {
565
+ additionalProperties: true
566
+ });
567
+ var LegacyConfigSchema = TypeObject2({
568
+ parentDir: Optional2(TypeString2()),
569
+ onCreate: Optional2(LegacyOnCreateSchema),
570
+ worktree: Optional2(LegacyWorktreeSettingsSchema)
571
+ }, {
572
+ additionalProperties: true
573
+ });
574
+ function toRecord(value) {
575
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
576
+ return {};
577
+ }
578
+ return { ...value };
579
+ }
580
+ function getFallbackSettings(config) {
581
+ const nested = config.worktree ?? {};
582
+ const fallback = {};
583
+ if (nested.parentDir !== undefined) {
584
+ fallback.parentDir = nested.parentDir;
585
+ } else if (config.parentDir !== undefined) {
586
+ fallback.parentDir = config.parentDir;
587
+ }
588
+ if (nested.onCreate !== undefined) {
589
+ fallback.onCreate = nested.onCreate;
590
+ } else if (config.onCreate !== undefined) {
591
+ fallback.onCreate = config.onCreate;
592
+ }
593
+ return fallback;
594
+ }
595
+ var migration = {
596
+ id: "legacy-flat-worktree-settings",
597
+ up(config) {
598
+ const record = toRecord(config);
599
+ const parsed = Parse(LegacyConfigSchema, record);
600
+ const fallback = getFallbackSettings(parsed);
601
+ const next = { ...record };
602
+ if (Object.keys(fallback).length > 0) {
603
+ next.worktree = fallback;
604
+ }
605
+ delete next.parentDir;
606
+ delete next.onCreate;
607
+ return next;
608
+ },
609
+ down(config) {
610
+ const record = toRecord(config);
611
+ const parsed = Parse(LegacyConfigSchema, record);
612
+ const worktree = toRecord(parsed.worktree);
613
+ const next = { ...record };
614
+ if (worktree.parentDir !== undefined) {
615
+ next.parentDir = worktree.parentDir;
616
+ }
617
+ if (worktree.onCreate !== undefined) {
618
+ next.onCreate = worktree.onCreate;
619
+ }
620
+ delete next.worktree;
621
+ return next;
622
+ }
623
+ };
624
+
625
+ // src/services/config/migrations/02-worktree-to-worktrees.ts
626
+ var FALLBACK_WORKTREE_PATTERN = "**";
627
+ function toRecord2(value) {
628
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
629
+ return {};
630
+ }
631
+ return { ...value };
632
+ }
633
+ function sanitizeLegacyWorktreeSettings(value) {
634
+ const source = toRecord2(value);
635
+ const next = {};
636
+ if (source.parentDir !== undefined) {
637
+ next.parentDir = source.parentDir;
638
+ }
639
+ if (source.onCreate !== undefined) {
640
+ next.onCreate = source.onCreate;
641
+ }
642
+ return next;
643
+ }
644
+ var migration2 = {
645
+ id: "legacy-worktree-to-worktrees",
646
+ up(config) {
647
+ const record = toRecord2(config);
648
+ const next = { ...record };
649
+ const topLevel = toRecord2(record);
650
+ const worktree = sanitizeLegacyWorktreeSettings(record.worktree);
651
+ const hasLegacyWorktreeSettings = Object.keys(worktree).length > 0;
652
+ if (!hasLegacyWorktreeSettings) {
653
+ return next;
654
+ }
655
+ const existingWorktrees = toRecord2(record.worktrees);
656
+ const mergedWorktrees = { ...existingWorktrees };
657
+ const existingFallback = toRecord2(mergedWorktrees[FALLBACK_WORKTREE_PATTERN]);
658
+ mergedWorktrees[FALLBACK_WORKTREE_PATTERN] = {
659
+ ...existingFallback,
660
+ ...worktree
661
+ };
662
+ next.worktrees = mergedWorktrees;
663
+ if (topLevel.logfile !== undefined) {
664
+ next.logfile = topLevel.logfile;
665
+ }
666
+ delete next.worktree;
667
+ return next;
668
+ },
669
+ down(config) {
670
+ const record = toRecord2(config);
671
+ const next = { ...record };
672
+ const topLevel = toRecord2(record);
673
+ const worktrees = toRecord2(record.worktrees);
674
+ const fallbackSettings = sanitizeLegacyWorktreeSettings(worktrees[FALLBACK_WORKTREE_PATTERN]);
675
+ if (Object.keys(fallbackSettings).length > 0) {
676
+ next.worktree = fallbackSettings;
677
+ const remaining = { ...worktrees };
678
+ delete remaining[FALLBACK_WORKTREE_PATTERN];
679
+ if (Object.keys(remaining).length > 0) {
680
+ next.worktrees = remaining;
681
+ } else {
682
+ delete next.worktrees;
683
+ }
684
+ }
685
+ if (topLevel.logfile !== undefined) {
686
+ next.logfile = topLevel.logfile;
687
+ }
688
+ return next;
689
+ }
690
+ };
691
+
692
+ // src/services/config/migrations/03-parentDir-to-worktreeRoot.ts
693
+ function toRecord3(value) {
694
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
695
+ return {};
696
+ }
697
+ return { ...value };
698
+ }
699
+ function migrateSettings(value) {
700
+ const source = toRecord3(value);
701
+ const next = {};
702
+ if (source.worktreeRoot !== undefined) {
703
+ next.worktreeRoot = source.worktreeRoot;
704
+ } else if (source.parentDir !== undefined) {
705
+ next.worktreeRoot = source.parentDir;
706
+ }
707
+ if (source.onCreate !== undefined) {
708
+ next.onCreate = source.onCreate;
709
+ }
710
+ return next;
711
+ }
712
+ var migration3 = {
713
+ id: "parentDir-to-worktreeRoot",
714
+ up(config) {
715
+ const record = toRecord3(config);
716
+ const next = { ...record };
717
+ const worktrees = toRecord3(record.worktrees);
718
+ const migratedEntries = Object.entries(worktrees).map(([pattern, value]) => [
719
+ pattern,
720
+ migrateSettings(value)
721
+ ]);
722
+ if (migratedEntries.length > 0) {
723
+ next.worktrees = Object.fromEntries(migratedEntries);
724
+ }
725
+ if (record.worktree !== undefined) {
726
+ next.worktree = migrateSettings(record.worktree);
727
+ }
728
+ return next;
729
+ },
730
+ down(config) {
731
+ const record = toRecord3(config);
732
+ const next = { ...record };
733
+ const worktrees = toRecord3(record.worktrees);
734
+ const downgradedEntries = Object.entries(worktrees).map(([pattern, value]) => {
735
+ const migrated = migrateSettings(value);
736
+ const downSettings = {};
737
+ if (migrated.worktreeRoot !== undefined) {
738
+ downSettings.parentDir = migrated.worktreeRoot;
739
+ }
740
+ if (migrated.onCreate !== undefined) {
741
+ downSettings.onCreate = migrated.onCreate;
742
+ }
743
+ return [pattern, downSettings];
744
+ });
745
+ if (downgradedEntries.length > 0) {
746
+ next.worktrees = Object.fromEntries(downgradedEntries);
747
+ }
748
+ if (record.worktree !== undefined) {
749
+ const migrated = migrateSettings(record.worktree);
750
+ const downSettings = {};
751
+ if (migrated.worktreeRoot !== undefined) {
752
+ downSettings.parentDir = migrated.worktreeRoot;
753
+ }
754
+ if (migrated.onCreate !== undefined) {
755
+ downSettings.onCreate = migrated.onCreate;
756
+ }
757
+ next.worktree = downSettings;
758
+ }
759
+ return next;
760
+ }
761
+ };
762
+
763
+ // src/services/config/migrations/04-oncreate-display-output-max-lines.ts
764
+ var DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES = 5;
765
+ function toRecord4(value) {
766
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
767
+ return {};
768
+ }
769
+ return { ...value };
770
+ }
771
+ var migration4 = {
772
+ id: "oncreate-display-output-max-lines-default",
773
+ up(config) {
774
+ const record = toRecord4(config);
775
+ if (record.onCreateDisplayOutputMaxLines !== undefined) {
776
+ return record;
777
+ }
778
+ return {
779
+ ...record,
780
+ onCreateDisplayOutputMaxLines: DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES
781
+ };
782
+ },
783
+ down(config) {
784
+ const record = toRecord4(config);
785
+ const next = { ...record };
786
+ delete next.onCreateDisplayOutputMaxLines;
787
+ return next;
788
+ }
789
+ };
790
+
791
+ // src/services/config/migrations/05-oncreate-command-display-format.ts
792
+ var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING = "[ ] {{cmd}}";
793
+ var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS = "[x] {{cmd}}";
794
+ var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR = "[ ] {{cmd}} [ERROR]";
795
+ var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR = "dim";
796
+ var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR = "success";
797
+ var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR = "error";
798
+ function toRecord5(value) {
799
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
800
+ return {};
801
+ }
802
+ return { ...value };
803
+ }
804
+ var migration5 = {
805
+ id: "oncreate-command-display-format-defaults",
806
+ up(config) {
807
+ const record = toRecord5(config);
808
+ const next = { ...record };
809
+ if (next.onCreateCmdDisplayPending === undefined) {
810
+ next.onCreateCmdDisplayPending = DEFAULT_ONCREATE_CMD_DISPLAY_PENDING;
811
+ }
812
+ if (next.onCreateCmdDisplaySuccess === undefined) {
813
+ next.onCreateCmdDisplaySuccess = DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS;
814
+ }
815
+ if (next.onCreateCmdDisplayError === undefined) {
816
+ next.onCreateCmdDisplayError = DEFAULT_ONCREATE_CMD_DISPLAY_ERROR;
817
+ }
818
+ if (next.onCreateCmdDisplayPendingColor === undefined) {
819
+ next.onCreateCmdDisplayPendingColor = DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR;
820
+ }
821
+ if (next.onCreateCmdDisplaySuccessColor === undefined) {
822
+ next.onCreateCmdDisplaySuccessColor = DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR;
823
+ }
824
+ if (next.onCreateCmdDisplayErrorColor === undefined) {
825
+ next.onCreateCmdDisplayErrorColor = DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR;
826
+ }
827
+ return next;
828
+ },
829
+ down(config) {
830
+ const record = toRecord5(config);
831
+ const next = { ...record };
832
+ delete next.onCreateCmdDisplayPending;
833
+ delete next.onCreateCmdDisplaySuccess;
834
+ delete next.onCreateCmdDisplayError;
835
+ delete next.onCreateCmdDisplayPendingColor;
836
+ delete next.onCreateCmdDisplaySuccessColor;
837
+ delete next.onCreateCmdDisplayErrorColor;
838
+ return next;
839
+ }
840
+ };
841
+
842
+ // src/services/config/config.ts
843
+ var DEFAULT_LOGFILE_TEMPLATE = "/tmp/pi-worktree-{sessionId}-{name}.log";
844
+ var DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES2 = 5;
845
+ var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING2 = "[ ] {{cmd}}";
846
+ var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS2 = "[x] {{cmd}}";
847
+ var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR2 = "[ ] {{cmd}} [ERROR]";
848
+ var DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR2 = "dim";
849
+ var DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR2 = "success";
850
+ var DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR2 = "error";
851
+ function normalizeConfiguredWorktrees(configured) {
852
+ const normalized = {
853
+ "**": { ...DefaultWorktreeSettings }
854
+ };
855
+ for (const [pattern, settings] of Object.entries(configured || {})) {
856
+ if (pattern === "**") {
857
+ normalized["**"] = {
858
+ ...normalized["**"],
859
+ ...settings
860
+ };
861
+ continue;
862
+ }
863
+ normalized[pattern] = settings;
864
+ }
865
+ return new Map(Object.entries(normalized));
866
+ }
867
+ async function createPiWorktreeConfigService() {
868
+ const parse = (value) => {
869
+ return Parse2(PiWorktreeConfigSchema, value);
870
+ };
871
+ const store = await createConfigService("pi-worktrees", {
872
+ defaults: {},
873
+ parse,
874
+ migrations: [migration, migration2, migration3, migration4, migration5]
875
+ });
876
+ await store.reload();
877
+ const save = async (data) => {
878
+ if (data.worktrees !== undefined) {
879
+ await store.set("worktrees", data.worktrees, "home");
880
+ }
881
+ if (data.matchingStrategy !== undefined) {
882
+ await store.set("matchingStrategy", data.matchingStrategy, "home");
883
+ }
884
+ if (data.logfile !== undefined) {
885
+ await store.set("logfile", data.logfile, "home");
886
+ }
887
+ if (data.onCreateDisplayOutputMaxLines !== undefined) {
888
+ await store.set("onCreateDisplayOutputMaxLines", data.onCreateDisplayOutputMaxLines, "home");
889
+ }
890
+ if (data.onCreateCmdDisplayPending !== undefined) {
891
+ await store.set("onCreateCmdDisplayPending", data.onCreateCmdDisplayPending, "home");
892
+ }
893
+ if (data.onCreateCmdDisplaySuccess !== undefined) {
894
+ await store.set("onCreateCmdDisplaySuccess", data.onCreateCmdDisplaySuccess, "home");
895
+ }
896
+ if (data.onCreateCmdDisplayError !== undefined) {
897
+ await store.set("onCreateCmdDisplayError", data.onCreateCmdDisplayError, "home");
898
+ }
899
+ if (data.onCreateCmdDisplayPendingColor !== undefined) {
900
+ await store.set("onCreateCmdDisplayPendingColor", data.onCreateCmdDisplayPendingColor, "home");
901
+ }
902
+ if (data.onCreateCmdDisplaySuccessColor !== undefined) {
903
+ await store.set("onCreateCmdDisplaySuccessColor", data.onCreateCmdDisplaySuccessColor, "home");
904
+ }
905
+ if (data.onCreateCmdDisplayErrorColor !== undefined) {
906
+ await store.set("onCreateCmdDisplayErrorColor", data.onCreateCmdDisplayErrorColor, "home");
907
+ }
908
+ await store.save("home");
909
+ };
910
+ const worktrees = normalizeConfiguredWorktrees(store.config.worktrees);
911
+ const current = (ctx) => {
912
+ const repo = getRemoteUrl(ctx.cwd);
913
+ const resolution = matchRepo(repo, worktrees, store.config.matchingStrategy);
914
+ if (resolution.type === "tie-conflict") {
915
+ throw new Error(resolution.message);
916
+ }
917
+ const settings = resolution.settings;
918
+ const project = getProjectName(ctx.cwd);
919
+ const mainWorktree = getMainWorktreePath(ctx.cwd);
920
+ const parentDir = getWorktreeParentDir(ctx.cwd, worktrees, store.config.matchingStrategy);
921
+ return {
922
+ ...settings,
923
+ repo,
924
+ project,
925
+ mainWorktree,
926
+ parentDir,
927
+ logfile: store.config.logfile ?? DEFAULT_LOGFILE_TEMPLATE,
928
+ onCreateDisplayOutputMaxLines: store.config.onCreateDisplayOutputMaxLines ?? DEFAULT_ONCREATE_DISPLAY_OUTPUT_MAX_LINES2,
929
+ onCreateCmdDisplayPending: store.config.onCreateCmdDisplayPending ?? DEFAULT_ONCREATE_CMD_DISPLAY_PENDING2,
930
+ onCreateCmdDisplaySuccess: store.config.onCreateCmdDisplaySuccess ?? DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS2,
931
+ onCreateCmdDisplayError: store.config.onCreateCmdDisplayError ?? DEFAULT_ONCREATE_CMD_DISPLAY_ERROR2,
932
+ onCreateCmdDisplayPendingColor: store.config.onCreateCmdDisplayPendingColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_PENDING_COLOR2,
933
+ onCreateCmdDisplaySuccessColor: store.config.onCreateCmdDisplaySuccessColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_SUCCESS_COLOR2,
934
+ onCreateCmdDisplayErrorColor: store.config.onCreateCmdDisplayErrorColor ?? DEFAULT_ONCREATE_CMD_DISPLAY_ERROR_COLOR2,
935
+ matchedPattern: resolution.matchedPattern
936
+ };
937
+ };
938
+ const service = {
939
+ ...store,
940
+ worktrees,
941
+ current,
942
+ save
943
+ };
944
+ return service;
945
+ }
946
+ var DefaultWorktreeSettings = {
947
+ worktreeRoot: "{{mainWorktree}}.worktrees",
948
+ onCreate: 'echo "Created {{path}}"'
949
+ };
950
+ var DefaultLogfileTemplate = DEFAULT_LOGFILE_TEMPLATE;
951
+
948
952
  // src/cmds/cmdCreate.ts
949
953
  function sanitizePathPart(value) {
950
954
  return value.replace(/[^a-zA-Z0-9._-]/g, "-");