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 { 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(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']), }; }