diff --git a/CLI/cmux.swift b/CLI/cmux.swift index b257328d25c8..fd22dd063559 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -5325,7 +5325,7 @@ struct CMUXCLI { return } } - if command == "setup-hooks" || command == "uninstall-hooks" { try runSetupHooks(uninstall: command == "uninstall-hooks"); return } // Backwards compatibility for old hook setup docs/scripts. + if command == "setup-hooks" || command == "uninstall-hooks" { try runSetupHooks(uninstall: command == "uninstall-hooks", arguments: try Self.parseHooksSetupArguments(commandArgs)); return } // Backwards compatibility for old hook setup docs/scripts. if (command == "codex-hook" || command == "feed-hook"), processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false, !commandArgs.contains(where: { $0 == "--workspace" || $0 == "--surface" || $0.hasPrefix("--workspace=") || $0.hasPrefix("--surface=") }) { print("{}"); return } // Backwards compatibility for old installed hooks outside cmux terminals. if command == "hooks" { @@ -42243,14 +42243,14 @@ export default { case "setup": try runSetupHooks( uninstall: false, - positionalAgentFilter: try Self.hooksSetupPositionalAgentFilter(from: Array(commandArgs.dropFirst())) + arguments: try Self.parseHooksSetupArguments(Array(commandArgs.dropFirst())) ) return true case "uninstall": try runSetupHooks( uninstall: true, - positionalAgentFilter: try Self.hooksSetupPositionalAgentFilter(from: Array(commandArgs.dropFirst())) + arguments: try Self.parseHooksSetupArguments(Array(commandArgs.dropFirst())) ) return true @@ -42430,34 +42430,71 @@ export default { } } - private static func hooksSetupPositionalAgentFilter(from args: [String]) throws -> String? { - var skipNext = false + /// The target and mode `cmux hooks setup`/`uninstall` (and the legacy + /// `setup-hooks`/`uninstall-hooks`) were asked for. + private struct HooksSetupArguments { + var flagAgent: String? var positionalAgent: String? - for arg in args { - if skipNext { - skipNext = false - continue + var uninstall = false + } + + /// Parses setup/uninstall arguments, rejecting anything it doesn't know + /// so a typo can't fall back to every agent. + private static func parseHooksSetupArguments(_ args: [String]) throws -> HooksSetupArguments { + var parsed = HooksSetupArguments() + func setFlagAgent(_ value: String) throws { + guard !value.isEmpty, !value.hasPrefix("-") else { + throw CLIError(message: String( + localized: "cli.hooks.setup.error.agentRequiresValue", + defaultValue: "--agent requires a value. Usage: cmux hooks setup [agent] [--agent ] [--yes|-y]" + )) + } + if let existing = parsed.flagAgent, existing != value { + throw CLIError(message: String( + localized: "cli.hooks.setup.error.conflictingAgent", + defaultValue: "--agent was given more than once with different values. Specify one agent." + )) } + parsed.flagAgent = value + } + var index = 0 + while index < args.count { + let arg = args[index] + index += 1 switch arg { case "--agent": - skipNext = true - case "--yes", "-y", "--uninstall": + try setFlagAgent(index < args.count ? args[index] : "") + index += 1 + case "--yes", "-y": continue + case "--uninstall": + parsed.uninstall = true default: - if !arg.hasPrefix("-") { - if positionalAgent != nil { + if arg.hasPrefix("--agent=") { + try setFlagAgent(String(arg.dropFirst("--agent=".count))) + } else if arg.hasPrefix("-") { + let name = arg.split(separator: "=", maxSplits: 1).first.map(String.init) ?? arg + throw CLIError(message: String.localizedStringWithFormat( + String( + localized: "cli.hooks.setup.error.unknownOption", + defaultValue: "Unknown option %@. Usage: cmux hooks setup [agent] [--agent ] [--yes|-y]" + ), + name + )) + } else { + if parsed.positionalAgent != nil { throw CLIError(message: "Too many hooks targets: specify at most one positional agent") } - positionalAgent = arg + parsed.positionalAgent = arg } } } - return positionalAgent + return parsed } - private func runSetupHooks(uninstall: Bool = false, positionalAgentFilter: String? = nil) throws { - let args = ProcessInfo.processInfo.arguments - let flagAgentFilter = optionValue(args, name: "--agent") + private func runSetupHooks(uninstall: Bool = false, arguments: HooksSetupArguments) throws { + let flagAgentFilter = arguments.flagAgent + let positionalAgentFilter = arguments.positionalAgent if let flagAgentFilter, let positionalAgentFilter { guard let flagDef = Self.agentDef(named: flagAgentFilter) else { throw CLIError(message: "Unknown hooks target: \(flagAgentFilter)") @@ -42479,7 +42516,7 @@ export default { } else { agentFilterDef = nil } - let isUninstall = uninstall || args.contains("--uninstall") + let isUninstall = uninstall || arguments.uninstall let fm = FileManager.default let verb = isUninstall ? "uninstalling" : "installing" print("cmux hooks \(isUninstall ? "uninstall" : "setup"): \(verb) agent hooks") diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index f5fb9110718c..c7113553a28a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -235781,7 +235781,7 @@ } } }, - "machines.new.plan.loading": { + "machines.new.plan.loading": { "extractionState": "manual", "localizations": { "en": { @@ -235906,7 +235906,7 @@ } } }, - "machines.new.plan.retry": { + "machines.new.plan.retry": { "extractionState": "manual", "localizations": { "en": { @@ -236031,7 +236031,7 @@ } } }, - "machines.new.plan.error": { + "machines.new.plan.error": { "extractionState": "manual", "localizations": { "en": { @@ -358728,131 +358728,6 @@ } } }, - "settings.search.alias.setting.terminal.copy-confirmation": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse تأكيد نسخ حافظة تحديد" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potvrda kopirano međuspremnik odabir" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekræftelse kopieret udklipsholder markering" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse Bestätigung kopiert Zwischenablage Auswahl" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmación copiado portapapeles selección" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmation copié presse-papiers sélection" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse conferma copiato appunti selezione" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 コピー クリップボード 選択" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse បញ្ជាក់ ចម្លង ក្ដារតម្រៀប ជ្រើស" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 확인 복사됨 클립보드 선택" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekreftelse kopiert utklippstavle markering" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potwierdzenie skopiowano schowek zaznaczenie" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmação copiado área de transferência seleção" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse подтверждение скопировано буфер обмена выделение" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse ยืนยัน คัดลอก คลิปบอร์ด เลือก" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse onay kopyalandı pano seçim" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse підтвердження скопійовано буфер обміну виділення" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 确认 已复制 剪贴板 选择" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 已複製 剪貼簿 選取" - } - } - } - }, "settings.search.alias.setting.terminal.reflow-hard-wrap-on-copy": { "extractionState": "manual", "localizations": { @@ -366360,256 +366235,6 @@ } } }, - "settings.terminal.showCopyConfirmation": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "إظهار تأكيد النسخ" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Prikaži potvrdu kopiranja" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Vis kopieringsbekræftelse" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopierbestätigung anzeigen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Show Copy Confirmation" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar confirmación de copia" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher la confirmation de copie" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Mostra conferma di copia" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピーの確認を表示" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "បង្ហាញការបញ្ជាក់ការចម្លង" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사 확인 표시" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Vis kopieringsbekreftelse" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Pokazuj potwierdzenie kopiowania" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Mostrar confirmação de cópia" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Показывать подтверждение копирования" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แสดงการยืนยันการคัดลอก" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyalama onayını göster" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Показувати підтвердження копіювання" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示复制确认" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "顯示複製確認" - } - } - } - }, - "settings.terminal.showCopyConfirmation.subtitle": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يعرض «تم النسخ إلى الحافظة» لفترة وجيزة أسفل الطرفية عندما يؤدي تحديد النص إلى نسخه." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kratko prikazuje „Kopirano u međuspremnik“ na dnu terminala kada se odabrani tekst kopira." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Viser kortvarigt „Kopieret til udklipsholder“ nederst i en terminal, når markering af tekst kopierer den." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Zeigt unten im Terminal kurz „In die Zwischenablage kopiert“ an, wenn durch Auswählen von Text kopiert wird." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Briefly shows “Copied to clipboard” at the bottom of a terminal when selecting text copies it." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Muestra brevemente «Copiado al portapapeles» en la parte inferior de una terminal cuando al seleccionar texto se copia." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Affiche brièvement « Copié dans le presse-papiers » en bas d’un terminal lorsque la sélection de texte le copie." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Mostra brevemente «Copiato negli appunti» in fondo a un terminale quando la selezione del testo lo copia." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "テキストを選択してコピーしたときに、ターミナルの下部に「クリップボードにコピーしました」を短く表示します。" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "បង្ហាញ «បានចម្លងទៅក្ដារតម្រៀប» មួយភ្លែតនៅខាងក្រោម terminal នៅពេលការជ្រើសអត្ថបទចម្លងវា។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "텍스트를 선택해 복사하면 터미널 하단에 “클립보드에 복사됨”을 잠시 표시합니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Viser kort «Kopiert til utklippstavlen» nederst i en terminal når markering av tekst kopierer den." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Na chwilę wyświetla „Skopiowano do schowka” u dołu terminala, gdy zaznaczenie tekstu go kopiuje." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Mostra brevemente “Copiado para a área de transferência” na parte inferior do terminal quando selecionar texto o copia." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Ненадолго показывает «Скопировано в буфер обмена» внизу терминала, когда выделение текста копирует его." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แสดง “คัดลอกไปยังคลิปบอร์ดแล้ว” ชั่วครู่ที่ด้านล่างของเทอร์มินัลเมื่อการเลือกข้อความคัดลอกข้อความนั้น" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Metin seçimi onu kopyaladığında terminalin altında kısa süreliğine “Panoya kopyalandı” gösterir." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Ненадовго показує «Скопійовано в буфер обміну» внизу термінала, коли виділення тексту копіює його." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择文本并复制时,在终端底部短暂显示“已复制到剪贴板”。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "選取文字並複製時,在終端機底部短暫顯示「已複製到剪貼簿」。" - } - } - } - }, "settings.terminal.reflowHardWrapOnCopy": { "extractionState": "manual", "localizations": { @@ -619288,6 +618913,558 @@ } } }, + "cli.hooks.setup.error.agentRequiresValue": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "--agent requires a value. Usage: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يتطلب --agent قيمة. الاستخدام: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "--agent erfordert einen Wert. Verwendung: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "--agent requiere un valor. Uso: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "--agent nécessite une valeur. Utilisation : cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "--agent には値が必要です。使い方: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "--agent에는 값이 필요합니다. 사용법: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "--agent 需要一个值。用法:cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "--agent 需要一個值。用法:cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + } + } + }, + "cli.hooks.setup.error.unknownOption": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unknown option %@. Usage: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "خيار غير معروف %@. الاستخدام: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Unbekannte Option %@. Verwendung: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opción desconocida %@. Uso: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Option inconnue %@. Utilisation : cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "不明なオプション %@。使い方: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "알 수 없는 옵션 %@. 사용법: cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未知选项 %@。用法:cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "未知的選項 %@。用法:cmux hooks setup [agent] [--agent ] [--yes|-y]" + } + } + } + }, + "cli.hooks.setup.error.conflictingAgent": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "--agent was given more than once with different values. Specify one agent." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تم تمرير --agent أكثر من مرة بقيم مختلفة. حدد وكيلًا واحدًا." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "--agent wurde mehrmals mit unterschiedlichen Werten angegeben. Gib einen Agenten an." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "--agent se indicó más de una vez con valores distintos. Indica un solo agente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "--agent a été indiqué plusieurs fois avec des valeurs différentes. Indiquez un seul agent." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "--agent が異なる値で複数回指定されました。エージェントを 1 つだけ指定してください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "--agent가 서로 다른 값으로 여러 번 지정되었습니다. 에이전트를 하나만 지정하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "--agent 被多次指定且值不同。请只指定一个代理。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "--agent 被多次指定且值不同。請只指定一個代理程式。" + } + } + } + }, + "settings.search.alias.setting.terminal.copy-confirmation": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse تأكيد نسخ حافظة تحديد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potvrda kopirano međuspremnik odabir" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekræftelse kopieret udklipsholder markering" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse Bestätigung kopiert Zwischenablage Auswahl" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmación copiado portapapeles selección" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmation copié presse-papiers sélection" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse conferma copiato appunti selezione" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 コピー クリップボード 選択" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse បញ្ជាក់ ចម្លង ក្ដារតម្រៀប ជ្រើស" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 확인 복사됨 클립보드 선택" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekreftelse kopiert utklippstavle markering" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potwierdzenie skopiowano schowek zaznaczenie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmação copiado área de transferência seleção" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse подтверждение скопировано буфер обмена выделение" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse ยืนยัน คัดลอก คลิปบอร์ด เลือก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse onay kopyalandı pano seçim" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse підтвердження скопійовано буфер обміну виділення" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 确认 已复制 剪贴板 选择" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 已複製 剪貼簿 選取" + } + } + } + }, + "settings.terminal.showCopyConfirmation": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار تأكيد النسخ" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prikaži potvrdu kopiranja" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Vis kopieringsbekræftelse" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kopierbestätigung anzeigen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Copy Confirmation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar confirmación de copia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher la confirmation de copie" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra conferma di copia" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コピーの確認を表示" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បង្ហាញការបញ្ជាក់ការចម្លង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "복사 확인 표시" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Vis kopieringsbekreftelse" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Pokazuj potwierdzenie kopiowania" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar confirmação de cópia" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Показывать подтверждение копирования" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แสดงการยืนยันการคัดลอก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kopyalama onayını göster" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Показувати підтвердження копіювання" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示复制确认" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示複製確認" + } + } + } + }, + "settings.terminal.showCopyConfirmation.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يعرض «تم النسخ إلى الحافظة» لفترة وجيزة أسفل الطرفية عندما يؤدي تحديد النص إلى نسخه." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kratko prikazuje „Kopirano u međuspremnik“ na dnu terminala kada se odabrani tekst kopira." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Viser kortvarigt „Kopieret til udklipsholder“ nederst i en terminal, når markering af tekst kopierer den." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt unten im Terminal kurz „In die Zwischenablage kopiert“ an, wenn durch Auswählen von Text kopiert wird." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Briefly shows “Copied to clipboard” at the bottom of a terminal when selecting text copies it." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra brevemente «Copiado al portapapeles» en la parte inferior de una terminal cuando al seleccionar texto se copia." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche brièvement « Copié dans le presse-papiers » en bas d’un terminal lorsque la sélection de texte le copie." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra brevemente «Copiato negli appunti» in fondo a un terminale quando la selezione del testo lo copia." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テキストを選択してコピーしたときに、ターミナルの下部に「クリップボードにコピーしました」を短く表示します。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បង្ហាញ «បានចម្លងទៅក្ដារតម្រៀប» មួយភ្លែតនៅខាងក្រោម terminal នៅពេលការជ្រើសអត្ថបទចម្លងវា។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "텍스트를 선택해 복사하면 터미널 하단에 “클립보드에 복사됨”을 잠시 표시합니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Viser kort «Kopiert til utklippstavlen» nederst i en terminal når markering av tekst kopierer den." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Na chwilę wyświetla „Skopiowano do schowka” u dołu terminala, gdy zaznaczenie tekstu go kopiuje." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostra brevemente “Copiado para a área de transferência” na parte inferior do terminal quando selecionar texto o copia." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ненадолго показывает «Скопировано в буфер обмена» внизу терминала, когда выделение текста копирует его." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แสดง “คัดลอกไปยังคลิปบอร์ดแล้ว” ชั่วครู่ที่ด้านล่างของเทอร์มินัลเมื่อการเลือกข้อความคัดลอกข้อความนั้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Metin seçimi onu kopyaladığında terminalin altında kısa süreliğine “Panoya kopyalandı” gösterir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ненадовго показує «Скопійовано в буфер обміну» внизу термінала, коли виділення тексту копіює його." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "选择文本并复制时,在终端底部短暂显示“已复制到剪贴板”。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "選取文字並複製時,在終端機底部短暫顯示「已複製到剪貼簿」。" + } + } + } + }, "terminal.copyConfirmation.title": { "extractionState": "manual", "localizations": { diff --git a/tests/test-execution.toml b/tests/test-execution.toml index 0bf8ed273b1d..3c774c27719f 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -1491,6 +1491,11 @@ path = "tests/test_claude_hook_spool.py" lane = "macos-cli-product" requirements = ["cmux-cli"] +[[test]] +path = "tests/test_cli_hooks_setup_arguments.py" +lane = "macos-cli-product" +requirements = ["cmux-cli"] + [[test]] path = "tests/test_ci_machine_failure.py" lane = "linux-guard" diff --git a/tests/test_ci_test_execution_registry.py b/tests/test_ci_test_execution_registry.py index d5f6f7028e27..2465270c49aa 100644 --- a/tests/test_ci_test_execution_registry.py +++ b/tests/test_ci_test_execution_registry.py @@ -63,8 +63,14 @@ def test_cli_product_lane_selects_the_hook_spool_regression(self) -> None: "--lane", "macos-cli-product", "--list"], cwd=ROOT, capture_output=True, text=True, check=True, ) - self.assertEqual(result.stdout.splitlines(), ["tests/test_claude_hook_spool.py"]) - errors, _, _ = validator.validate(ROOT, added={"tests/test_claude_hook_spool.py"}) + self.assertEqual( + result.stdout.splitlines(), + ["tests/test_claude_hook_spool.py", "tests/test_cli_hooks_setup_arguments.py"], + ) + errors, _, _ = validator.validate( + ROOT, + added={"tests/test_claude_hook_spool.py", "tests/test_cli_hooks_setup_arguments.py"}, + ) self.assertEqual(errors, []) def make_root(self, *, tests: list[str], registry: str) -> Path: diff --git a/tests/test_cli_hooks_setup_arguments.py b/tests/test_cli_hooks_setup_arguments.py new file mode 100644 index 000000000000..03aaa5b658aa --- /dev/null +++ b/tests/test_cli_hooks_setup_arguments.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""`cmux hooks setup` and `uninstall` reject options they don't understand. + +An unknown or valueless option used to be skipped, so the command fell back +to every agent and rewrote their configs. Each case runs the real CLI against +a temporary home with no agent binaries on PATH. Pi hooks install without a +binary, so an unfiltered setup always writes Pi's extension there; a rejected +command must exit non-zero and write nothing. +""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from claude_teams_test_utils import resolve_cmux_cli + + +class HooksSetupArgumentTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='cmux-hooks-args-') + self.addCleanup(self.temp.cleanup) + self.home = Path(self.temp.name) / 'home' + self.home.mkdir() + self.cli = str(resolve_cmux_cli()) + + def run_cli(self, *args): + # A minimal environment so no agent config override points outside + # the temporary home. + env = { + 'HOME': str(self.home), + 'CFFIXED_USER_HOME': str(self.home), + 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin', + 'TMPDIR': os.environ.get('TMPDIR', '/tmp'), + 'CMUX_CLI_SENTRY_DISABLED': '1', + 'CMUX_SOCKET_PATH': str(Path(self.temp.name) / 'no-socket.sock'), + } + # No stdin, so a confirmation prompt can't wait on the test runner. + return subprocess.run([self.cli, *args], env=env, stdin=subprocess.DEVNULL, + capture_output=True, text=True, timeout=30, check=False) + + def written_files(self): + return sorted(str(p.relative_to(self.home)) for p in self.home.rglob('*') if p.is_file()) + + def assert_rejected(self, args, message): + result = self.run_cli(*args) + output = result.stdout + result.stderr + self.assertNotEqual(result.returncode, 0, f'{args} should fail:\n{output}') + self.assertIn(message, output, f'{args}:\n{output}') + self.assertEqual(self.written_files(), [], f'{args} wrote files:\n{output}') + + def test_unknown_option_is_rejected_before_writing(self): + for subcommand in ('setup', 'uninstall'): + for args in (['--agnt', 'codex'], ['--yes', '--agnt=codex']): + with self.subTest(subcommand=subcommand, args=args): + self.assert_rejected(['hooks', subcommand, *args], 'Unknown option --agnt') + + def test_agent_option_requires_a_value(self): + for args in (['hooks', 'setup', '--agent'], + ['hooks', 'setup', '--agent', '--yes'], + ['hooks', 'setup', '--agent='], + ['hooks', 'uninstall', '--agent']): + with self.subTest(args=args): + self.assert_rejected(args, '--agent requires a value') + + def test_repeated_agent_with_different_values_is_rejected(self): + self.assert_rejected(['hooks', 'setup', '--agent', 'codex', '--agent', 'pi'], + '--agent was given more than once with different values') + + def test_positional_and_agent_option_must_agree(self): + self.assert_rejected(['hooks', 'setup', 'codex', '--agent', 'pi'], 'Conflicting hooks target') + + def test_legacy_aliases_reject_unknown_options(self): + for command in ('setup-hooks', 'uninstall-hooks'): + with self.subTest(command=command): + self.assert_rejected([command, '--agnt', 'codex'], 'Unknown option --agnt') + + def test_help_prints_usage_without_writing(self): + for args in (['hooks', 'setup', '--help'], ['hooks', 'uninstall', '-h']): + with self.subTest(args=args): + result = self.run_cli(*args) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('Usage: cmux hooks setup', result.stdout) + self.assertEqual(self.written_files(), []) + + def test_documented_forms_still_work(self): + for args in (['hooks', 'setup', 'pi', '--yes'], + ['hooks', 'setup', '--agent', 'pi', '-y'], + ['hooks', 'setup', '--agent=pi'], + ['hooks', 'setup', 'pi', '--agent', 'pi'], + ['hooks', 'setup', '--uninstall', 'pi']): + with self.subTest(args=args): + result = self.run_cli(*args) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('pi', result.stdout) + + + def test_agent_name_aliases_still_resolve(self): + for args, agent in ((['hooks', 'setup', 'agy', '--yes'], 'antigravity'), + (['hooks', 'setup', '--agent', 'rovo', '--yes'], 'rovodev'), + (['hooks', 'uninstall', '--agent=agy', '--yes'], 'antigravity')): + with self.subTest(args=args): + result = self.run_cli(*args) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn(f' {agent}:', result.stdout) + + +if __name__ == '__main__': + unittest.main()