@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
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { usageError } from "../errors/cliError.js";
|
|
4
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
5
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
6
|
+
import { addProjectKnowledge, createProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProjectName } from "../repository/project.js";
|
|
7
|
+
export async function runProjectCommand(args, store, options = {}) {
|
|
8
|
+
const [command, ...rest] = args;
|
|
9
|
+
if (command === "add" || command === "clone") {
|
|
10
|
+
const project = command === "add"
|
|
11
|
+
? await addProject(rest, store, options)
|
|
12
|
+
: await cloneProject(rest, store, options);
|
|
13
|
+
return { output: renderAddedProject(project), data: { project } };
|
|
14
|
+
}
|
|
15
|
+
if (command === "list") {
|
|
16
|
+
return { output: listProjects(rest, store), data: { projects: store.listProjects() } };
|
|
17
|
+
}
|
|
18
|
+
if (command === "refresh") {
|
|
19
|
+
const refreshed = await refreshProject(rest, store, options);
|
|
20
|
+
return {
|
|
21
|
+
output: refreshed.changed
|
|
22
|
+
? `Refreshed project ${refreshed.project.id}: ${refreshed.fromCommit} -> ${refreshed.toCommit}\n`
|
|
23
|
+
: `Project ${refreshed.project.id} is already current at ${refreshed.toCommit}\n`,
|
|
24
|
+
data: refreshed
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (command === "update") {
|
|
28
|
+
const project = await updateProject(rest, store, options);
|
|
29
|
+
return {
|
|
30
|
+
output: `Updated project ${project.id}\n`,
|
|
31
|
+
data: { project }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (command === "discover") {
|
|
35
|
+
const discovered = await discoverProjects(rest, store, options);
|
|
36
|
+
return { output: renderDiscoveredProjects(discovered), data: { projects: discovered } };
|
|
37
|
+
}
|
|
38
|
+
if (command === "show") {
|
|
39
|
+
const project = rest[0] === undefined ? null : resolveProject(store.listProjects(), rest[0]);
|
|
40
|
+
return { output: showProject(rest, store), ...(project === null ? {} : { data: { project } }) };
|
|
41
|
+
}
|
|
42
|
+
if (command === "knowledge") {
|
|
43
|
+
const result = projectKnowledge(rest, store, options);
|
|
44
|
+
const project = resolveProject(store.listProjects(), result.projectId);
|
|
45
|
+
if (project === null)
|
|
46
|
+
return { output: result.output };
|
|
47
|
+
if (result.knowledgeId !== undefined) {
|
|
48
|
+
const knowledge = project.knowledge.find(({ id }) => id === result.knowledgeId);
|
|
49
|
+
return { output: result.output, data: { project, knowledge } };
|
|
50
|
+
}
|
|
51
|
+
const knowledge = result.all
|
|
52
|
+
? project.knowledge
|
|
53
|
+
: project.knowledge.filter(({ status }) => status === "active");
|
|
54
|
+
return { output: result.output, data: { project, knowledge } };
|
|
55
|
+
}
|
|
56
|
+
throw usageError(command === undefined
|
|
57
|
+
? "Project command is required."
|
|
58
|
+
: `Unknown command: project ${command}`);
|
|
59
|
+
}
|
|
60
|
+
async function refreshProject(args, store, options) {
|
|
61
|
+
if (args.length !== 1) {
|
|
62
|
+
throw usageError("Project refresh usage: yui project refresh <project>.");
|
|
63
|
+
}
|
|
64
|
+
const project = requireProject(store, args[0]);
|
|
65
|
+
if (project.remoteUrl === undefined) {
|
|
66
|
+
throw usageError(`Project refresh requires a remote URL: ${project.id}.`);
|
|
67
|
+
}
|
|
68
|
+
if (project.stableBranch !== project.developmentBranch) {
|
|
69
|
+
throw usageError(`Project refresh requires matching stable and development branches: ${project.id}.`);
|
|
70
|
+
}
|
|
71
|
+
const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
|
|
72
|
+
repositoryPath: project.path,
|
|
73
|
+
remoteUrl: project.remoteUrl,
|
|
74
|
+
stableRef: project.stableBranch
|
|
75
|
+
});
|
|
76
|
+
return { project, ...refreshed };
|
|
77
|
+
}
|
|
78
|
+
async function discoverProjects(args, store, options) {
|
|
79
|
+
if (args.length > 1) {
|
|
80
|
+
throw usageError("Project discover usage: yui project discover [name].");
|
|
81
|
+
}
|
|
82
|
+
const query = args[0]?.trim().toLocaleLowerCase();
|
|
83
|
+
const workspace = store.getConfig().defaultWorkspace;
|
|
84
|
+
if (workspace === undefined)
|
|
85
|
+
throw usageError("No default workspace is configured; run yui setup.");
|
|
86
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
87
|
+
const entries = await readdir(workspace, { withFileTypes: true });
|
|
88
|
+
const discovered = [];
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.isDirectory() || entry.name === "worktree"
|
|
91
|
+
|| (query !== undefined && !entry.name.toLocaleLowerCase().includes(query)))
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
const candidate = resolve(workspace, entry.name);
|
|
95
|
+
const inspected = await git.inspect(candidate, "HEAD");
|
|
96
|
+
if (inspected.root !== candidate)
|
|
97
|
+
continue;
|
|
98
|
+
const registered = store.listProjects().find(({ path }) => path === inspected.root);
|
|
99
|
+
discovered.push({
|
|
100
|
+
name: entry.name,
|
|
101
|
+
path: inspected.root,
|
|
102
|
+
branch: await git.headRef(inspected.root),
|
|
103
|
+
...(registered === undefined ? {} : { registeredProjectId: registered.id })
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// The workspace may contain ordinary directories; only Git roots are candidates.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return discovered.sort((left, right) => left.name.localeCompare(right.name));
|
|
111
|
+
}
|
|
112
|
+
function renderDiscoveredProjects(projects) {
|
|
113
|
+
if (projects.length === 0)
|
|
114
|
+
return "No Git projects found in the workspace.\n";
|
|
115
|
+
return `${renderTable("Workspace projects", [
|
|
116
|
+
{ header: "Name", minWidth: 4, maxWidth: 28 },
|
|
117
|
+
{ header: "Path", minWidth: 8, maxWidth: 64 },
|
|
118
|
+
{ header: "Project", minWidth: 8, maxWidth: 24 }
|
|
119
|
+
], projects.map((project) => [
|
|
120
|
+
project.name,
|
|
121
|
+
project.path,
|
|
122
|
+
project.registeredProjectId ?? "-"
|
|
123
|
+
]), defaultTableWidth())}\n`;
|
|
124
|
+
}
|
|
125
|
+
async function cloneProject(args, store, options) {
|
|
126
|
+
const usage = "Project clone usage: yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>].";
|
|
127
|
+
const parsed = parseCloneArguments(args, usage);
|
|
128
|
+
const workspace = store.getConfig().defaultWorkspace;
|
|
129
|
+
if (workspace === undefined)
|
|
130
|
+
throw usageError("No default workspace is configured; run yui setup.");
|
|
131
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
132
|
+
const name = validateProjectName(parsed.name);
|
|
133
|
+
const workspaceRoot = resolve(workspace);
|
|
134
|
+
const destination = resolve(workspaceRoot, name);
|
|
135
|
+
if (dirname(destination) !== workspaceRoot) {
|
|
136
|
+
throw usageError("Project clone destination must be directly inside the configured workspace.");
|
|
137
|
+
}
|
|
138
|
+
assertOutsideManagedWorktrees(destination, workspace);
|
|
139
|
+
const stableRef = parsed.stable ?? "HEAD";
|
|
140
|
+
const developmentRef = parsed.development ?? stableRef;
|
|
141
|
+
const projectId = store.nextProjectId();
|
|
142
|
+
const now = (options.now ?? (() => new Date()))();
|
|
143
|
+
const candidate = createProject(projectId, name, destination, { stable: stableRef, development: developmentRef }, now, { aliases: parsed.aliases, remoteUrl: parsed.remote });
|
|
144
|
+
assertProjectAvailable(store, candidate);
|
|
145
|
+
let cloned = false;
|
|
146
|
+
try {
|
|
147
|
+
const head = await git.clone({
|
|
148
|
+
remoteUrl: parsed.remote,
|
|
149
|
+
destination,
|
|
150
|
+
...(stableRef === "HEAD" ? {} : { branch: stableRef })
|
|
151
|
+
});
|
|
152
|
+
cloned = true;
|
|
153
|
+
const stable = stableRef === "HEAD" ? head : await git.inspect(destination, stableRef);
|
|
154
|
+
if (head.baseCommit !== stable.baseCommit) {
|
|
155
|
+
throw new Error(`Project checkout is not on its stable ref: ${stableRef}.`);
|
|
156
|
+
}
|
|
157
|
+
if (developmentRef !== stableRef) {
|
|
158
|
+
await git.ensureLocalBranch(destination, developmentRef);
|
|
159
|
+
}
|
|
160
|
+
const project = createProject(projectId, name, head.root, { stable: stableRef, development: developmentRef }, now, { aliases: parsed.aliases, remoteUrl: parsed.remote });
|
|
161
|
+
assertProjectAvailable(store, project);
|
|
162
|
+
const created = store.createProjectIfAbsent(project);
|
|
163
|
+
if (created === null)
|
|
164
|
+
throw usageError(`Project already exists: ${project.name}.`);
|
|
165
|
+
return created;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (cloned && !store.listProjects().some(({ path }) => path === destination)) {
|
|
169
|
+
await rm(destination, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function addProject(args, store, options) {
|
|
175
|
+
const usage = "Project add usage: yui project add <name> <path> [--alias <name> ...] [--remote <url>] [--stable <ref>] [--development <ref>].";
|
|
176
|
+
const parsed = parseAddArguments(args, usage);
|
|
177
|
+
assertOutsideManagedWorktrees(parsed.path, store.getConfig().defaultWorkspace);
|
|
178
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
179
|
+
const head = await git.inspect(parsed.path, "HEAD");
|
|
180
|
+
assertOutsideManagedWorktrees(head.root, store.getConfig().defaultWorkspace);
|
|
181
|
+
if (!await git.isClean(head.root)) {
|
|
182
|
+
throw usageError("Project checkout must be clean before it can be registered.");
|
|
183
|
+
}
|
|
184
|
+
const stableRef = parsed.stable ?? head.baseRef;
|
|
185
|
+
const stable = stableRef === "HEAD" ? head : await git.inspect(head.root, stableRef);
|
|
186
|
+
if (head.baseCommit !== stable.baseCommit) {
|
|
187
|
+
throw usageError(`Project checkout must be on its stable ref: ${stableRef}.`);
|
|
188
|
+
}
|
|
189
|
+
const developmentRef = parsed.development ?? stableRef;
|
|
190
|
+
const project = createProject(store.nextProjectId(), parsed.name, head.root, {
|
|
191
|
+
stable: stableRef,
|
|
192
|
+
development: developmentRef
|
|
193
|
+
}, (options.now ?? (() => new Date()))(), { aliases: parsed.aliases, ...(parsed.remote === undefined ? {} : { remoteUrl: parsed.remote }) });
|
|
194
|
+
assertProjectAvailable(store, project);
|
|
195
|
+
if (developmentRef !== stableRef)
|
|
196
|
+
await git.ensureLocalBranch(head.root, developmentRef);
|
|
197
|
+
const created = store.createProjectIfAbsent(project);
|
|
198
|
+
if (created === null)
|
|
199
|
+
throw usageError(`Project already exists: ${project.name}.`);
|
|
200
|
+
return created;
|
|
201
|
+
}
|
|
202
|
+
async function updateProject(args, store, options) {
|
|
203
|
+
const usage = "Project update usage: yui project update <project> [--alias <name> ...|--clear-aliases] [--remote <url>|--clear-remote] [--stable <ref>] [--development <ref>].";
|
|
204
|
+
const parsed = parseProjectUpdateArguments(args, usage);
|
|
205
|
+
const current = requireProject(store, parsed.reference);
|
|
206
|
+
const now = (options.now ?? (() => new Date()))();
|
|
207
|
+
const patch = {
|
|
208
|
+
...(parsed.aliases === undefined ? {} : { aliases: parsed.aliases }),
|
|
209
|
+
...(parsed.remote === undefined ? {} : { remoteUrl: parsed.remote }),
|
|
210
|
+
...(parsed.stable === undefined ? {} : { stableBranch: parsed.stable }),
|
|
211
|
+
...(parsed.development === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { developmentBranch: parsed.development })
|
|
214
|
+
};
|
|
215
|
+
assertProjectAvailable(store, updateProjectMetadata(current, patch, now), current.id);
|
|
216
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
217
|
+
if (parsed.stable !== undefined) {
|
|
218
|
+
const head = await git.inspect(current.path, "HEAD");
|
|
219
|
+
if (!await git.isClean(head.root)) {
|
|
220
|
+
throw usageError("Project checkout must be clean before its stable ref can be changed.");
|
|
221
|
+
}
|
|
222
|
+
const stable = parsed.stable === "HEAD"
|
|
223
|
+
? head
|
|
224
|
+
: await git.inspect(current.path, parsed.stable);
|
|
225
|
+
if (head.baseCommit !== stable.baseCommit) {
|
|
226
|
+
throw usageError(`Project checkout must be on its stable ref: ${parsed.stable}.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const developmentBranch = parsed.development ?? current.developmentBranch;
|
|
230
|
+
const stableBranch = parsed.stable ?? current.stableBranch;
|
|
231
|
+
if (parsed.development !== undefined && developmentBranch !== stableBranch) {
|
|
232
|
+
await git.ensureLocalBranch(current.path, developmentBranch);
|
|
233
|
+
}
|
|
234
|
+
return store.transaction((tx) => {
|
|
235
|
+
const latest = requireProject(tx, current.id);
|
|
236
|
+
if (latest.path !== current.path) {
|
|
237
|
+
throw new Error(`Project path changed while updating: ${current.id}.`);
|
|
238
|
+
}
|
|
239
|
+
const updated = updateProjectMetadata(latest, patch, now);
|
|
240
|
+
assertProjectAvailable(tx, updated, current.id);
|
|
241
|
+
tx.saveProject(updated);
|
|
242
|
+
return updated;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function renderAddedProject(created) {
|
|
246
|
+
return [
|
|
247
|
+
`Added project ${created.id}`,
|
|
248
|
+
`Name: ${created.name}`,
|
|
249
|
+
`Path: ${created.path}`,
|
|
250
|
+
`Stable: ${created.stableBranch}`,
|
|
251
|
+
`Development: ${created.developmentBranch}`
|
|
252
|
+
].join("\n").concat("\n");
|
|
253
|
+
}
|
|
254
|
+
function assertProjectAvailable(store, project, exceptId) {
|
|
255
|
+
const others = store.listProjects().filter(({ id }) => id !== exceptId);
|
|
256
|
+
if (others.some((entry) => entry.path === project.path)) {
|
|
257
|
+
throw usageError(`Project path is already registered: ${project.path}.`);
|
|
258
|
+
}
|
|
259
|
+
for (const reference of [project.id, project.name, ...project.aliases]) {
|
|
260
|
+
if (resolveProject(others, reference) !== null) {
|
|
261
|
+
throw usageError(`Project reference is already registered: ${reference}.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function assertOutsideManagedWorktrees(path, workspace) {
|
|
266
|
+
if (workspace === undefined)
|
|
267
|
+
return;
|
|
268
|
+
const managedRoot = join(resolve(workspace), "worktree");
|
|
269
|
+
const candidate = resolve(path);
|
|
270
|
+
const fromManagedRoot = relative(managedRoot, candidate);
|
|
271
|
+
const toManagedRoot = relative(candidate, managedRoot);
|
|
272
|
+
if (isContainedPath(fromManagedRoot) || isContainedPath(toManagedRoot)) {
|
|
273
|
+
throw usageError("Project path is reserved for managed worktrees.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function isContainedPath(relativePath) {
|
|
277
|
+
return relativePath === ""
|
|
278
|
+
|| (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
279
|
+
}
|
|
280
|
+
function listProjects(args, store) {
|
|
281
|
+
if (args.length !== 0)
|
|
282
|
+
throw usageError("Project list usage: yui project list.");
|
|
283
|
+
const projects = store.listProjects();
|
|
284
|
+
if (projects.length === 0)
|
|
285
|
+
return "No projects found.\n";
|
|
286
|
+
return `${renderTable("Projects", [
|
|
287
|
+
{ header: "Project", minWidth: 8, maxWidth: 24 },
|
|
288
|
+
{ header: "Name", minWidth: 4, maxWidth: 28 },
|
|
289
|
+
{ header: "Path", minWidth: 8, maxWidth: 64 },
|
|
290
|
+
{ header: "Stable", minWidth: 6, maxWidth: 24 },
|
|
291
|
+
{ header: "Development", minWidth: 11, maxWidth: 24 }
|
|
292
|
+
], projects.map((project) => [
|
|
293
|
+
project.id, project.name, project.path, project.stableBranch, project.developmentBranch
|
|
294
|
+
]), defaultTableWidth())}\n`;
|
|
295
|
+
}
|
|
296
|
+
function showProject(args, store) {
|
|
297
|
+
if (args.length !== 1)
|
|
298
|
+
throw usageError("Project show usage: yui project show <project>.");
|
|
299
|
+
const project = requireProject(store, args[0]);
|
|
300
|
+
return [
|
|
301
|
+
`Project: ${project.id}`,
|
|
302
|
+
`Name: ${project.name}`,
|
|
303
|
+
`Aliases: ${project.aliases.length === 0 ? "-" : project.aliases.join(", ")}`,
|
|
304
|
+
`Path: ${project.path}`,
|
|
305
|
+
...(project.remoteUrl === undefined ? [] : [`Remote: ${project.remoteUrl}`]),
|
|
306
|
+
`Stable: ${project.stableBranch}`,
|
|
307
|
+
`Development: ${project.developmentBranch}`,
|
|
308
|
+
`Knowledge: ${project.knowledge.filter(({ status }) => status === "active").length} active`
|
|
309
|
+
].join("\n").concat("\n");
|
|
310
|
+
}
|
|
311
|
+
function projectKnowledge(args, store, options) {
|
|
312
|
+
const [command, ...rest] = args;
|
|
313
|
+
if (command === "add") {
|
|
314
|
+
const usage = "Project knowledge add usage: yui project knowledge add <project> <title> --body <text>.";
|
|
315
|
+
const parsed = parseSingleOption(rest, "--body", usage);
|
|
316
|
+
if (parsed.positionals.length !== 2)
|
|
317
|
+
throw usageError(usage);
|
|
318
|
+
const added = store.transaction((tx) => {
|
|
319
|
+
const project = requireProject(tx, parsed.positionals[0]);
|
|
320
|
+
const id = nextKnowledgeId(project);
|
|
321
|
+
tx.saveProject(addProjectKnowledge(project, id, parsed.positionals[1], parsed.value, (options.now ?? (() => new Date()))()));
|
|
322
|
+
return { id, projectId: project.id };
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
output: `Added project knowledge ${added.id} to ${added.projectId}\n`,
|
|
326
|
+
projectId: added.projectId,
|
|
327
|
+
knowledgeId: added.id
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (command === "list") {
|
|
331
|
+
const all = rest.includes("--all");
|
|
332
|
+
const positionals = rest.filter((value) => value !== "--all");
|
|
333
|
+
if (positionals.length !== 1 || rest.some((value) => value.startsWith("--") && value !== "--all")) {
|
|
334
|
+
throw usageError("Project knowledge list usage: yui project knowledge list <project> [--all].");
|
|
335
|
+
}
|
|
336
|
+
const project = requireProject(store, positionals[0]);
|
|
337
|
+
const entries = all
|
|
338
|
+
? project.knowledge
|
|
339
|
+
: project.knowledge.filter(({ status }) => status === "active");
|
|
340
|
+
if (entries.length === 0) {
|
|
341
|
+
return {
|
|
342
|
+
output: "No project knowledge found.\n",
|
|
343
|
+
projectId: project.id,
|
|
344
|
+
all
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
output: `${renderTable(`Project knowledge: ${project.name}`, [
|
|
349
|
+
{ header: "Knowledge", minWidth: 9, maxWidth: 24 },
|
|
350
|
+
{ header: "Title", minWidth: 8, maxWidth: 64 },
|
|
351
|
+
{ header: "Status", minWidth: 6, maxWidth: 10 }
|
|
352
|
+
], entries.map((entry) => [entry.id, entry.title, entry.status]), defaultTableWidth())}\n`,
|
|
353
|
+
projectId: project.id,
|
|
354
|
+
all
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (command === "show") {
|
|
358
|
+
if (rest.length !== 2) {
|
|
359
|
+
throw usageError("Project knowledge show usage: yui project knowledge show <project> <knowledge>.");
|
|
360
|
+
}
|
|
361
|
+
const project = requireProject(store, rest[0]);
|
|
362
|
+
const entry = project.knowledge.find(({ id }) => id === rest[1]);
|
|
363
|
+
if (entry === undefined)
|
|
364
|
+
throw usageError(`Project knowledge not found: ${rest[1]}.`);
|
|
365
|
+
return {
|
|
366
|
+
output: [
|
|
367
|
+
`Knowledge: ${entry.id}`,
|
|
368
|
+
`Project: ${project.id}`,
|
|
369
|
+
`Title: ${entry.title}`,
|
|
370
|
+
`Status: ${entry.status}`,
|
|
371
|
+
"",
|
|
372
|
+
entry.body
|
|
373
|
+
].join("\n").concat("\n"),
|
|
374
|
+
projectId: project.id,
|
|
375
|
+
knowledgeId: entry.id
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (command === "update") {
|
|
379
|
+
const usage = "Project knowledge update usage: yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>].";
|
|
380
|
+
const parsed = parseKnowledgeUpdateArguments(rest, usage);
|
|
381
|
+
const updated = store.transaction((tx) => {
|
|
382
|
+
const project = requireProject(tx, parsed.project);
|
|
383
|
+
const next = updateProjectKnowledge(project, parsed.id, {
|
|
384
|
+
...(parsed.title === undefined ? {} : { title: parsed.title }),
|
|
385
|
+
...(parsed.body === undefined ? {} : { body: parsed.body })
|
|
386
|
+
}, (options.now ?? (() => new Date()))());
|
|
387
|
+
tx.saveProject(next);
|
|
388
|
+
return next;
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
output: `Updated project knowledge ${parsed.id} in ${updated.id}\n`,
|
|
392
|
+
projectId: updated.id,
|
|
393
|
+
knowledgeId: parsed.id
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (command === "retire") {
|
|
397
|
+
if (rest.length !== 2) {
|
|
398
|
+
throw usageError("Project knowledge retire usage: yui project knowledge retire <project> <knowledge>.");
|
|
399
|
+
}
|
|
400
|
+
const updated = store.transaction((tx) => {
|
|
401
|
+
const project = requireProject(tx, rest[0]);
|
|
402
|
+
const next = retireProjectKnowledge(project, rest[1], (options.now ?? (() => new Date()))());
|
|
403
|
+
tx.saveProject(next);
|
|
404
|
+
return next;
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
output: `Retired project knowledge ${rest[1]} in ${updated.id}\n`,
|
|
408
|
+
projectId: updated.id,
|
|
409
|
+
knowledgeId: rest[1]
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
throw usageError(command === undefined
|
|
413
|
+
? "Project knowledge command is required."
|
|
414
|
+
: `Unknown command: project knowledge ${command}`);
|
|
415
|
+
}
|
|
416
|
+
function requireProject(store, reference) {
|
|
417
|
+
const project = resolveProject(store.listProjects(), reference);
|
|
418
|
+
if (project === null)
|
|
419
|
+
throw usageError(`Project not found: ${reference}.`);
|
|
420
|
+
return project;
|
|
421
|
+
}
|
|
422
|
+
function nextKnowledgeId(project) {
|
|
423
|
+
const used = new Set(project.knowledge.map(({ id }) => id));
|
|
424
|
+
for (let index = 1;; index += 1) {
|
|
425
|
+
const id = `knowledge-${index}`;
|
|
426
|
+
if (!used.has(id))
|
|
427
|
+
return id;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function parseAddArguments(args, usage) {
|
|
431
|
+
const positionals = [];
|
|
432
|
+
const aliases = [];
|
|
433
|
+
let remote;
|
|
434
|
+
let stable;
|
|
435
|
+
let development;
|
|
436
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
437
|
+
const value = args[index];
|
|
438
|
+
if (["--alias", "--remote", "--stable", "--development"].includes(value)) {
|
|
439
|
+
const next = args[index + 1];
|
|
440
|
+
if (next === undefined || next.startsWith("--"))
|
|
441
|
+
throw usageError(`${value} is required. ${usage}`);
|
|
442
|
+
if (value === "--alias")
|
|
443
|
+
aliases.push(requireText(next, value));
|
|
444
|
+
else if (value === "--remote") {
|
|
445
|
+
if (remote !== undefined)
|
|
446
|
+
throw usageError(`--remote may only be provided once. ${usage}`);
|
|
447
|
+
remote = requireText(next, value);
|
|
448
|
+
}
|
|
449
|
+
else if (value === "--stable") {
|
|
450
|
+
if (stable !== undefined)
|
|
451
|
+
throw usageError(`--stable may only be provided once. ${usage}`);
|
|
452
|
+
stable = requireText(next, value);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (development !== undefined)
|
|
456
|
+
throw usageError(`--development may only be provided once. ${usage}`);
|
|
457
|
+
development = requireText(next, value);
|
|
458
|
+
}
|
|
459
|
+
index += 1;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (value.startsWith("--"))
|
|
463
|
+
throw usageError(`Unknown option: ${value}. ${usage}`);
|
|
464
|
+
positionals.push(value);
|
|
465
|
+
}
|
|
466
|
+
if (positionals.length !== 2)
|
|
467
|
+
throw usageError(usage);
|
|
468
|
+
return {
|
|
469
|
+
name: requireText(positionals[0], "Project name"),
|
|
470
|
+
path: requireText(positionals[1], "Project path"),
|
|
471
|
+
aliases,
|
|
472
|
+
...(remote === undefined ? {} : { remote }),
|
|
473
|
+
...(stable === undefined ? {} : { stable }),
|
|
474
|
+
...(development === undefined ? {} : { development })
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function parseProjectUpdateArguments(args, usage) {
|
|
478
|
+
const positionals = [];
|
|
479
|
+
const aliases = [];
|
|
480
|
+
let aliasesProvided = false;
|
|
481
|
+
let clearAliases = false;
|
|
482
|
+
let remote;
|
|
483
|
+
let stable;
|
|
484
|
+
let development;
|
|
485
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
486
|
+
const value = args[index];
|
|
487
|
+
if (value === "--clear-aliases") {
|
|
488
|
+
clearAliases = true;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (value === "--clear-remote") {
|
|
492
|
+
if (remote !== undefined)
|
|
493
|
+
throw usageError(`Remote may only be changed once. ${usage}`);
|
|
494
|
+
remote = null;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (["--alias", "--remote", "--stable", "--development"].includes(value)) {
|
|
498
|
+
const next = args[index + 1];
|
|
499
|
+
if (next === undefined || next.startsWith("--"))
|
|
500
|
+
throw usageError(`${value} is required. ${usage}`);
|
|
501
|
+
if (value === "--alias") {
|
|
502
|
+
aliasesProvided = true;
|
|
503
|
+
aliases.push(requireText(next, value));
|
|
504
|
+
}
|
|
505
|
+
else if (value === "--remote") {
|
|
506
|
+
if (remote !== undefined)
|
|
507
|
+
throw usageError(`Remote may only be changed once. ${usage}`);
|
|
508
|
+
remote = requireText(next, value);
|
|
509
|
+
}
|
|
510
|
+
else if (value === "--stable") {
|
|
511
|
+
if (stable !== undefined)
|
|
512
|
+
throw usageError(`--stable may only be provided once. ${usage}`);
|
|
513
|
+
stable = requireText(next, value);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
if (development !== undefined)
|
|
517
|
+
throw usageError(`--development may only be provided once. ${usage}`);
|
|
518
|
+
development = requireText(next, value);
|
|
519
|
+
}
|
|
520
|
+
index += 1;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (value.startsWith("--"))
|
|
524
|
+
throw usageError(`Unknown option: ${value}. ${usage}`);
|
|
525
|
+
positionals.push(value);
|
|
526
|
+
}
|
|
527
|
+
if (positionals.length !== 1 || (aliasesProvided && clearAliases))
|
|
528
|
+
throw usageError(usage);
|
|
529
|
+
if (!aliasesProvided && !clearAliases && remote === undefined
|
|
530
|
+
&& stable === undefined && development === undefined)
|
|
531
|
+
throw usageError(usage);
|
|
532
|
+
return {
|
|
533
|
+
reference: requireText(positionals[0], "Project reference"),
|
|
534
|
+
...(aliasesProvided || clearAliases ? { aliases: clearAliases ? [] : aliases } : {}),
|
|
535
|
+
...(remote === undefined ? {} : { remote }),
|
|
536
|
+
...(stable === undefined ? {} : { stable }),
|
|
537
|
+
...(development === undefined ? {} : { development })
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function parseKnowledgeUpdateArguments(args, usage) {
|
|
541
|
+
const positionals = [];
|
|
542
|
+
let title;
|
|
543
|
+
let body;
|
|
544
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
545
|
+
const value = args[index];
|
|
546
|
+
if (value === "--title" || value === "--body") {
|
|
547
|
+
const next = args[index + 1];
|
|
548
|
+
if (next === undefined || next.startsWith("--"))
|
|
549
|
+
throw usageError(`${value} is required. ${usage}`);
|
|
550
|
+
if (value === "--title") {
|
|
551
|
+
if (title !== undefined)
|
|
552
|
+
throw usageError(`--title may only be provided once. ${usage}`);
|
|
553
|
+
title = requireText(next, value);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
if (body !== undefined)
|
|
557
|
+
throw usageError(`--body may only be provided once. ${usage}`);
|
|
558
|
+
body = requireText(next, value);
|
|
559
|
+
}
|
|
560
|
+
index += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (value.startsWith("--"))
|
|
564
|
+
throw usageError(`Unknown option: ${value}. ${usage}`);
|
|
565
|
+
positionals.push(value);
|
|
566
|
+
}
|
|
567
|
+
if (positionals.length !== 2 || (title === undefined && body === undefined)) {
|
|
568
|
+
throw usageError(usage);
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
project: positionals[0],
|
|
572
|
+
id: positionals[1],
|
|
573
|
+
...(title === undefined ? {} : { title }),
|
|
574
|
+
...(body === undefined ? {} : { body })
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function parseCloneArguments(args, usage) {
|
|
578
|
+
const positionals = [];
|
|
579
|
+
const aliases = [];
|
|
580
|
+
let stable;
|
|
581
|
+
let development;
|
|
582
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
583
|
+
const value = args[index];
|
|
584
|
+
if (["--alias", "--stable", "--development"].includes(value)) {
|
|
585
|
+
const next = args[index + 1];
|
|
586
|
+
if (next === undefined || next.startsWith("--")) {
|
|
587
|
+
throw usageError(`${value} is required. ${usage}`);
|
|
588
|
+
}
|
|
589
|
+
if (value === "--alias")
|
|
590
|
+
aliases.push(requireText(next, value));
|
|
591
|
+
else if (value === "--stable") {
|
|
592
|
+
if (stable !== undefined)
|
|
593
|
+
throw usageError(`--stable may only be provided once. ${usage}`);
|
|
594
|
+
stable = requireText(next, value);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
if (development !== undefined) {
|
|
598
|
+
throw usageError(`--development may only be provided once. ${usage}`);
|
|
599
|
+
}
|
|
600
|
+
development = requireText(next, value);
|
|
601
|
+
}
|
|
602
|
+
index += 1;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (value.startsWith("--"))
|
|
606
|
+
throw usageError(`Unknown option: ${value}. ${usage}`);
|
|
607
|
+
positionals.push(value);
|
|
608
|
+
}
|
|
609
|
+
if (positionals.length !== 2)
|
|
610
|
+
throw usageError(usage);
|
|
611
|
+
return {
|
|
612
|
+
name: requireText(positionals[0], "Project name"),
|
|
613
|
+
remote: requireText(positionals[1], "Project remote"),
|
|
614
|
+
aliases,
|
|
615
|
+
...(stable === undefined ? {} : { stable }),
|
|
616
|
+
...(development === undefined ? {} : { development })
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function parseSingleOption(args, option, usage) {
|
|
620
|
+
const positionals = [];
|
|
621
|
+
let found;
|
|
622
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
623
|
+
const value = args[index];
|
|
624
|
+
if (value === option) {
|
|
625
|
+
if (found !== undefined)
|
|
626
|
+
throw usageError(`${option} may only be provided once. ${usage}`);
|
|
627
|
+
const next = args[index + 1];
|
|
628
|
+
if (next === undefined || next.startsWith("--"))
|
|
629
|
+
throw usageError(`${option} is required. ${usage}`);
|
|
630
|
+
found = requireText(next, option);
|
|
631
|
+
index += 1;
|
|
632
|
+
}
|
|
633
|
+
else if (value.startsWith("--")) {
|
|
634
|
+
throw usageError(`Unknown option: ${value}. ${usage}`);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
positionals.push(value);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (found === undefined)
|
|
641
|
+
throw usageError(`${option} is required. ${usage}`);
|
|
642
|
+
return { positionals, value: found };
|
|
643
|
+
}
|
|
644
|
+
function requireText(value, label) {
|
|
645
|
+
const normalized = value.trim();
|
|
646
|
+
if (normalized.length === 0 || normalized.includes("\0")) {
|
|
647
|
+
throw usageError(`${label} is required.`);
|
|
648
|
+
}
|
|
649
|
+
return normalized;
|
|
650
|
+
}
|