@zq-silk/yui 0.2.0 → 0.4.2
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/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -1,15 +1,289 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { lstat, mkdir, realpath, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
const executeFile = promisify(execFile);
|
|
7
|
-
/**
|
|
7
|
+
/** A remote baseline merge stopped for an explicit Leader resolution. */
|
|
8
|
+
export class RemoteBaselineConflictError extends Error {
|
|
9
|
+
affectedPaths;
|
|
10
|
+
constructor(affectedPaths, message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "RemoteBaselineConflictError";
|
|
13
|
+
this.affectedPaths = [...affectedPaths];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The small Git boundary used by project registration and Task workspaces. */
|
|
8
17
|
export class NodeGitWorkspace {
|
|
18
|
+
async findCommitWithSameTreeInHistory(input) {
|
|
19
|
+
const source = (await this.inspect(input.repositoryPath, input.sourceCommit)).baseCommit;
|
|
20
|
+
const history = (await this.inspect(input.repositoryPath, input.historyHead)).baseCommit;
|
|
21
|
+
if (await this.isAncestor(input.repositoryPath, source, history))
|
|
22
|
+
return source;
|
|
23
|
+
const sourceTree = await gitLine([
|
|
24
|
+
"-C", input.repositoryPath,
|
|
25
|
+
"rev-parse", "--verify", "--end-of-options", `${source}^{tree}`
|
|
26
|
+
]);
|
|
27
|
+
const pageSize = 1000;
|
|
28
|
+
for (let skip = 0;; skip += pageSize) {
|
|
29
|
+
const output = await git([
|
|
30
|
+
"-C", input.repositoryPath,
|
|
31
|
+
"log", `--max-count=${pageSize}`, `--skip=${skip}`,
|
|
32
|
+
"--format=%H%x09%T", history
|
|
33
|
+
]);
|
|
34
|
+
const lines = output.trimEnd().split("\n").filter(Boolean);
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const [commit, tree, ...extra] = line.split("\t");
|
|
37
|
+
if (extra.length > 0 || commit === undefined || tree === undefined) {
|
|
38
|
+
throw new Error("Git returned invalid publication history.");
|
|
39
|
+
}
|
|
40
|
+
if (tree === sourceTree)
|
|
41
|
+
return commit;
|
|
42
|
+
}
|
|
43
|
+
if (lines.length < pageSize)
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the configured Project branch directly from its remote. This is
|
|
49
|
+
* deliberately read-only: unlike `refresh`, it never advances the stable
|
|
50
|
+
* Project checkout or changes its refs.
|
|
51
|
+
*/
|
|
52
|
+
async resolveRemoteHead(input) {
|
|
53
|
+
const remote = safeRemote(input.remoteUrl);
|
|
54
|
+
const configuredBranch = await safeFetchBranch(input.branch);
|
|
55
|
+
const branch = configuredBranch === "HEAD"
|
|
56
|
+
? await resolveRemoteHeadBranch(remote)
|
|
57
|
+
: configuredBranch;
|
|
58
|
+
const commit = await resolveRemoteBranchCommit(remote, branch);
|
|
59
|
+
return { branch, commit };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Merge a remote Project baseline into a clean, managed worktree. The
|
|
63
|
+
* fetch is performed from that worktree and the stable checkout is never
|
|
64
|
+
* touched. Fast-forward updates are preferred; a diverged baseline gets a
|
|
65
|
+
* deterministic merge commit so the caller can run its normal Integration
|
|
66
|
+
* checks and CAS the target ref.
|
|
67
|
+
*/
|
|
68
|
+
async mergeRemoteIntoWorktree(input) {
|
|
69
|
+
const initial = await this.inspect(input.repositoryPath, "HEAD");
|
|
70
|
+
if (!await this.isClean(initial.root)) {
|
|
71
|
+
throw new Error("Managed Task/Integration worktree must be clean before remote merge.");
|
|
72
|
+
}
|
|
73
|
+
const fetched = await this.#fetchRemoteHeadIntoWorktree(input);
|
|
74
|
+
const fetchedCommit = fetched.commit;
|
|
75
|
+
const fetchedRef = fetched.fetchedRef;
|
|
76
|
+
try {
|
|
77
|
+
if (fetchedCommit === initial.baseCommit) {
|
|
78
|
+
return { fromCommit: initial.baseCommit, toCommit: fetchedCommit, changed: false };
|
|
79
|
+
}
|
|
80
|
+
if (await gitSucceeds([
|
|
81
|
+
"-C", initial.root,
|
|
82
|
+
"merge-base", "--is-ancestor", fetchedCommit, initial.baseCommit
|
|
83
|
+
])) {
|
|
84
|
+
// The Task already contains the current remote baseline.
|
|
85
|
+
return { fromCommit: initial.baseCommit, toCommit: initial.baseCommit, changed: false };
|
|
86
|
+
}
|
|
87
|
+
if (await gitSucceeds([
|
|
88
|
+
"-C", initial.root,
|
|
89
|
+
"merge-base", "--is-ancestor", initial.baseCommit, fetchedCommit
|
|
90
|
+
])) {
|
|
91
|
+
await git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedRef]);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
try {
|
|
95
|
+
await git([
|
|
96
|
+
"-C", initial.root,
|
|
97
|
+
"-c", "user.name=Yui",
|
|
98
|
+
"-c", "user.email=yui@local",
|
|
99
|
+
"merge", "--no-edit", "--no-ff", fetchedRef
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const affected = (await git([
|
|
104
|
+
"-C", initial.root,
|
|
105
|
+
"diff", "--name-only", "--diff-filter=U"
|
|
106
|
+
])).trim().split("\n").filter(Boolean);
|
|
107
|
+
const suffix = affected.length === 0 ? "" : ` (${affected.join(", ")})`;
|
|
108
|
+
throw new RemoteBaselineConflictError(affected, `Remote baseline merge conflicts in managed worktree${suffix}; resolve it in the Integration workspace.`, { cause: error });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mergedCommit = (await gitLine([
|
|
112
|
+
"-C", initial.root,
|
|
113
|
+
"rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
|
|
114
|
+
])).toLowerCase();
|
|
115
|
+
if (!await this.isClean(initial.root)) {
|
|
116
|
+
throw new Error("Remote baseline merge left the managed worktree dirty.");
|
|
117
|
+
}
|
|
118
|
+
return { fromCommit: initial.baseCommit, toCommit: mergedCommit, changed: true };
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await gitSucceeds(["-C", initial.root, "update-ref", "-d", fetchedRef]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Fetch a remote branch into a managed worktree without changing HEAD. */
|
|
125
|
+
async fetchRemoteHeadIntoWorktree(input) {
|
|
126
|
+
const fetched = await this.#fetchRemoteHeadIntoWorktree(input);
|
|
127
|
+
try {
|
|
128
|
+
return { branch: fetched.branch, commit: fetched.commit };
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await gitSucceeds(["-C", input.repositoryPath, "update-ref", "-d", fetched.fetchedRef]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async #fetchRemoteHeadIntoWorktree(input) {
|
|
135
|
+
const repositoryPath = (await this.inspect(input.repositoryPath, "HEAD")).root;
|
|
136
|
+
const remote = safeRemote(input.remoteUrl);
|
|
137
|
+
const configuredBranch = await safeFetchBranch(input.branch);
|
|
138
|
+
const branch = configuredBranch === "HEAD"
|
|
139
|
+
? await resolveRemoteHeadBranch(remote)
|
|
140
|
+
: configuredBranch;
|
|
141
|
+
const fetchedRef = remoteBaselineRef(remote, branch);
|
|
142
|
+
try {
|
|
143
|
+
await git([
|
|
144
|
+
"-C", repositoryPath,
|
|
145
|
+
"fetch", "--no-tags", "--no-write-fetch-head",
|
|
146
|
+
remote,
|
|
147
|
+
`refs/heads/${branch}:${fetchedRef}`
|
|
148
|
+
]);
|
|
149
|
+
const commit = await resolveFetchedCommit(repositoryPath, fetchedRef);
|
|
150
|
+
return { branch, commit, fetchedRef };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
await gitSucceeds(["-C", repositoryPath, "update-ref", "-d", fetchedRef]);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async refresh(input) {
|
|
158
|
+
const remote = safeRemote(input.remoteUrl);
|
|
159
|
+
const configuredStableRef = await safeFetchBranch(input.stableRef);
|
|
160
|
+
const initial = await this.inspect(input.repositoryPath, "HEAD");
|
|
161
|
+
await this.#assertRefreshCheckout(initial.root, configuredStableRef, initial.baseCommit);
|
|
162
|
+
const stableBranch = configuredStableRef === "HEAD"
|
|
163
|
+
? await resolveRemoteHeadBranch(remote)
|
|
164
|
+
: configuredStableRef;
|
|
165
|
+
if (stableBranch !== configuredStableRef) {
|
|
166
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
|
|
167
|
+
}
|
|
168
|
+
const stableRemoteRef = `refs/heads/${stableBranch}`;
|
|
169
|
+
await git(["-C", initial.root, "fetch", "--no-tags", remote, stableRemoteRef]);
|
|
170
|
+
const fetchedCommit = (await gitLine([
|
|
171
|
+
"-C", initial.root,
|
|
172
|
+
"rev-parse", "--verify", "--end-of-options", "FETCH_HEAD^{commit}"
|
|
173
|
+
])).toLowerCase();
|
|
174
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
|
|
175
|
+
if (fetchedCommit === initial.baseCommit) {
|
|
176
|
+
return {
|
|
177
|
+
fromCommit: initial.baseCommit,
|
|
178
|
+
toCommit: fetchedCommit,
|
|
179
|
+
changed: false
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (!await gitSucceeds([
|
|
183
|
+
"-C", initial.root,
|
|
184
|
+
"merge-base", "--is-ancestor", initial.baseCommit, fetchedCommit
|
|
185
|
+
])) {
|
|
186
|
+
throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
|
|
187
|
+
}
|
|
188
|
+
await git([
|
|
189
|
+
"-C", initial.root,
|
|
190
|
+
"merge", "--ff-only", "--no-edit", fetchedCommit
|
|
191
|
+
]);
|
|
192
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
|
|
193
|
+
return {
|
|
194
|
+
fromCommit: initial.baseCommit,
|
|
195
|
+
toCommit: fetchedCommit,
|
|
196
|
+
changed: true
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Fetch a Project's configured development branch into a temporary ref and
|
|
201
|
+
* return the commit confirmed by that fetch. The stable checkout is only
|
|
202
|
+
* inspected; it is never checked out, reset, rebased, or fast-forwarded.
|
|
203
|
+
*/
|
|
204
|
+
async resolveRemoteBaseline(input) {
|
|
205
|
+
const remote = safeRemote(input.remoteUrl);
|
|
206
|
+
const configuredRef = await safeFetchBranch(input.developmentRef);
|
|
207
|
+
const branch = configuredRef === "HEAD"
|
|
208
|
+
? await resolveRemoteHeadBranch(remote)
|
|
209
|
+
: configuredRef;
|
|
210
|
+
const repository = await this.inspect(input.repositoryPath, "HEAD");
|
|
211
|
+
const temporaryRef = remoteBaselineRef(remote, branch);
|
|
212
|
+
try {
|
|
213
|
+
await git([
|
|
214
|
+
"-C", repository.root,
|
|
215
|
+
"fetch", "--no-tags", "--no-write-fetch-head",
|
|
216
|
+
remote,
|
|
217
|
+
`refs/heads/${branch}:${temporaryRef}`
|
|
218
|
+
]);
|
|
219
|
+
const fetchedCommit = await gitLine([
|
|
220
|
+
"-C", repository.root,
|
|
221
|
+
"rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
|
|
222
|
+
]);
|
|
223
|
+
const advertisedCommit = await resolveRemoteBranchCommit(remote, branch);
|
|
224
|
+
if (fetchedCommit.toLowerCase() !== advertisedCommit) {
|
|
225
|
+
throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
branch,
|
|
229
|
+
commit: fetchedCommit.toLowerCase()
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
// A temporary namespace keeps the fetch independent from the stable
|
|
234
|
+
// branch and FETCH_HEAD. Cleanup is best effort so the original fetch
|
|
235
|
+
// or consistency error remains the actionable diagnosis.
|
|
236
|
+
await gitSucceeds([
|
|
237
|
+
"-C", repository.root,
|
|
238
|
+
"update-ref", "-d", temporaryRef
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async clone(input) {
|
|
243
|
+
const remote = safeRemote(input.remoteUrl);
|
|
244
|
+
const destination = resolve(requireText(input.destination, "Project destination"));
|
|
245
|
+
if (await pathKind(destination) !== undefined) {
|
|
246
|
+
throw new Error(`Project destination already exists: ${destination}.`);
|
|
247
|
+
}
|
|
248
|
+
await canonicalContainer(dirname(destination), true);
|
|
249
|
+
const branch = input.branch === undefined ? undefined : safeRef(input.branch);
|
|
250
|
+
try {
|
|
251
|
+
await git([
|
|
252
|
+
"clone",
|
|
253
|
+
...(branch === undefined ? [] : ["--branch", branch]),
|
|
254
|
+
"--",
|
|
255
|
+
remote,
|
|
256
|
+
destination
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
await rm(destination, { recursive: true, force: true });
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
return this.inspect(destination, "HEAD");
|
|
264
|
+
}
|
|
265
|
+
async ensureLocalBranch(repositoryPath, branch) {
|
|
266
|
+
try {
|
|
267
|
+
return await this.inspect(repositoryPath, branch);
|
|
268
|
+
}
|
|
269
|
+
catch (originalError) {
|
|
270
|
+
const root = (await this.inspect(repositoryPath, "HEAD")).root;
|
|
271
|
+
const name = safeRef(branch);
|
|
272
|
+
if (!await gitSucceeds(["check-ref-format", "--branch", name]))
|
|
273
|
+
throw originalError;
|
|
274
|
+
const remoteRef = `refs/remotes/origin/${name}`;
|
|
275
|
+
if (!await gitSucceeds([
|
|
276
|
+
"-C", root, "show-ref", "--verify", "--quiet", remoteRef
|
|
277
|
+
]))
|
|
278
|
+
throw originalError;
|
|
279
|
+
await git(["-C", root, "branch", "--", name, remoteRef]);
|
|
280
|
+
return this.inspect(root, name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
9
283
|
async inspect(repositoryPath, baseRef = "HEAD") {
|
|
10
|
-
const requested = await canonicalDirectory(repositoryPath, "
|
|
284
|
+
const requested = await canonicalDirectory(repositoryPath, "Project");
|
|
11
285
|
const ref = safeRef(baseRef);
|
|
12
|
-
const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "
|
|
286
|
+
const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "Project root");
|
|
13
287
|
const gitDirectory = await canonicalDirectory(await gitLine([
|
|
14
288
|
"-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"
|
|
15
289
|
]), "Git common directory");
|
|
@@ -19,11 +293,115 @@ export class NodeGitWorkspace {
|
|
|
19
293
|
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i.test(baseCommit)) {
|
|
20
294
|
throw new Error("Git returned an invalid base commit.");
|
|
21
295
|
}
|
|
22
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
root,
|
|
298
|
+
gitDirectory,
|
|
299
|
+
baseRef: ref,
|
|
300
|
+
baseCommit: baseCommit.toLowerCase()
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async isAncestor(repositoryPath, ancestor, descendant) {
|
|
304
|
+
const root = (await this.inspect(repositoryPath)).root;
|
|
305
|
+
return gitSucceeds([
|
|
306
|
+
"-C", root,
|
|
307
|
+
"merge-base", "--is-ancestor",
|
|
308
|
+
safeRef(ancestor),
|
|
309
|
+
safeRef(descendant)
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
async headRef(repositoryPath) {
|
|
313
|
+
const requested = await canonicalDirectory(repositoryPath, "Project");
|
|
314
|
+
return gitLine(["-C", requested, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
315
|
+
}
|
|
316
|
+
async isClean(repositoryPath) {
|
|
317
|
+
const root = (await this.inspect(repositoryPath)).root;
|
|
318
|
+
const status = await git([
|
|
319
|
+
"-C", root, "status", "--porcelain=v1", "--untracked-files=all"
|
|
320
|
+
]);
|
|
321
|
+
return status.length === 0;
|
|
322
|
+
}
|
|
323
|
+
async mergeWorktree(input) {
|
|
324
|
+
const target = await canonicalDirectory(input.targetPath, "Merge target");
|
|
325
|
+
const sources = input.sourceRefs.map((source) => safeRef(source));
|
|
326
|
+
if (sources.length === 0) {
|
|
327
|
+
throw new Error(`Git merge requires at least one source ref: ${target}.`);
|
|
328
|
+
}
|
|
329
|
+
if (!await this.isClean(target)) {
|
|
330
|
+
throw new Error(`Git merge target must be clean: ${target}.`);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
// Merge all selected Lane heads in one Git transaction. A conflict in
|
|
334
|
+
// any Lane therefore aborts the complete Candidate materialization
|
|
335
|
+
// instead of leaving an earlier Lane merged into the WorkItem target.
|
|
336
|
+
await git(["-C", target, "merge", "--no-edit", "--no-ff", ...sources]);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
try {
|
|
340
|
+
await git(["-C", target, "merge", "--abort"]);
|
|
341
|
+
}
|
|
342
|
+
catch (abortError) {
|
|
343
|
+
throw new Error(`Git merge failed and conflict cleanup could not be completed for ${target}.`, { cause: abortError });
|
|
344
|
+
}
|
|
345
|
+
throw new Error(`Git merge failed for ${target} from ${sources.join(", ")}; Candidate materialization was not completed.`, { cause: error });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async resetWorktree(input) {
|
|
349
|
+
const target = await canonicalDirectory(input.targetPath, "Reset target");
|
|
350
|
+
const expectedHead = safeRef(input.expectedHead);
|
|
351
|
+
const restoreHead = safeRef(input.restoreHead);
|
|
352
|
+
if (!await this.isClean(target)) {
|
|
353
|
+
throw new Error(`Git reset target must be clean: ${target}.`);
|
|
354
|
+
}
|
|
355
|
+
const currentHead = (await this.inspect(target, "HEAD")).baseCommit;
|
|
356
|
+
if (currentHead !== expectedHead) {
|
|
357
|
+
throw new Error(`Git reset target changed before compensation: ${target} (${currentHead}).`);
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await git(["-C", target, "reset", "--hard", restoreHead]);
|
|
361
|
+
const restoredHead = (await this.inspect(target, "HEAD")).baseCommit;
|
|
362
|
+
if (restoredHead !== restoreHead || !await this.isClean(target)) {
|
|
363
|
+
throw new Error(`Git reset target did not restore its exact head: ${target}.`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
throw new Error(`Git reset compensation failed for ${target}; Candidate materialization remains retained for diagnosis.`, { cause: error });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async #assertRefreshCheckout(repositoryPath, stableRef, expectedCommit) {
|
|
371
|
+
if (stableRef !== "HEAD") {
|
|
372
|
+
const currentBranch = await this.headRef(repositoryPath);
|
|
373
|
+
if (currentBranch !== stableRef) {
|
|
374
|
+
const found = currentBranch === "HEAD" ? "detached HEAD" : currentBranch;
|
|
375
|
+
throw new Error(`Project checkout must be on its stable branch: expected ${stableRef}, found ${found}.`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const head = await this.inspect(repositoryPath, "HEAD");
|
|
379
|
+
if (head.baseCommit !== expectedCommit) {
|
|
380
|
+
throw new Error("Project checkout changed while it was being refreshed.");
|
|
381
|
+
}
|
|
382
|
+
if (!await this.isClean(head.root)) {
|
|
383
|
+
throw new Error("Project checkout must be clean before it can be refreshed.");
|
|
384
|
+
}
|
|
23
385
|
}
|
|
24
386
|
async ensureWorktree(input) {
|
|
387
|
+
return this.#ensureManagedWorktree({
|
|
388
|
+
repositoryPath: input.repositoryPath,
|
|
389
|
+
container: input.container,
|
|
390
|
+
identity: worktreeIdentity(input.taskId, input.roleName),
|
|
391
|
+
baseRef: input.baseRef
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async ensureIntegrationWorktree(input) {
|
|
395
|
+
return this.#ensureManagedWorktree({
|
|
396
|
+
repositoryPath: input.repositoryPath,
|
|
397
|
+
container: input.container,
|
|
398
|
+
identity: integrationWorktreeIdentity(input.taskId, input.integrationId),
|
|
399
|
+
baseRef: input.baseRef
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async #ensureManagedWorktree(input) {
|
|
25
403
|
const container = await canonicalContainer(input.container, true);
|
|
26
|
-
const identity =
|
|
404
|
+
const identity = input.identity;
|
|
27
405
|
const path = managedPath(container, identity.directory);
|
|
28
406
|
const kind = await pathKind(path);
|
|
29
407
|
if (kind === "symlink")
|
|
@@ -31,31 +409,55 @@ export class NodeGitWorkspace {
|
|
|
31
409
|
if (kind === "directory") {
|
|
32
410
|
// A worktree already created before a crash remains usable even if the
|
|
33
411
|
// original base branch/tag is later deleted.
|
|
34
|
-
const
|
|
35
|
-
await assertOwnedWorktree(
|
|
412
|
+
const project = await this.inspect(input.repositoryPath);
|
|
413
|
+
await assertOwnedWorktree(project, container, path);
|
|
36
414
|
await assertExpectedBranch(path, identity.branch);
|
|
37
415
|
const head = await gitLine([
|
|
38
416
|
"-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
|
|
39
417
|
]);
|
|
40
418
|
return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
|
|
41
419
|
}
|
|
42
|
-
const
|
|
420
|
+
const project = await this.inspect(input.repositoryPath, input.baseRef);
|
|
43
421
|
await canonicalContainer(dirname(path), true);
|
|
44
422
|
const branchRef = `refs/heads/${identity.branch}`;
|
|
45
423
|
const branchExists = await gitSucceeds([
|
|
46
|
-
"-C",
|
|
424
|
+
"-C", project.root, "show-ref", "--verify", "--quiet", branchRef
|
|
47
425
|
]);
|
|
48
426
|
await git(branchExists
|
|
49
|
-
? ["-C",
|
|
427
|
+
? ["-C", project.root, "worktree", "add", "--", path, identity.branch]
|
|
50
428
|
: [
|
|
51
|
-
"-C",
|
|
52
|
-
"--", path,
|
|
429
|
+
"-C", project.root, "worktree", "add", "-b", identity.branch,
|
|
430
|
+
"--", path, project.baseCommit
|
|
53
431
|
]);
|
|
54
|
-
await assertOwnedWorktree(
|
|
432
|
+
await assertOwnedWorktree(project, container, path);
|
|
55
433
|
await assertExpectedBranch(path, identity.branch);
|
|
56
|
-
|
|
434
|
+
const head = await gitLine([
|
|
435
|
+
"-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
|
|
436
|
+
]);
|
|
437
|
+
return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
|
|
57
438
|
}
|
|
58
439
|
async removeWorktree(input) {
|
|
440
|
+
const state = await this.inspectWorktree(input);
|
|
441
|
+
if (state === "dirty")
|
|
442
|
+
return state;
|
|
443
|
+
const container = resolve(input.container);
|
|
444
|
+
const identity = worktreeIdentity(input.taskId, input.roleName);
|
|
445
|
+
const path = managedPath(container, identity.directory);
|
|
446
|
+
const project = await this.inspect(input.repositoryPath);
|
|
447
|
+
if (state === "missing") {
|
|
448
|
+
if (input.deleteBranch === true) {
|
|
449
|
+
await git(["-C", project.root, "worktree", "prune"]);
|
|
450
|
+
await deleteBranchIfPresent(project.root, identity.branch);
|
|
451
|
+
}
|
|
452
|
+
return state;
|
|
453
|
+
}
|
|
454
|
+
await git(["-C", project.root, "worktree", "remove", "--", path]);
|
|
455
|
+
if (input.deleteBranch === true) {
|
|
456
|
+
await deleteBranchIfPresent(project.root, identity.branch);
|
|
457
|
+
}
|
|
458
|
+
return "removed";
|
|
459
|
+
}
|
|
460
|
+
async inspectWorktree(input) {
|
|
59
461
|
const container = resolve(input.container);
|
|
60
462
|
const path = managedPath(container, worktreeIdentity(input.taskId, input.roleName).directory);
|
|
61
463
|
const kind = await pathKind(path);
|
|
@@ -64,15 +466,67 @@ export class NodeGitWorkspace {
|
|
|
64
466
|
if (kind === "symlink")
|
|
65
467
|
throw new Error("Managed worktree path must not be a symbolic link.");
|
|
66
468
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
67
|
-
const
|
|
68
|
-
await assertOwnedWorktree(
|
|
469
|
+
const project = await this.inspect(input.repositoryPath);
|
|
470
|
+
await assertOwnedWorktree(project, canonicalContainerPath, path);
|
|
69
471
|
const porcelain = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
472
|
+
return porcelain.length > 0 ? "dirty" : "clean";
|
|
473
|
+
}
|
|
474
|
+
async removeIntegrationWorktree(input) {
|
|
475
|
+
return this.#removeManagedWorktree({
|
|
476
|
+
repositoryPath: input.repositoryPath,
|
|
477
|
+
container: input.container,
|
|
478
|
+
identity: integrationWorktreeIdentity(input.taskId, input.integrationId),
|
|
479
|
+
discardChanges: input.discardChanges
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async #removeManagedWorktree(input) {
|
|
483
|
+
const container = resolve(input.container);
|
|
484
|
+
const path = managedPath(container, input.identity.directory);
|
|
485
|
+
const kind = await pathKind(path);
|
|
486
|
+
if (kind === "symlink")
|
|
487
|
+
throw new Error("Managed worktree path must not be a symbolic link.");
|
|
488
|
+
const repository = await this.inspect(input.repositoryPath);
|
|
489
|
+
if (kind === "directory") {
|
|
490
|
+
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
491
|
+
await assertOwnedWorktree(repository, canonicalContainerPath, path);
|
|
492
|
+
await assertExpectedBranch(path, input.identity.branch);
|
|
493
|
+
if (input.discardChanges === true) {
|
|
494
|
+
await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
const porcelain = await git([
|
|
498
|
+
"-C", path, "status", "--porcelain=v1", "--untracked-files=all"
|
|
499
|
+
]);
|
|
500
|
+
if (porcelain.length > 0)
|
|
501
|
+
return "dirty";
|
|
502
|
+
if (input.expectedBaseCommit !== undefined
|
|
503
|
+
&& input.allowCommittedChanges !== true) {
|
|
504
|
+
const head = await gitLine([
|
|
505
|
+
"-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
|
|
506
|
+
]);
|
|
507
|
+
if (head.toLowerCase() !== input.expectedBaseCommit.toLowerCase())
|
|
508
|
+
return "dirty";
|
|
509
|
+
}
|
|
510
|
+
await git(["-C", repository.root, "worktree", "remove", "--", path]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const branchRef = `refs/heads/${input.identity.branch}`;
|
|
514
|
+
if (await gitSucceeds([
|
|
515
|
+
"-C", repository.root, "show-ref", "--verify", "--quiet", branchRef
|
|
516
|
+
])) {
|
|
517
|
+
await git(["-C", repository.root, "branch", "-D", "--", input.identity.branch]);
|
|
518
|
+
}
|
|
519
|
+
return kind === "directory" ? "removed" : "missing";
|
|
74
520
|
}
|
|
75
521
|
}
|
|
522
|
+
async function deleteBranchIfPresent(repositoryRoot, branch) {
|
|
523
|
+
const branchRef = `refs/heads/${branch}`;
|
|
524
|
+
if (!await gitSucceeds([
|
|
525
|
+
"-C", repositoryRoot, "show-ref", "--verify", "--quiet", branchRef
|
|
526
|
+
]))
|
|
527
|
+
return;
|
|
528
|
+
await git(["-C", repositoryRoot, "branch", "-D", "--", branch]);
|
|
529
|
+
}
|
|
76
530
|
export function worktreeIdentity(taskId, roleName) {
|
|
77
531
|
const taskKey = safeIdentity(taskId, "Task id");
|
|
78
532
|
const roleKey = safeIdentity(roleName, "Role name");
|
|
@@ -81,6 +535,14 @@ export function worktreeIdentity(taskId, roleName) {
|
|
|
81
535
|
branch: `yui/${gitRefSegment(taskKey)}/${gitRefSegment(roleKey)}`
|
|
82
536
|
};
|
|
83
537
|
}
|
|
538
|
+
export function integrationWorktreeIdentity(taskId, integrationId) {
|
|
539
|
+
const taskKey = safeIdentity(taskId, "Task id");
|
|
540
|
+
const integrationKey = safeIdentity(integrationId, "Integration Attempt id");
|
|
541
|
+
return {
|
|
542
|
+
directory: join(taskKey, "integrations", integrationKey),
|
|
543
|
+
branch: `yui/${gitRefSegment(taskKey)}/integration/${gitRefSegment(integrationKey)}`
|
|
544
|
+
};
|
|
545
|
+
}
|
|
84
546
|
function gitRefSegment(identity) {
|
|
85
547
|
if (/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(identity)
|
|
86
548
|
&& !identity.includes("..")
|
|
@@ -97,7 +559,7 @@ async function assertExpectedBranch(path, expected) {
|
|
|
97
559
|
throw new Error(`Managed worktree is on an unexpected branch: ${branch}.`);
|
|
98
560
|
}
|
|
99
561
|
}
|
|
100
|
-
async function assertOwnedWorktree(
|
|
562
|
+
async function assertOwnedWorktree(project, container, path) {
|
|
101
563
|
const canonicalPath = await canonicalDirectory(path, "Managed worktree");
|
|
102
564
|
assertContained(container, canonicalPath);
|
|
103
565
|
if (canonicalPath !== path)
|
|
@@ -106,8 +568,8 @@ async function assertOwnedWorktree(repository, container, path) {
|
|
|
106
568
|
if (root !== path)
|
|
107
569
|
throw new Error("Managed worktree root does not match its deterministic path.");
|
|
108
570
|
const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
|
|
109
|
-
if (common !==
|
|
110
|
-
throw new Error("Managed worktree belongs to another
|
|
571
|
+
if (common !== project.gitDirectory) {
|
|
572
|
+
throw new Error("Managed worktree belongs to another project.");
|
|
111
573
|
}
|
|
112
574
|
}
|
|
113
575
|
async function canonicalContainer(path, create) {
|
|
@@ -198,6 +660,98 @@ function safeRef(value) {
|
|
|
198
660
|
throw new Error("Git base ref is invalid.");
|
|
199
661
|
return ref;
|
|
200
662
|
}
|
|
663
|
+
async function safeFetchBranch(value) {
|
|
664
|
+
const configuredRef = safeRef(value);
|
|
665
|
+
if (configuredRef === "HEAD")
|
|
666
|
+
return configuredRef;
|
|
667
|
+
const branchPrefix = "refs/heads/";
|
|
668
|
+
const branch = configuredRef.startsWith(branchPrefix)
|
|
669
|
+
? configuredRef.slice(branchPrefix.length)
|
|
670
|
+
: configuredRef;
|
|
671
|
+
if (!await gitSucceeds(["check-ref-format", "--branch", branch])) {
|
|
672
|
+
throw new Error("Git stable branch is invalid.");
|
|
673
|
+
}
|
|
674
|
+
return branch;
|
|
675
|
+
}
|
|
676
|
+
async function resolveRemoteHeadBranch(remote) {
|
|
677
|
+
let output;
|
|
678
|
+
try {
|
|
679
|
+
output = await git(["ls-remote", "--symref", remote, "HEAD"]);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
const detail = error instanceof Error ? error.message : "Git command failed.";
|
|
683
|
+
throw new Error(`Project remote HEAD could not be resolved: ${detail}`, { cause: error });
|
|
684
|
+
}
|
|
685
|
+
const symbolicLines = output.split("\n").filter((line) => line.startsWith("ref: "));
|
|
686
|
+
const prefix = "ref: refs/heads/";
|
|
687
|
+
const suffix = "\tHEAD";
|
|
688
|
+
const symbolic = symbolicLines.length === 1 ? symbolicLines[0] : "";
|
|
689
|
+
if (!symbolic.startsWith(prefix) || !symbolic.endsWith(suffix)) {
|
|
690
|
+
throw invalidRemoteHead();
|
|
691
|
+
}
|
|
692
|
+
const branch = symbolic.slice(prefix.length, -suffix.length);
|
|
693
|
+
if (branch === "HEAD")
|
|
694
|
+
throw invalidRemoteHead();
|
|
695
|
+
try {
|
|
696
|
+
return await safeFetchBranch(branch);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
throw invalidRemoteHead();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function resolveRemoteBranchCommit(remote, branch) {
|
|
703
|
+
const target = `refs/heads/${branch}`;
|
|
704
|
+
let output;
|
|
705
|
+
try {
|
|
706
|
+
output = await git(["ls-remote", "--refs", remote, target]);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
const detail = error instanceof Error ? error.message : "Git command failed.";
|
|
710
|
+
throw new Error(`Project remote development branch could not be resolved: ${branch}: ${detail}`, { cause: error });
|
|
711
|
+
}
|
|
712
|
+
const matches = output
|
|
713
|
+
.split("\n")
|
|
714
|
+
.map((line) => line.trim())
|
|
715
|
+
.filter((line) => line.length > 0)
|
|
716
|
+
.map((line) => line.split(/\s+/u))
|
|
717
|
+
.filter(([commit, ref]) => ref === target && commit !== undefined);
|
|
718
|
+
if (matches.length !== 1) {
|
|
719
|
+
throw new Error(`Project remote development branch could not be resolved: ${branch}.`);
|
|
720
|
+
}
|
|
721
|
+
const commit = matches[0][0];
|
|
722
|
+
if (!isCommit(commit)) {
|
|
723
|
+
throw new Error(`Project remote development branch returned an invalid commit: ${branch}.`);
|
|
724
|
+
}
|
|
725
|
+
return commit.toLowerCase();
|
|
726
|
+
}
|
|
727
|
+
async function resolveFetchedCommit(repositoryPath, fetchedRef) {
|
|
728
|
+
const commit = (await gitLine([
|
|
729
|
+
"-C", repositoryPath,
|
|
730
|
+
"rev-parse", "--verify", "--end-of-options", `${fetchedRef}^{commit}`
|
|
731
|
+
])).toLowerCase();
|
|
732
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
|
|
733
|
+
throw new Error("Git returned an invalid fetched remote commit.");
|
|
734
|
+
}
|
|
735
|
+
return commit;
|
|
736
|
+
}
|
|
737
|
+
function remoteBaselineRef(remote, branch) {
|
|
738
|
+
const identity = `${remote}\0${branch}\0${process.pid}\0${Date.now()}\0${Math.random()}`;
|
|
739
|
+
const digest = createHash("sha256").update(identity).digest("hex");
|
|
740
|
+
return `refs/yui/task-baselines/${digest}`;
|
|
741
|
+
}
|
|
742
|
+
function invalidRemoteHead() {
|
|
743
|
+
return new Error("Project remote HEAD must identify a valid symbolic branch under refs/heads/.");
|
|
744
|
+
}
|
|
745
|
+
function isCommit(value) {
|
|
746
|
+
return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
|
|
747
|
+
}
|
|
748
|
+
function safeRemote(value) {
|
|
749
|
+
const remote = requireText(value, "Git remote URL");
|
|
750
|
+
if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
|
|
751
|
+
throw new Error("Git remote URL is invalid.");
|
|
752
|
+
}
|
|
753
|
+
return remote;
|
|
754
|
+
}
|
|
201
755
|
function requireText(value, label) {
|
|
202
756
|
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
203
757
|
throw new Error(`${label} is required.`);
|