@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/README.md +3 -2
- package/dist/index.js +723 -719
- package/dist/services/git.d.ts +0 -4
- package/package.json +7 -3
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/
|
|
71
|
-
|
|
72
|
-
|
|
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/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
173
|
+
return worktrees;
|
|
221
174
|
}
|
|
222
|
-
function
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
198
|
+
return `${mainWorktree}.worktrees`;
|
|
234
199
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
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 (
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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 {
|
|
270
|
+
return {
|
|
271
|
+
settings,
|
|
272
|
+
matchedPattern: winner,
|
|
273
|
+
type: strategy
|
|
274
|
+
};
|
|
326
275
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 (
|
|
348
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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/
|
|
460
|
-
|
|
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/
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
|
484
|
-
|
|
485
|
-
return
|
|
486
|
-
}
|
|
487
|
-
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
503
|
-
return
|
|
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
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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 (
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
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
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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, "-");
|