Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions CLI/CLI+AgentWrappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ extension ProgramaCLI {
guard let data = FileManager.default.contents(atPath: path) else { return false }
let prefixData = data.prefix(512)
guard let prefix = String(data: prefixData, encoding: .utf8) else { return false }
// Legacy cmux name, still matched so wrappers installed by older versions are recognized.
return prefix.contains("programa claude wrapper - injects hooks and session tracking")
|| prefix.contains("cmux claude wrapper - injects hooks and session tracking")
}

func resolveExecutableInSearchPath(
Expand Down
3 changes: 1 addition & 2 deletions CLI/SocketClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ final class SocketClient {
private static let maxSocketTimeoutSeconds: TimeInterval = 9_007_199_254_740_991
private static let responseTimeoutSeconds: TimeInterval = {
let env = ProcessInfo.processInfo.environment
// Legacy cmux name (CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC), still read so existing scripts keep working.
if let raw = env["PROGRAMA_CLI_RESPONSE_TIMEOUT_SEC"] ?? env["CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC"],
if let raw = env["PROGRAMA_CLI_RESPONSE_TIMEOUT_SEC"],
let seconds = Double(raw),
seconds.isFinite,
seconds > 0 {
Expand Down
2 changes: 1 addition & 1 deletion CLI/programa.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ enum SocketPasswordResolver {
}

let candidate = URL(fileURLWithPath: socketPath).lastPathComponent
let prefixes = ["programa-debug-", "programa-", "cmux-debug-", "cmux-"] // Legacy cmux socket names, still read so older sockets keep resolving a scope.
let prefixes = ["programa-debug-", "programa-"]
for prefix in prefixes {
guard candidate.hasPrefix(prefix), candidate.hasSuffix(".sock") else { continue }
let start = candidate.index(candidate.startIndex, offsetBy: prefix.count)
Expand Down
4 changes: 2 additions & 2 deletions Resources/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
"properties": {
"socketControlMode": {
"type": "string",
"enum": ["off", "programaOnly", "cmuxOnly", "automation", "password", "allowAll", "openAccess", "fullOpenAccess", "notifications", "full"],
"enum": ["off", "programaOnly", "automation", "password", "allowAll", "openAccess", "fullOpenAccess", "notifications", "full"],
"default": "programaOnly",
"description": "Socket control mode. Legacy aliases are accepted and normalized."
},
Expand Down Expand Up @@ -312,7 +312,7 @@
"type": "string"
},
"default": [],
"description": "Directories whose programa.json (or legacy cmux.json) commands can run without confirmation."
"description": "Directories whose programa.json commands can run without confirmation."
}
}
},
Expand Down
4 changes: 1 addition & 3 deletions Resources/shell-integration/fish/config.fish
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ if test "$_programa_integration_enabled" != 0
printf '%s\n' "$PROGRAMA_BUNDLED_CLI_PATH"
return 0
end
# Rebranded CLI binary ships as "programa"; fall back to the pre-rebrand
# "cmux" name for older installs that only symlinked that binary.
command -v programa 2>/dev/null; or command -v cmux 2>/dev/null
command -v programa 2>/dev/null
end

function _programa_socket_uses_remote_relay
Expand Down
4 changes: 1 addition & 3 deletions Resources/shell-integration/programa-bash-integration.bash
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ _programa_relay_cli_path() {
printf '%s\n' "${PROGRAMA_BUNDLED_CLI_PATH}"
return 0
fi
# Rebranded CLI binary ships as "programa"; fall back to the pre-rebrand
# "cmux" name for older installs that only symlinked that binary.
command -v programa 2>/dev/null || command -v cmux 2>/dev/null
command -v programa 2>/dev/null
}

_programa_socket_uses_remote_relay() {
Expand Down
4 changes: 1 addition & 3 deletions Resources/shell-integration/programa-zsh-integration.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ _programa_relay_cli_path() {
print -r -- "${PROGRAMA_BUNDLED_CLI_PATH}"
return 0
fi
# Rebranded CLI binary ships as "programa"; fall back to the pre-rebrand
# "cmux" name for older installs that only symlinked that binary.
command -v programa 2>/dev/null || command -v cmux 2>/dev/null
command -v programa 2>/dev/null
}

_programa_socket_uses_remote_relay() {
Expand Down
4 changes: 1 addition & 3 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4923,9 +4923,7 @@ struct ContentView: View {
guard let data = UserDefaults.standard.data(forKey: Self.commandPaletteUsageDefaultsKey) else {
return [:]
}
let history = (try? JSONDecoder().decode([String: CommandPaletteUsageEntry].self, from: data)) ?? [:]
// Legacy cmux name, still read so existing custom-command usage history keeps working.
return Dictionary(history.map { ($0.key.replacingOccurrences(of: "cmux.config.", with: "programa.config.", options: .anchored), $0.value) }) { current, _ in current }
return (try? JSONDecoder().decode([String: CommandPaletteUsageEntry].self, from: data)) ?? [:]
}

private func persistCommandPaletteUsageHistory(_ history: [String: CommandPaletteUsageEntry]) {
Expand Down
8 changes: 2 additions & 6 deletions Sources/GitWorktreeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,8 @@ enum ProgramaWorktreeSettings {

private static func readSettingsRoot() -> [String: Any]? {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let newPath = (home as NSString).appendingPathComponent(".config/programa/settings.json")
// Legacy cmux name, still read so existing ~/.config/cmux/settings.json files keep working.
let legacyPath = (home as NSString).appendingPathComponent(".config/cmux/settings.json")
let fm = FileManager.default
let path = fm.fileExists(atPath: newPath) ? newPath : legacyPath
guard let data = fm.contents(atPath: path), !data.isEmpty else { return nil }
let path = (home as NSString).appendingPathComponent(".config/programa/settings.json")
guard let data = FileManager.default.contents(atPath: path), !data.isEmpty else { return nil }
guard let sanitized = try? JSONCParser.preprocess(data: data),
let object = try? JSONSerialization.jsonObject(with: sanitized, options: []) else {
return nil
Expand Down
2 changes: 1 addition & 1 deletion Sources/JSONCParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
/// so a comment marker or trailing comma inside a quoted string is left alone.
///
/// Shared by `ProgramaConfigStore` (`~/.config/programa/programa.json` and
/// project-local `programa.json`/legacy `cmux.json`) and `ProgramaSettingsFileStore`
/// project-local `programa.json`) and `ProgramaSettingsFileStore`
/// (`~/.config/programa/settings.json`) so both config surfaces accept the
/// same JSONC dialect.
enum JSONCParser {
Expand Down
19 changes: 0 additions & 19 deletions Sources/ProgramaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ struct programaApp: App {
)
let primaryTabManagerStore = PrimaryTabManagerStore()
_primaryTabManagerStore = StateObject(wrappedValue: primaryTabManagerStore)
// Rebrand: forward every legacy cmux-prefixed default to its programa key
// before anything reads the new keys, so existing users keep their prefs.
Self.migrateCmuxDefaultsToProgramaIfNeeded(defaults: defaults)
// Migrate legacy and old-format socket mode values to the new enum.
if let stored = defaults.string(forKey: SocketControlSettings.appStorageKey) {
let migrated = SocketControlSettings.migrateMode(stored)
Expand Down Expand Up @@ -240,22 +237,6 @@ struct programaApp: App {
setenv(key, updated, 1)
}

/// One-time rebrand migration: copy every `cmux`-prefixed UserDefaults value to
/// the corresponding `programa`-prefixed key. Version-gated so it runs once, and
/// never deletes the legacy keys (a downgrade still finds its old values).
private static func migrateCmuxDefaultsToProgramaIfNeeded(defaults: UserDefaults) {
let migrationKey = "programaDefaultsRebrandMigrationVersion"
let targetVersion = 1
guard defaults.integer(forKey: migrationKey) < targetVersion else { return }
for (key, value) in defaults.dictionaryRepresentation() where key.hasPrefix("cmux") {
let newKey = "programa" + key.dropFirst("cmux".count)
if defaults.object(forKey: newKey) == nil {
defaults.set(value, forKey: newKey)
}
}
defaults.set(targetVersion, forKey: migrationKey)
}

private static func migrateLanguageOverrideRemovalIfNeeded(defaults: UserDefaults) {
let migrationKey = "programaLanguageOverrideRemovalVersion"
let targetVersion = 1
Expand Down
17 changes: 4 additions & 13 deletions Sources/ProgramaConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,7 @@ final class ProgramaConfigStore: ObservableObject {
var localConfigPath: String?
var globalConfigPath: String = {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let newPath = (home as NSString).appendingPathComponent(".config/programa/programa.json")
// Legacy cmux name, still read so existing ~/.config/cmux/cmux.json files keep working.
let legacyPath = (home as NSString).appendingPathComponent(".config/cmux/cmux.json")
let fm = FileManager.default
if fm.fileExists(atPath: newPath) { return newPath }
if fm.fileExists(atPath: legacyPath) { return legacyPath }
return newPath
return (home as NSString).appendingPathComponent(".config/programa/programa.json")
}()

private var cancellables = Set<AnyCancellable>()
Expand Down Expand Up @@ -455,12 +449,9 @@ final class ProgramaConfigStore: ObservableObject {
var current = directory
let fs = FileManager.default
while true {
// Legacy cmux name, still read so existing project roots keep working.
for name in ["programa.json", "cmux.json"] {
let candidate = (current as NSString).appendingPathComponent(name)
if fs.fileExists(atPath: candidate) {
return candidate
}
let candidate = (current as NSString).appendingPathComponent("programa.json")
if fs.fileExists(atPath: candidate) {
return candidate
}
let parent = (current as NSString).deletingLastPathComponent
if parent == current { break }
Expand Down
10 changes: 2 additions & 8 deletions Sources/ProgramaDirectoryTrust.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ final class ProgramaDirectoryTrust: @unchecked Sendable {
/// means "nothing trusted yet".
///
/// A version-2 digest was recorded for whichever config was trusted under that root, which
/// in practice is the root's own `programa.json` (or legacy `cmux.json`), so it migrates to
/// in practice is the root's own `programa.json`, so it migrates to
/// that file's key. If the guess is wrong the affected config prompts once as `.changed`.
/// Entries with no digest migrate to an empty map and prompt once too.
private static func load(fromStorePath path: String) -> [String: ConfigDigests] {
Expand Down Expand Up @@ -283,13 +283,7 @@ final class ProgramaDirectoryTrust: @unchecked Sendable {
}

private static func migratedConfigPath(forRoot root: String) -> String {
let programaPath = (root as NSString).appendingPathComponent("programa.json")
let legacyPath = (root as NSString).appendingPathComponent("cmux.json")
let fm = FileManager.default
if !fm.fileExists(atPath: programaPath), fm.fileExists(atPath: legacyPath) {
return legacyPath
}
return programaPath
(root as NSString).appendingPathComponent("programa.json")
}

/// Caller holds `stateLock`; notification is deliberately posted after unlocking so a
Expand Down
12 changes: 3 additions & 9 deletions Sources/ProgramaSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,7 @@ final class ProgramaSettingsFileStore {

static var defaultPrimaryPath: String {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let newPath = (home as NSString).appendingPathComponent(".config/programa/settings.json")
// Legacy cmux name, still read so existing ~/.config/cmux/settings.json files keep working.
let legacyPath = (home as NSString).appendingPathComponent(".config/cmux/settings.json")
let fm = FileManager.default
if fm.fileExists(atPath: newPath) { return newPath }
if fm.fileExists(atPath: legacyPath) { return legacyPath }
return newPath
return (home as NSString).appendingPathComponent(".config/programa/settings.json")
}

static var defaultFallbackPath: String? {
Expand Down Expand Up @@ -721,7 +715,7 @@ final class ProgramaSettingsFileStore {
) {
if let raw = jsonString(section["socketControlMode"]) {
let knownModes = Set([
"off", "cmuxonly", "programaonly", "automation", "password", "allowall", "openaccess", "fullopenaccess",
"off", "programaonly", "automation", "password", "allowall", "openaccess", "fullopenaccess",
"notifications", "full",
])
let normalizedRaw = raw.replacingOccurrences(of: "-", with: "").lowercased()
Expand Down Expand Up @@ -1788,7 +1782,7 @@ private enum BackupValue: Codable, Equatable {
}

// JSONCParser (comments + trailing-comma stripping) now lives in Sources/JSONCParser.swift,
// shared with ProgramaConfigStore's programa.json (and legacy cmux.json) parsing.
// shared with ProgramaConfigStore's programa.json parsing.

// NOTE (drift, refs #100): unlike ProgramaConfigStore's local/global config watchers, this
// watcher has no delayed retry/backoff at all — on delete/rename it re-evaluates and
Expand Down
3 changes: 1 addition & 2 deletions Sources/SocketControlSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,7 @@ struct SocketControlSettings {
switch normalizeMode(raw) {
case "off":
return .off
// Legacy cmux name, still read so existing socket mode settings keep working.
case "cmuxonly", "programaonly":
case "programaonly":
return .programaOnly
case "automation":
return .automation
Expand Down
4 changes: 2 additions & 2 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Read by the app, the CLI and `programa-mcp`. Modes and discovery are described i
| `PROGRAMA_SOCKET_PATH` | CLI, MCP, app | Socket path to use. The app honors it for Debug and Staging builds, or when `PROGRAMA_ALLOW_SOCKET_OVERRIDE` is truthy. |
| `PROGRAMA_SOCKET` | CLI, MCP | Second choice for the socket path. `PROGRAMA_SOCKET_PATH` wins when both are set. The app never reads it. |
| `PROGRAMA_SOCKET_PASSWORD` | CLI, MCP, app | Socket password for `password` mode. The CLI checks `--password` first. The app checks it before the saved password file. |
| `PROGRAMA_SOCKET_MODE` | app | Socket mode for this launch: `off`, `programaOnly` (legacy alias `cmuxOnly`), `automation`, `password` or `allowAll`. Overrides the setting. |
| `PROGRAMA_SOCKET_MODE` | app | Socket mode for this launch: `off`, `programaOnly`, `automation`, `password` or `allowAll`. Overrides the setting. |
| `PROGRAMA_SOCKET_ENABLE` | app | `1`/`true`/`yes`/`on` or `0`/`false`/`no`/`off`. `0` turns the socket off regardless of the mode. |
| `PROGRAMA_ALLOW_SOCKET_OVERRIDE` | app | Truthy value lets `PROGRAMA_SOCKET_PATH` move the socket in a Release build or a tagged Debug build. |
| `PROGRAMA_TAG` | app, CLI | Name of a tagged build. The app uses it for the tag badge (at most 10 characters), for the tagged socket path, and to allow a Debug launch. The CLI adds `/tmp/programa-debug-<tag>.sock` to its socket search. An untagged Debug build refuses to launch. |
Expand All @@ -59,7 +59,7 @@ Read by the app, the CLI and `programa-mcp`. Modes and discovery are described i
| `PROGRAMA_PANE_ID` | Default pane target for the tmux-compatible commands when they are given none. |
| `PROGRAMA_COMMIT` | Commit hash the CLI and the About box show when the build has no embedded value. |
| `PROGRAMA_CLI_TTY_NAME`, `PROGRAMA_TTY_NAME` | Terminal device name that the agent hooks report for the surface. They fall back to `TTY` and `SSH_TTY`. |
| `PROGRAMA_CLI_RESPONSE_TIMEOUT_SEC` | CLI | Seconds the CLI waits for a socket response (default 15). The legacy name `CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC` is still read. |
| `PROGRAMA_CLI_RESPONSE_TIMEOUT_SEC` | CLI | Seconds the CLI waits for a socket response (default 15). |
| `PROGRAMA_CLAUDE_HOOK_STATE_PATH` | Overrides where the Claude, Codex and OpenCode hooks keep their session state file. |
| `PROGRAMA_CLAUDE_PID` | Process id of the Claude Code instance a hook belongs to. Set by the `claude` wrapper. |
| `PROGRAMA_CLAUDE_HOOK_PROGRAMA_BIN` | `programa` binary the `claude` wrapper's hooks call. |
Expand Down
3 changes: 1 addition & 2 deletions docs/plans/rust-core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,7 @@ Swift source of truth: `Sources/ProgramaSettingsFileStore.swift`,

**`programa.json`** (`docs/programa-json.md`, `Sources/ProgramaConfig.swift`,
`Sources/ProgramaConfigExecutor.swift`). Command-palette entries, walked up from the focused
workspace's cwd plus a global `~/.config/programa/programa.json` fallback (legacy names
`cmux.json`/`~/.config/cmux/cmux.json` still read). Two entry kinds: `commands` (either a
workspace's cwd plus a global `~/.config/programa/programa.json` fallback. Two entry kinds: `commands` (either a
`workspace` command — opens/recreates a named workspace with a saved layout — or a `command`
command — types+submits shell text) and `recipes` (fills a prompt template and types it into the
focused terminal **without** auto-submitting, so a cloned repo can't fire attacker-chosen text
Expand Down
5 changes: 2 additions & 3 deletions docs/programa-json.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# `programa.json`

Custom command-palette entries, shareable via git. Programa looks for `programa.json`
(falling back to the legacy `cmux.json`) by walking up from the focused workspace's current
directory, and always loads `~/.config/programa/programa.json` (or the legacy
`~/.config/cmux/cmux.json`) as a global fallback. Local entries take precedence over global ones
by walking up from the focused workspace's current
directory, and always loads `~/.config/programa/programa.json` as a global fallback. Local entries take precedence over global ones
with the same `name`.

The Swift structs in `Sources/ProgramaConfig.swift` (`ProgramaConfigFile`,
Expand Down
1 change: 1 addition & 0 deletions docs/removed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ Reductive pass of 2026-09-02, base commit 903027ccef. Core kept: the Ghostty ter
| Inline VS Code (serve-web) | [inline-vscode.md](inline-vscode.md) | 610 |
| Custom notification sound files | [custom-notification-sounds.md](custom-notification-sounds.md) | 400 |
| tmux active-pane overlay target | [tmux-active-pane-overlay.md](tmux-active-pane-overlay.md) | 70 |
| cmux upgrade shims (old config paths, defaults migration, env and alias fallbacks) | [cmux-upgrade-shims.md](cmux-upgrade-shims.md) | 100 |

Line counts are git-tracked lines at removal time and include tests and vendored code.
Loading
Loading