@zq-silk/yui 0.6.0 → 0.6.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/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { dataError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { createCapabilityGrant, revokeGrant } from "../grant/capabilityGrant.js";
|
|
4
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
5
|
+
import { formatTimestamp } from "../output/timePresentation.js";
|
|
6
|
+
import { isCurrentGlobalOperator } from "./taskInputCommands.js";
|
|
7
|
+
const CEILINGS = new Set(["none", "reversible", "irreversible"]);
|
|
8
|
+
export function runGrantCommand(args, store, options) {
|
|
9
|
+
const [command, ...rest] = args;
|
|
10
|
+
switch (command) {
|
|
11
|
+
case "issue": return issueGrant(rest, store, options);
|
|
12
|
+
case "show": return showGrant(rest, store);
|
|
13
|
+
case "list": return listGrants(rest, store);
|
|
14
|
+
case "revoke": return revokeGrantCommand(rest, store, options);
|
|
15
|
+
default:
|
|
16
|
+
throw usageError(command === undefined
|
|
17
|
+
? "Task grant command is required."
|
|
18
|
+
: `Unknown command: task grant ${command}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function issueGrant(args, store, options) {
|
|
22
|
+
const usage = "Task grant issue usage: yui task grant issue <task> --action <name> (repeatable) [--scope-project <id>...] [--scope-repo <owner/name>...] [--scope-package <name>...] [--scope-home <path>] [--param <name=v1,v2>...] [--expires-at <iso-8601>] [--max-uses <int>] [--irreversibility-ceiling <none|reversible|irreversible>].";
|
|
23
|
+
const parsed = parseMultiValueTail(args, new Set(["--scope-home", "--expires-at", "--max-uses", "--irreversibility-ceiling"]), new Set(["--action", "--scope-project", "--scope-repo", "--scope-package", "--param"]), usage);
|
|
24
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
25
|
+
const taskId = parsed.positionals[0];
|
|
26
|
+
requireTask(store, taskId);
|
|
27
|
+
const actions = parsed.multiOptions.get("--action") ?? [];
|
|
28
|
+
if (actions.length === 0)
|
|
29
|
+
throw usageError("--action is required.", usage);
|
|
30
|
+
const scope = buildScope(parsed, usage);
|
|
31
|
+
const parameterBounds = buildParameterBounds(parsed, usage);
|
|
32
|
+
const expiresAt = optionalOption(parsed.options, "--expires-at");
|
|
33
|
+
const maxUses = parseMaxUses(parsed, usage);
|
|
34
|
+
const ceiling = parseCeiling(parsed, usage);
|
|
35
|
+
// Grant issuance mints irreversible authority. The granter is bound to the
|
|
36
|
+
// authenticated global Operator Session, not to a caller-supplied label.
|
|
37
|
+
const env = options.environment ?? {};
|
|
38
|
+
const granter = authorizeGrantOrigin(store, env, usage);
|
|
39
|
+
const now = clock(options);
|
|
40
|
+
// The grant record and its audit event commit in one transaction: a failed
|
|
41
|
+
// event save cannot leave a persisted grant without its audit trail.
|
|
42
|
+
const grant = store.transaction((tx) => {
|
|
43
|
+
const issued = createCapabilityGrant(tx.nextCapabilityGrantId(taskId), taskId, {
|
|
44
|
+
granter,
|
|
45
|
+
...(scope === undefined ? {} : { scope }),
|
|
46
|
+
actions,
|
|
47
|
+
...(parameterBounds === undefined ? {} : { parameterBounds }),
|
|
48
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
49
|
+
...(maxUses === undefined ? {} : { maxUses }),
|
|
50
|
+
...(ceiling === undefined ? {} : { irreversibilityCeiling: ceiling })
|
|
51
|
+
}, now);
|
|
52
|
+
tx.saveCapabilityGrant(taskId, issued);
|
|
53
|
+
recordTaskEvent(tx, taskId, "capability-grant.issued", {
|
|
54
|
+
grantId: issued.id,
|
|
55
|
+
granter: issued.granter,
|
|
56
|
+
actions: issued.actions.join(","),
|
|
57
|
+
scope: scopeSummary(issued.scope)
|
|
58
|
+
}, now);
|
|
59
|
+
return issued;
|
|
60
|
+
});
|
|
61
|
+
return output(`Granted ${grant.id} to ${grant.taskId}.\n`, grant);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Authorizes a grant mutation (issue or revoke) and returns the origin-bound
|
|
65
|
+
* identity to record as the granter/revoker.
|
|
66
|
+
*
|
|
67
|
+
* Grant issue and revoke carry irreversible authority: they mint or revoke
|
|
68
|
+
* the capability that lets a managed Agent perform irreversible site changes.
|
|
69
|
+
* The only accepted origin is the authenticated global Operator Session,
|
|
70
|
+
* whose env claims are verified against the durable live session binding by
|
|
71
|
+
* isCurrentGlobalOperator.
|
|
72
|
+
*
|
|
73
|
+
* Absence of YUI identity vars is deliberately NOT treated as user authority:
|
|
74
|
+
* a managed Agent can clear its child-process environment, so a "clean"
|
|
75
|
+
* environment is indistinguishable from a scrubbed managed one. A Task-scoped
|
|
76
|
+
* Session, a partial managed identity, and a stale/forged global binding are
|
|
77
|
+
* all refused. Caller-supplied --granter/--by labels are not identity; the
|
|
78
|
+
* recorded granter/revoker is bound to the Operator Session instead.
|
|
79
|
+
*/
|
|
80
|
+
function authorizeGrantOrigin(store, env, usage) {
|
|
81
|
+
if (!isCurrentGlobalOperator(store, env)) {
|
|
82
|
+
throw usageError("Grant issue and revoke require the authenticated global Operator " +
|
|
83
|
+
"session. A managed Agent cannot self-issue or self-revoke a " +
|
|
84
|
+
"capability grant, and a clean environment is not user authority.", usage);
|
|
85
|
+
}
|
|
86
|
+
// isCurrentGlobalOperator verified YUI_AGENT_ID against the durable binding.
|
|
87
|
+
return `operator:${env.YUI_AGENT_ID ?? "unknown"}`;
|
|
88
|
+
}
|
|
89
|
+
function showGrant(args, store) {
|
|
90
|
+
const usage = "Task grant show usage: yui task grant show <task> <grant-id>.";
|
|
91
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
92
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
93
|
+
const taskId = parsed.positionals[0];
|
|
94
|
+
requireTask(store, taskId);
|
|
95
|
+
const grant = store.getCapabilityGrant(taskId, parsed.positionals[1]);
|
|
96
|
+
if (grant === null) {
|
|
97
|
+
throw dataError(`Capability grant not found: ${taskId}/${parsed.positionals[1]}.`);
|
|
98
|
+
}
|
|
99
|
+
return output(renderGrant(grant, store.getConfig().timeZone), grant);
|
|
100
|
+
}
|
|
101
|
+
function listGrants(args, store) {
|
|
102
|
+
const usage = "Task grant list usage: yui task grant list <task>.";
|
|
103
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
104
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
105
|
+
const taskId = parsed.positionals[0];
|
|
106
|
+
requireTask(store, taskId);
|
|
107
|
+
const grants = store.listCapabilityGrants(taskId);
|
|
108
|
+
if (grants.length === 0) {
|
|
109
|
+
return output("No capability grants found.\n", []);
|
|
110
|
+
}
|
|
111
|
+
const timeZone = store.getConfig().timeZone;
|
|
112
|
+
const rendered = `${renderTable(`Capability grants: ${taskId}`, [
|
|
113
|
+
{ header: "ID", minWidth: 6, maxWidth: 20 },
|
|
114
|
+
{ header: "Granter", minWidth: 6, maxWidth: 18 },
|
|
115
|
+
{ header: "Actions", minWidth: 8, maxWidth: 40 },
|
|
116
|
+
{ header: "Expires", minWidth: 10, maxWidth: 28 },
|
|
117
|
+
{ header: "Uses", minWidth: 5, maxWidth: 10 },
|
|
118
|
+
{ header: "Ceiling", minWidth: 8, maxWidth: 14 },
|
|
119
|
+
{ header: "Revoked", minWidth: 8, maxWidth: 28 }
|
|
120
|
+
], grants.map((grant) => [
|
|
121
|
+
grant.id,
|
|
122
|
+
grant.granter,
|
|
123
|
+
grant.actions.join(","),
|
|
124
|
+
grant.expiresAt === undefined ? "-" : formatTimestamp(grant.expiresAt, timeZone),
|
|
125
|
+
grant.maxUses === undefined ? `${grant.usesUsed}` : `${grant.usesUsed}/${grant.maxUses}`,
|
|
126
|
+
grant.irreversibilityCeiling,
|
|
127
|
+
grant.revokedAt === undefined ? "no" : formatTimestamp(grant.revokedAt, timeZone)
|
|
128
|
+
]), defaultTableWidth())}\n`;
|
|
129
|
+
return output(rendered, grants);
|
|
130
|
+
}
|
|
131
|
+
function revokeGrantCommand(args, store, options) {
|
|
132
|
+
const usage = "Task grant revoke usage: yui task grant revoke <task> <grant-id>.";
|
|
133
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
134
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
135
|
+
const taskId = parsed.positionals[0];
|
|
136
|
+
requireTask(store, taskId);
|
|
137
|
+
const existing = store.getCapabilityGrant(taskId, parsed.positionals[1]);
|
|
138
|
+
if (existing === null) {
|
|
139
|
+
throw dataError(`Capability grant not found: ${taskId}/${parsed.positionals[1]}.`);
|
|
140
|
+
}
|
|
141
|
+
// Revoke is the same irreversible-authority operation as issue: it requires
|
|
142
|
+
// the authenticated Operator Session, and the revoker is bound to it.
|
|
143
|
+
const env = options.environment ?? {};
|
|
144
|
+
const by = authorizeGrantOrigin(store, env, usage);
|
|
145
|
+
const wasRevoked = existing.revokedAt !== undefined;
|
|
146
|
+
const now = clock(options);
|
|
147
|
+
const grant = revokeGrant(existing, by, now);
|
|
148
|
+
// The revoked grant and its audit event commit in one transaction: a failed
|
|
149
|
+
// event save cannot leave a revoked grant without its audit trail.
|
|
150
|
+
store.transaction((tx) => {
|
|
151
|
+
tx.saveCapabilityGrant(taskId, grant);
|
|
152
|
+
if (!wasRevoked) {
|
|
153
|
+
recordTaskEvent(tx, taskId, "capability-grant.revoked", {
|
|
154
|
+
grantId: grant.id,
|
|
155
|
+
revokedBy: grant.revokedBy ?? by
|
|
156
|
+
}, now);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return output(`Revoked ${grant.id}.\n`, grant);
|
|
160
|
+
}
|
|
161
|
+
function buildScope(parsed, usage) {
|
|
162
|
+
const projectIds = parsed.multiOptions.get("--scope-project") ?? [];
|
|
163
|
+
const repositories = (parsed.multiOptions.get("--scope-repo") ?? []).map((value) => {
|
|
164
|
+
const separator = value.indexOf("/");
|
|
165
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
166
|
+
throw usageError("--scope-repo must use owner/name.", usage);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
owner: value.slice(0, separator).trim(),
|
|
170
|
+
name: value.slice(separator + 1).trim()
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const packages = parsed.multiOptions.get("--scope-package") ?? [];
|
|
174
|
+
const homePath = optionalOption(parsed.options, "--scope-home");
|
|
175
|
+
if (projectIds.length === 0
|
|
176
|
+
&& repositories.length === 0
|
|
177
|
+
&& packages.length === 0
|
|
178
|
+
&& homePath === undefined) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...(projectIds.length > 0 ? { projectIds } : {}),
|
|
183
|
+
...(repositories.length > 0 ? { repositories } : {}),
|
|
184
|
+
...(packages.length > 0 ? { packages } : {}),
|
|
185
|
+
...(homePath === undefined ? {} : { homePath })
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function buildParameterBounds(parsed, usage) {
|
|
189
|
+
const paramValues = parsed.multiOptions.get("--param") ?? [];
|
|
190
|
+
if (paramValues.length === 0)
|
|
191
|
+
return undefined;
|
|
192
|
+
const bounds = {};
|
|
193
|
+
for (const value of paramValues) {
|
|
194
|
+
const separator = value.indexOf("=");
|
|
195
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
196
|
+
throw usageError("--param must use name=value1,value2.", usage);
|
|
197
|
+
}
|
|
198
|
+
const name = value.slice(0, separator).trim();
|
|
199
|
+
const allowed = value.slice(separator + 1)
|
|
200
|
+
.split(",")
|
|
201
|
+
.map((entry) => entry.trim())
|
|
202
|
+
.filter((entry) => entry.length > 0);
|
|
203
|
+
if (allowed.length === 0) {
|
|
204
|
+
throw usageError(`--param must list at least one value: ${name}.`, usage);
|
|
205
|
+
}
|
|
206
|
+
if (Object.hasOwn(bounds, name)) {
|
|
207
|
+
throw usageError(`--param may only be specified once per name: ${name}.`, usage);
|
|
208
|
+
}
|
|
209
|
+
bounds[name] = allowed;
|
|
210
|
+
}
|
|
211
|
+
return bounds;
|
|
212
|
+
}
|
|
213
|
+
function parseMaxUses(parsed, usage) {
|
|
214
|
+
const value = optionalOption(parsed.options, "--max-uses");
|
|
215
|
+
if (value === undefined)
|
|
216
|
+
return undefined;
|
|
217
|
+
if (!/^[1-9][0-9]*$/.test(value)) {
|
|
218
|
+
throw usageError("--max-uses must be a positive integer.", usage);
|
|
219
|
+
}
|
|
220
|
+
return Number(value);
|
|
221
|
+
}
|
|
222
|
+
function parseCeiling(parsed, usage) {
|
|
223
|
+
const value = optionalOption(parsed.options, "--irreversibility-ceiling");
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return undefined;
|
|
226
|
+
if (!CEILINGS.has(value)) {
|
|
227
|
+
throw usageError("--irreversibility-ceiling must be one of: none, reversible, irreversible.", usage);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function scopeSummary(scope) {
|
|
232
|
+
const parts = [];
|
|
233
|
+
if (scope.taskId !== undefined)
|
|
234
|
+
parts.push(`task:${scope.taskId}`);
|
|
235
|
+
if (scope.projectIds !== undefined && scope.projectIds.length > 0) {
|
|
236
|
+
parts.push(`project:${scope.projectIds.join(",")}`);
|
|
237
|
+
}
|
|
238
|
+
if (scope.repositories !== undefined && scope.repositories.length > 0) {
|
|
239
|
+
parts.push(`repo:${scope.repositories.map((repo) => `${repo.owner}/${repo.name}`).join(",")}`);
|
|
240
|
+
}
|
|
241
|
+
if (scope.packages !== undefined && scope.packages.length > 0) {
|
|
242
|
+
parts.push(`package:${scope.packages.join(",")}`);
|
|
243
|
+
}
|
|
244
|
+
if (scope.homePath !== undefined)
|
|
245
|
+
parts.push(`home:${scope.homePath}`);
|
|
246
|
+
return parts.join(";");
|
|
247
|
+
}
|
|
248
|
+
function renderGrant(grant, timeZone) {
|
|
249
|
+
return [
|
|
250
|
+
`Grant: ${grant.id}`,
|
|
251
|
+
`Task: ${grant.taskId}`,
|
|
252
|
+
`Granter: ${grant.granter}`,
|
|
253
|
+
`Scope: ${scopeSummary(grant.scope)}`,
|
|
254
|
+
`Actions: ${grant.actions.join(", ")}`,
|
|
255
|
+
...(Object.keys(grant.parameterBounds).length === 0
|
|
256
|
+
? []
|
|
257
|
+
: [
|
|
258
|
+
"Parameters:",
|
|
259
|
+
...Object.entries(grant.parameterBounds)
|
|
260
|
+
.map(([name, values]) => ` ${name}: ${values.join(", ")}`)
|
|
261
|
+
]),
|
|
262
|
+
...(grant.expiresAt === undefined
|
|
263
|
+
? []
|
|
264
|
+
: [`Expires: ${formatTimestamp(grant.expiresAt, timeZone)}`]),
|
|
265
|
+
...(grant.maxUses === undefined ? [] : [`Max uses: ${grant.maxUses}`]),
|
|
266
|
+
`Uses used: ${grant.usesUsed}`,
|
|
267
|
+
`Ceiling: ${grant.irreversibilityCeiling}`,
|
|
268
|
+
...(grant.revokedAt === undefined
|
|
269
|
+
? ["Revoked: no"]
|
|
270
|
+
: [
|
|
271
|
+
"Revoked: yes",
|
|
272
|
+
`Revoked by: ${grant.revokedBy}`,
|
|
273
|
+
`Revoked at: ${formatTimestamp(grant.revokedAt, timeZone)}`
|
|
274
|
+
]),
|
|
275
|
+
`Created: ${formatTimestamp(grant.createdAt, timeZone)}`,
|
|
276
|
+
`Updated: ${formatTimestamp(grant.updatedAt, timeZone)}`
|
|
277
|
+
].join("\n").concat("\n");
|
|
278
|
+
}
|
|
279
|
+
function requireTask(store, taskId) {
|
|
280
|
+
const id = requiredText(taskId, "Task id");
|
|
281
|
+
const task = store.getTask(id);
|
|
282
|
+
if (task === null)
|
|
283
|
+
throw taskNotFound(id);
|
|
284
|
+
return task;
|
|
285
|
+
}
|
|
286
|
+
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
287
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now));
|
|
288
|
+
}
|
|
289
|
+
function requiredOption(options, name) {
|
|
290
|
+
return requiredText(options.get(name), name);
|
|
291
|
+
}
|
|
292
|
+
function optionalOption(options, name) {
|
|
293
|
+
if (!options.has(name))
|
|
294
|
+
return undefined;
|
|
295
|
+
return requiredText(options.get(name), name);
|
|
296
|
+
}
|
|
297
|
+
function requiredText(value, label) {
|
|
298
|
+
const normalized = value?.trim();
|
|
299
|
+
if (normalized === undefined || normalized.length === 0) {
|
|
300
|
+
throw usageError(`${label} is required.`);
|
|
301
|
+
}
|
|
302
|
+
return normalized;
|
|
303
|
+
}
|
|
304
|
+
function clock(options) {
|
|
305
|
+
return options.now?.() ?? new Date();
|
|
306
|
+
}
|
|
307
|
+
function output(value, data) {
|
|
308
|
+
return data === undefined
|
|
309
|
+
? { kind: "output", output: value }
|
|
310
|
+
: { kind: "output", output: value, data };
|
|
311
|
+
}
|
|
312
|
+
function exactPositionals(values, count, usage) {
|
|
313
|
+
if (values.length !== count || values.some((value) => value.trim().length === 0)) {
|
|
314
|
+
throw usageError(usage);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function parseTail(args, valueOptions, usage) {
|
|
318
|
+
const positionals = [];
|
|
319
|
+
const options = new Map();
|
|
320
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
321
|
+
const value = args[index];
|
|
322
|
+
if (!value.startsWith("--")) {
|
|
323
|
+
positionals.push(value);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!valueOptions.has(value)) {
|
|
327
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
328
|
+
}
|
|
329
|
+
if (options.has(value)) {
|
|
330
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
331
|
+
}
|
|
332
|
+
const optionValue = args[index + 1];
|
|
333
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
334
|
+
throw usageError(`${value} is required.`, usage);
|
|
335
|
+
}
|
|
336
|
+
options.set(value, optionValue);
|
|
337
|
+
index += 1;
|
|
338
|
+
}
|
|
339
|
+
return { positionals, options };
|
|
340
|
+
}
|
|
341
|
+
function parseMultiValueTail(args, valueOptions, repeatOptions, usage) {
|
|
342
|
+
const positionals = [];
|
|
343
|
+
const options = new Map();
|
|
344
|
+
const multiOptions = new Map();
|
|
345
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
346
|
+
const value = args[index];
|
|
347
|
+
if (!value.startsWith("--")) {
|
|
348
|
+
positionals.push(value);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (!valueOptions.has(value) && !repeatOptions.has(value)) {
|
|
352
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
353
|
+
}
|
|
354
|
+
if (repeatOptions.has(value)) {
|
|
355
|
+
const optionValue = args[index + 1];
|
|
356
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
357
|
+
throw usageError(`${value} is required.`, usage);
|
|
358
|
+
}
|
|
359
|
+
multiOptions.set(value, [...(multiOptions.get(value) ?? []), optionValue]);
|
|
360
|
+
index += 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (options.has(value)) {
|
|
364
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
365
|
+
}
|
|
366
|
+
const optionValue = args[index + 1];
|
|
367
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
368
|
+
throw usageError(`${value} is required.`, usage);
|
|
369
|
+
}
|
|
370
|
+
options.set(value, optionValue);
|
|
371
|
+
index += 1;
|
|
372
|
+
}
|
|
373
|
+
return { positionals, options, multiOptions };
|
|
374
|
+
}
|
|
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
6
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
7
|
+
import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
|
|
7
8
|
import { addProjectKnowledge, createProject, managedProjectPath, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
|
|
8
9
|
export async function runProjectCommand(args, store, options = {}) {
|
|
9
10
|
const [command, ...rest] = args;
|
|
@@ -223,104 +224,141 @@ async function migrateProject(args, store, options) {
|
|
|
223
224
|
}
|
|
224
225
|
const git = options.git ?? new NodeGitWorkspace();
|
|
225
226
|
const destination = managedProjectPath(store.rootDirectory(), project.id);
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
: destination;
|
|
231
|
-
if (!parsed.preflight) {
|
|
232
|
-
// A crashed earlier attempt can leave an unreferenced managed clone
|
|
233
|
-
// behind. It is safe to remove only because no catalog record points at
|
|
234
|
-
// it; a registered path fails closed instead.
|
|
235
|
-
await removeUnreferencedClone(store, destination, project.id);
|
|
236
|
-
}
|
|
237
|
-
else if (existsSync(verifyRoot)) {
|
|
238
|
-
// A crashed preflight can only leave its own throwaway clone behind.
|
|
239
|
-
await rm(verifyRoot, { recursive: true, force: true });
|
|
240
|
-
}
|
|
241
|
-
let prepared = false;
|
|
227
|
+
// Migration rewrites the Project's Git repository: hold the per-Project
|
|
228
|
+
// maintenance fence so no rebuild/archive/cleanup (or a second migrate)
|
|
229
|
+
// interleaves, and the Controller defers worktree preparation meanwhile.
|
|
230
|
+
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
242
231
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
232
|
+
// Re-read the Project under the fence. A concurrent migration may have
|
|
233
|
+
// completed and switched the catalog to the Home-managed repo since the
|
|
234
|
+
// record was resolved above; the stale snapshot must not drive any Git
|
|
235
|
+
// effect (in particular it must not delete the now-canonical repo).
|
|
236
|
+
const current = requireProject(store, project.id);
|
|
237
|
+
if (current.ownership === "managed") {
|
|
238
|
+
throw usageError(`Project is already Home-managed: ${project.id}.`);
|
|
239
|
+
}
|
|
240
|
+
if (current.remoteUrl === undefined) {
|
|
241
|
+
throw usageError(`Project migrate requires a remote URL: ${project.id}.`);
|
|
242
|
+
}
|
|
243
|
+
if (current.path !== project.path
|
|
244
|
+
|| current.remoteUrl !== project.remoteUrl
|
|
245
|
+
|| current.stableBranch !== project.stableBranch
|
|
246
|
+
|| current.developmentBranch !== project.developmentBranch) {
|
|
247
|
+
throw new Error(`Project changed while migrating: ${project.id}.`);
|
|
248
|
+
}
|
|
249
|
+
// Preflight verifies the remote into a throwaway clone: it changes neither
|
|
250
|
+
// the catalog nor the persistent projects directory.
|
|
251
|
+
const verifyRoot = parsed.preflight
|
|
252
|
+
? join(store.rootDirectory(), "projects", `.preflight-${project.id}`)
|
|
253
|
+
: destination;
|
|
254
|
+
if (!parsed.preflight) {
|
|
255
|
+
// A crashed earlier attempt can leave an unreferenced managed clone
|
|
256
|
+
// behind. It is safe to remove only because no catalog record points at
|
|
257
|
+
// it; a registered path fails closed instead.
|
|
258
|
+
await removeUnreferencedClone(store, destination, project.id);
|
|
259
|
+
}
|
|
260
|
+
else if (existsSync(verifyRoot)) {
|
|
261
|
+
// A crashed preflight can only leave its own throwaway clone behind.
|
|
262
|
+
await rm(verifyRoot, { recursive: true, force: true });
|
|
257
263
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
const switched = store.transaction((tx) => {
|
|
280
|
-
const latest = requireProject(tx, project.id);
|
|
281
|
-
if (latest.ownership !== "external"
|
|
282
|
-
|| latest.path !== project.path
|
|
283
|
-
|| latest.remoteUrl !== project.remoteUrl) {
|
|
284
|
-
throw new Error(`Project changed while migrating: ${project.id}.`);
|
|
264
|
+
let prepared = false;
|
|
265
|
+
try {
|
|
266
|
+
// Every Git effect is driven from the under-fence snapshot `current`,
|
|
267
|
+
// never the pre-lock `project`: a branch/remote change between the read
|
|
268
|
+
// and the lock fails closed above, and the publication CAS below covers
|
|
269
|
+
// a change between the lock and the switch.
|
|
270
|
+
const head = await git.clone({
|
|
271
|
+
remoteUrl: current.remoteUrl,
|
|
272
|
+
destination: verifyRoot,
|
|
273
|
+
...(current.stableBranch === "HEAD" ? {} : { branch: current.stableBranch })
|
|
274
|
+
});
|
|
275
|
+
prepared = true;
|
|
276
|
+
const stable = current.stableBranch === "HEAD"
|
|
277
|
+
? head
|
|
278
|
+
: await git.inspect(verifyRoot, current.stableBranch);
|
|
279
|
+
if (head.baseCommit !== stable.baseCommit) {
|
|
280
|
+
throw new Error(`Project checkout is not on its stable ref: ${current.stableBranch}.`);
|
|
281
|
+
}
|
|
282
|
+
if (current.developmentBranch !== current.stableBranch) {
|
|
283
|
+
await git.ensureLocalBranch(verifyRoot, current.developmentBranch);
|
|
285
284
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
285
|
+
await assertRemoteBranchesVerified(git, verifyRoot, current.remoteUrl, [
|
|
286
|
+
{ ref: current.stableBranch, localCommit: head.baseCommit },
|
|
287
|
+
...(current.developmentBranch === current.stableBranch
|
|
288
|
+
? []
|
|
289
|
+
: [{
|
|
290
|
+
ref: current.developmentBranch,
|
|
291
|
+
localCommit: (await git.inspect(verifyRoot, current.developmentBranch)).baseCommit
|
|
292
|
+
}])
|
|
293
|
+
]);
|
|
294
|
+
const copyRefs = git.copyRefs;
|
|
295
|
+
if (typeof copyRefs !== "function") {
|
|
296
|
+
throw new Error(`Git workspace cannot preserve local Yui refs for Project: ${project.id}.`);
|
|
297
|
+
}
|
|
298
|
+
await copyRefs.call(git, {
|
|
299
|
+
sourceRepositoryPath: current.path,
|
|
300
|
+
destinationRepositoryPath: verifyRoot,
|
|
301
|
+
patterns: ["refs/heads/yui/", "refs/yui/archive/"]
|
|
293
302
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
if (parsed.preflight) {
|
|
304
|
+
return { project: current, path: destination, preflight: true };
|
|
305
|
+
}
|
|
306
|
+
const switched = store.transaction((tx) => {
|
|
307
|
+
const latest = requireProject(tx, project.id);
|
|
308
|
+
if (latest.ownership !== "external"
|
|
309
|
+
|| latest.path !== current.path
|
|
310
|
+
|| latest.remoteUrl !== current.remoteUrl
|
|
311
|
+
|| latest.stableBranch !== current.stableBranch
|
|
312
|
+
|| latest.developmentBranch !== current.developmentBranch) {
|
|
313
|
+
throw new Error(`Project changed while migrating: ${project.id}.`);
|
|
314
|
+
}
|
|
315
|
+
// The switch only changes ownership and path; every other field is
|
|
316
|
+
// frozen, and the catalog validator re-checks the whole record.
|
|
317
|
+
const next = validateProject({
|
|
318
|
+
...latest,
|
|
319
|
+
path: destination,
|
|
320
|
+
ownership: "managed",
|
|
321
|
+
updatedAt: (options.now ?? (() => new Date()))().toISOString()
|
|
322
|
+
});
|
|
323
|
+
assertProjectAvailable(tx, next, latest.id);
|
|
324
|
+
tx.saveProject(next);
|
|
325
|
+
return next;
|
|
326
|
+
});
|
|
327
|
+
return { project: switched, path: switched.path, preflight: false };
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (prepared && !parsed.preflight
|
|
331
|
+
&& !store.listProjects().some(({ path }) => path === destination)) {
|
|
332
|
+
await rm(destination, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (parsed.preflight) {
|
|
338
|
+
await rm(verifyRoot, { recursive: true, force: true });
|
|
339
|
+
}
|
|
304
340
|
}
|
|
305
|
-
throw error;
|
|
306
341
|
}
|
|
307
342
|
finally {
|
|
308
|
-
|
|
309
|
-
await rm(verifyRoot, { recursive: true, force: true });
|
|
310
|
-
}
|
|
343
|
+
releaseMaintenance();
|
|
311
344
|
}
|
|
312
345
|
}
|
|
313
346
|
/**
|
|
314
347
|
* Remove a managed clone left behind by a crashed migration attempt. Such a
|
|
315
348
|
* directory is unreferenced garbage; a path any catalog record points at is
|
|
316
|
-
* never deleted.
|
|
349
|
+
* never deleted. The check fails closed even when the destination is
|
|
350
|
+
* registered to the migrating Project itself: a concurrent migration may
|
|
351
|
+
* already have completed and switched the catalog to this path, making it
|
|
352
|
+
* the canonical repository that must not be removed.
|
|
317
353
|
*/
|
|
318
354
|
async function removeUnreferencedClone(store, destination, migratingProjectId) {
|
|
319
355
|
if (!existsSync(destination))
|
|
320
356
|
return;
|
|
321
357
|
const registered = store.listProjects().find(({ path }) => path === destination);
|
|
322
|
-
if (registered !== undefined
|
|
323
|
-
throw new Error(
|
|
358
|
+
if (registered !== undefined) {
|
|
359
|
+
throw new Error(registered.id === migratingProjectId
|
|
360
|
+
? `Project already migrated to the managed path: ${destination}.`
|
|
361
|
+
: `Managed Project path is already registered: ${destination}.`);
|
|
324
362
|
}
|
|
325
363
|
await rm(destination, { recursive: true, force: true });
|
|
326
364
|
}
|