ktworkflow-mcp/src/core/cli.ts

315 lines
12 KiB
TypeScript

import type {
ChangeDocSyncInput,
DeployObservationInput,
GlobalCodeReviewInput,
NapcatRemoteDevHandoffInput,
NapcatRuntimeReleaseReadinessInput,
NapcatSyncCandidateReviewInput,
NasCodexBootstrapPlanInput,
ObsidianContextInput,
ObsidianSyncInput,
ObsidianValidateInput,
WorkstreamCloseoutInput,
WorkstreamReusablePattern,
} from '../types.js';
import { workstreamReusablePatternValues } from './constants.js';
import type { NapcatUpstreamAuditInput } from '../tools/napcatAutomation.types.js';
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
return 'all';
}
const modeIndex = argv.findIndex(
(item) => item === '--contentScanMode' || item === '--content-scan-mode',
);
return modeIndex >= 0 && argv[modeIndex + 1] === 'all' ? 'all' : 'changed';
}
export function parseGlobalReviewCliArgs(
argv: string[],
): Pick<GlobalCodeReviewInput, 'contentScanMode' | 'includeContentScan' | 'includeRootScan'> {
return {
contentScanMode: parseContentScanMode(argv),
includeContentScan: !argv.includes('--no-content-scan'),
includeRootScan: !argv.includes('--no-root-scan'),
};
}
/**
* Parses the change documentation sync CLI mode into the same input shape as the MCP tool.
* @param argv - Full process argv including `--change-doc-sync` and optional project/file flags.
* @returns Project key plus explicitly supplied changed files; absent files make the tool infer git status.
*/
export function parseChangeDocSyncCliArgs(argv: string[]): ChangeDocSyncInput {
return {
changedFiles: readOptionList(argv, [
'--changed-file',
'--changed-files',
'--file',
'--files',
]),
project: readOption(argv, ['--project']),
};
}
function readOption(argv: string[], names: string[]): string | undefined {
for (const name of names) {
const keyValue = argv.find((item) => item.startsWith(`${name}=`));
if (keyValue) return keyValue.slice(name.length + 1);
const index = argv.indexOf(name);
if (index >= 0 && argv[index + 1] && !argv[index + 1].startsWith('--')) {
return argv[index + 1];
}
}
return undefined;
}
function readOptionList(argv: string[], names: string[]): string[] {
const values: string[] = [];
names.forEach((name) => {
argv.forEach((item, index) => {
if (item.startsWith(`${name}=`)) {
values.push(item.slice(name.length + 1));
} else if (item === name && argv[index + 1] && !argv[index + 1].startsWith('--')) {
values.push(argv[index + 1]);
}
});
});
return values
.flatMap((value) => value.split(','))
.map((value) => value.trim())
.filter(Boolean);
}
/**
* Checks whether any supported boolean CLI flag is present in the raw argv list.
* @param argv - Full process argv including node, script, mode flag, and user options.
* @param names - Flag names to accept in dashed or camelCase form.
* @returns True when the caller provided one of the named flags.
*/
function hasFlag(argv: string[], names: string[]): boolean {
return names.some((name) => argv.includes(name));
}
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
const patternSet = new Set<string>(workstreamReusablePatternValues);
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
.filter((pattern): pattern is WorkstreamReusablePattern => patternSet.has(pattern));
const cleanupFinalDeleted = Number(
readOption(argv, ['--cleanup-final-deleted', '--cleanupFinalDeleted']),
);
return {
changedProjects: readOptionList(argv, ['--project', '--changed-project']),
cleanupEvidence: readOptionList(argv, ['--cleanup', '--cleanup-evidence']),
cleanupFinalDeleted: Number.isFinite(cleanupFinalDeleted) ? cleanupFinalDeleted : undefined,
docSyncEvidence: readOptionList(argv, ['--doc-sync', '--doc-sync-evidence']),
ktWorkflowUpdated: argv.includes('--kt-workflow-updated'),
problemRecords: readOptionList(argv, ['--problem', '--problem-record']),
reviewEvidence: readOptionList(argv, ['--review', '--review-evidence']),
reusablePatterns: patterns,
stableSolutions: readOptionList(argv, ['--solution', '--stable-solution']),
superpowersReviewEvidence: readOptionList(argv, [
'--superpowers-review',
'--superpowers-review-evidence',
'--sp-review',
]),
title: readOption(argv, ['--title']),
upgradeNotes: readOption(argv, ['--upgrade-notes']),
verificationEvidence: readOptionList(argv, ['--verification', '--verification-evidence']),
};
}
export function parseDeployObservationCliArgs(argv: string[]): DeployObservationInput {
const sshPort = Number(readOption(argv, ['--ssh-port', '--sshPort']));
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
buildNumber: readOption(argv, ['--build', '--build-number', '--buildNumber']),
container: readOption(argv, ['--container']),
deployment: readOption(argv, ['--deployment']),
execute: argv.includes('--execute'),
expectedCommit: readOption(argv, [
'--commit',
'--expected-commit',
'--expectedCommit',
]),
healthUrl: readOption(argv, ['--health-url', '--healthUrl']),
imageTag: readOption(argv, ['--image-tag', '--imageTag']),
jenkinsHome: readOption(argv, ['--jenkins-home', '--jenkinsHome']),
jobName: readOption(argv, ['--job', '--job-name', '--jobName']),
kubeconfigPath: readOption(argv, [
'--kubeconfig',
'--kubeconfig-path',
'--kubeconfigPath',
]),
namespace: readOption(argv, ['--namespace']),
project: readOption(argv, ['--project']),
selector: readOption(argv, ['--selector']),
smoke: readOption(argv, ['--smoke']),
sshPort: Number.isFinite(sshPort) ? sshPort : undefined,
sshTarget: readOption(argv, ['--ssh', '--ssh-target', '--sshTarget']),
};
}
/**
* Parses the NapCat upstream audit CLI mode into the deterministic audit builder input.
* @param argv - Full process argv from `src/server.ts`; `--execute` and `--use-codex` must both be explicit before Codex can run.
* @returns Upstream audit input with dry-run defaults for all optional metadata lists.
*/
export function parseNapcatUpstreamAuditCliArgs(
argv: string[],
): NapcatUpstreamAuditInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
createCandidateBranch: hasFlag(argv, ['--create-candidate-branch', '--createCandidateBranch']),
dryMergeConflict: hasFlag(argv, ['--dry-merge-conflict', '--dryMergeConflict']),
execute: hasFlag(argv, ['--execute']),
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
forkPatchFiles: readOptionList(argv, [
'--fork-patch-file',
'--fork-patch-files',
'--forkPatchFile',
'--forkPatchFiles',
]),
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
lastAcceptedUpstreamBase: readOption(argv, [
'--last-accepted-upstream-base',
'--lastAcceptedUpstreamBase',
]),
outputSchemaPath: readOption(argv, ['--output-schema', '--outputSchemaPath']),
upstreamChangedFiles: readOptionList(argv, [
'--upstream-changed-file',
'--upstream-changed-files',
'--upstreamChangedFile',
'--upstreamChangedFiles',
]),
upstreamReleaseTag: readOption(argv, [
'--upstream-release-tag',
'--upstream-release-ref',
'--upstreamReleaseTag',
]),
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
/**
* Parses common dry-run NapCat prompt tool options shared by candidate review, readiness, and handoff modes.
* @param argv - Full process argv from a NapCat automation CLI script.
* @returns Prompt skeleton input with `execute` false unless explicitly requested by the caller.
*/
function parseNapcatPromptToolCliArgs(argv: string[]): NapcatSyncCandidateReviewInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
candidateBranch: readOption(argv, ['--candidate-branch', '--candidateBranch']),
execute: hasFlag(argv, ['--execute']),
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
lastAcceptedUpstreamBase: readOption(argv, [
'--last-accepted-upstream-base',
'--lastAcceptedUpstreamBase',
]),
upstreamReleaseTag: readOption(argv, [
'--upstream-release-tag',
'--upstream-release-ref',
'--upstreamReleaseTag',
]),
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
/**
* Parses the NapCat sync-candidate review CLI mode.
* @param argv - Full process argv from `pnpm run napcat-sync-candidate-review`.
* @returns Dry-run prompt skeleton input for human/Codex candidate review handoff.
*/
export function parseNapcatSyncCandidateReviewCliArgs(
argv: string[],
): NapcatSyncCandidateReviewInput {
return parseNapcatPromptToolCliArgs(argv);
}
/**
* Parses the NapCat runtime release readiness CLI mode.
* @param argv - Full process argv from `pnpm run napcat-runtime-release-readiness`.
* @returns Dry-run prompt skeleton input with image/profile hints and no default mutation.
*/
export function parseNapcatRuntimeReleaseReadinessCliArgs(
argv: string[],
): NapcatRuntimeReleaseReadinessInput {
return {
...parseNapcatPromptToolCliArgs(argv),
apiImageTag: readOption(argv, ['--api-image-tag', '--apiImageTag']),
napcatImageTag: readOption(argv, ['--napcat-image-tag', '--napcatImageTag']),
profile: readOption(argv, ['--profile']),
};
}
/**
* Parses the NapCat remote development handoff CLI mode.
* @param argv - Full process argv from `src/server.ts --napcat-remote-dev-handoff`.
* @returns Dry-run prompt skeleton input for a remote workspace handoff packet.
*/
export function parseNapcatRemoteDevHandoffCliArgs(
argv: string[],
): NapcatRemoteDevHandoffInput {
return {
...parseNapcatPromptToolCliArgs(argv),
targetHost: readOption(argv, ['--target-host', '--targetHost']),
};
}
/**
* Parses the NAS Codex bootstrap planning CLI mode.
* @param argv - Full process argv from `pnpm run nas-codex-bootstrap`.
* @returns Dry-run bootstrap plan input; `execute` is false unless the caller explicitly opts in.
*/
export function parseNasCodexBootstrapCliArgs(
argv: string[],
): NasCodexBootstrapPlanInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
execute: hasFlag(argv, ['--execute']),
serviceName: readOption(argv, ['--service-name', '--serviceName']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
return {
checkLayout: !argv.includes('--no-layout'),
failOnWarnings: argv.includes('--fail-on-warnings'),
includeMarkdownLinks: !argv.includes('--no-markdown-links'),
includeStaleReferences: !argv.includes('--no-stale-references'),
};
}
export function parseObsidianSyncCliArgs(argv: string[]): ObsidianSyncInput {
return {
...parseObsidianCliArgs(argv),
includeValidation: !argv.includes('--no-validation'),
};
}
export function parseObsidianContextCliArgs(argv: string[]): ObsidianContextInput {
const maxDocuments = Number(readOption(argv, ['--max-documents', '--maxDocuments']));
return {
includeCanvas: !argv.includes('--no-canvas'),
includeContent: !argv.includes('--no-content'),
maxDocuments: Number.isFinite(maxDocuments) ? maxDocuments : undefined,
module: readOption(argv, ['--module']),
query: readOption(argv, ['--query']),
};
}