tmux-team 1.1.0 → 2.0.0-alpha.3

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/src/config.ts ADDED
@@ -0,0 +1,223 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Config loading with XDG support and 3-tier hierarchy
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import type {
9
+ GlobalConfig,
10
+ LocalConfig,
11
+ LocalConfigFile,
12
+ LocalSettings,
13
+ ResolvedConfig,
14
+ Paths,
15
+ } from './types.js';
16
+
17
+ const CONFIG_FILENAME = 'config.json';
18
+ const LOCAL_CONFIG_FILENAME = 'tmux-team.json';
19
+ const STATE_FILENAME = 'state.json';
20
+
21
+ // Default configuration values
22
+ const DEFAULT_CONFIG: Omit<GlobalConfig, 'agents'> & { agents: Record<string, never> } = {
23
+ mode: 'polling',
24
+ preambleMode: 'always',
25
+ defaults: {
26
+ timeout: 60,
27
+ pollInterval: 1,
28
+ captureLines: 100,
29
+ },
30
+ agents: {},
31
+ };
32
+
33
+ /**
34
+ * Resolve the global config directory path using XDG spec with smart detection.
35
+ *
36
+ * Priority:
37
+ * 1. TMUX_TEAM_HOME env (escape hatch)
38
+ * 2. XDG_CONFIG_HOME env → ${XDG_CONFIG_HOME}/tmux-team
39
+ * 3. ~/.config/tmux-team/ exists → use XDG style
40
+ * 4. ~/.tmux-team/ exists → use legacy
41
+ * 5. Else (new install) → default to XDG
42
+ */
43
+ export function resolveGlobalDir(): string {
44
+ const home = os.homedir();
45
+
46
+ // 1. Explicit override
47
+ if (process.env.TMUX_TEAM_HOME) {
48
+ return process.env.TMUX_TEAM_HOME;
49
+ }
50
+
51
+ // 2. XDG_CONFIG_HOME is set
52
+ if (process.env.XDG_CONFIG_HOME) {
53
+ return path.join(process.env.XDG_CONFIG_HOME, 'tmux-team');
54
+ }
55
+
56
+ const xdgPath = path.join(home, '.config', 'tmux-team');
57
+ const legacyPath = path.join(home, '.tmux-team');
58
+
59
+ // 3. XDG path exists
60
+ if (fs.existsSync(xdgPath)) {
61
+ // Edge case: both exist - prefer the one with config.json
62
+ if (fs.existsSync(legacyPath)) {
63
+ const xdgHasConfig = fs.existsSync(path.join(xdgPath, CONFIG_FILENAME));
64
+ const legacyHasConfig = fs.existsSync(path.join(legacyPath, CONFIG_FILENAME));
65
+
66
+ if (legacyHasConfig && !xdgHasConfig) {
67
+ return legacyPath;
68
+ }
69
+ // If both have config or only XDG has it, prefer XDG
70
+ }
71
+ return xdgPath;
72
+ }
73
+
74
+ // 4. Legacy path exists
75
+ if (fs.existsSync(legacyPath)) {
76
+ return legacyPath;
77
+ }
78
+
79
+ // 5. New install - default to XDG
80
+ return xdgPath;
81
+ }
82
+
83
+ export function resolvePaths(cwd: string = process.cwd()): Paths {
84
+ const globalDir = resolveGlobalDir();
85
+ return {
86
+ globalDir,
87
+ globalConfig: path.join(globalDir, CONFIG_FILENAME),
88
+ localConfig: path.join(cwd, LOCAL_CONFIG_FILENAME),
89
+ stateFile: path.join(globalDir, STATE_FILENAME),
90
+ };
91
+ }
92
+
93
+ export class ConfigParseError extends Error {
94
+ constructor(
95
+ public readonly filePath: string,
96
+ public readonly cause: Error
97
+ ) {
98
+ super(`Invalid JSON in ${filePath}: ${cause.message}`);
99
+ this.name = 'ConfigParseError';
100
+ }
101
+ }
102
+
103
+ function loadJsonFile<T>(filePath: string): T | null {
104
+ if (!fs.existsSync(filePath)) {
105
+ return null;
106
+ }
107
+ try {
108
+ const content = fs.readFileSync(filePath, 'utf-8');
109
+ return JSON.parse(content) as T;
110
+ } catch (err) {
111
+ // Throw on parse errors - don't silently ignore invalid config
112
+ throw new ConfigParseError(filePath, err as Error);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Load and merge configuration from all tiers.
118
+ *
119
+ * Precedence (lowest → highest):
120
+ * Defaults → Global config → Local config → CLI flags
121
+ *
122
+ * Note: CLI flags are applied by the caller after this function returns.
123
+ */
124
+ export function loadConfig(paths: Paths): ResolvedConfig {
125
+ // Start with defaults
126
+ const config: ResolvedConfig = {
127
+ ...DEFAULT_CONFIG,
128
+ defaults: { ...DEFAULT_CONFIG.defaults },
129
+ agents: {},
130
+ paneRegistry: {},
131
+ };
132
+
133
+ // Merge global config
134
+ const globalConfig = loadJsonFile<Partial<GlobalConfig>>(paths.globalConfig);
135
+ if (globalConfig) {
136
+ if (globalConfig.mode) config.mode = globalConfig.mode;
137
+ if (globalConfig.preambleMode) config.preambleMode = globalConfig.preambleMode;
138
+ if (globalConfig.defaults) {
139
+ config.defaults = { ...config.defaults, ...globalConfig.defaults };
140
+ }
141
+ if (globalConfig.agents) {
142
+ config.agents = { ...config.agents, ...globalConfig.agents };
143
+ }
144
+ }
145
+
146
+ // Load local config (pane registry + optional settings)
147
+ const localConfigFile = loadJsonFile<LocalConfigFile>(paths.localConfig);
148
+ if (localConfigFile) {
149
+ // Extract local settings if present
150
+ const { $config: localSettings, ...paneEntries } = localConfigFile;
151
+
152
+ // Merge local settings (override global)
153
+ if (localSettings) {
154
+ if (localSettings.mode) config.mode = localSettings.mode;
155
+ if (localSettings.preambleMode) config.preambleMode = localSettings.preambleMode;
156
+ }
157
+
158
+ // Set pane registry (filter out $config)
159
+ config.paneRegistry = paneEntries as LocalConfig;
160
+ }
161
+
162
+ return config;
163
+ }
164
+
165
+ export function saveLocalConfig(
166
+ paths: Paths,
167
+ paneRegistry: Record<string, { pane: string; remark?: string }>
168
+ ): void {
169
+ fs.writeFileSync(paths.localConfig, JSON.stringify(paneRegistry, null, 2) + '\n');
170
+ }
171
+
172
+ export function ensureGlobalDir(paths: Paths): void {
173
+ if (!fs.existsSync(paths.globalDir)) {
174
+ fs.mkdirSync(paths.globalDir, { recursive: true });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Load raw global config file (for editing).
180
+ */
181
+ export function loadGlobalConfig(paths: Paths): Partial<GlobalConfig> {
182
+ return loadJsonFile<Partial<GlobalConfig>>(paths.globalConfig) ?? {};
183
+ }
184
+
185
+ /**
186
+ * Save global config file.
187
+ */
188
+ export function saveGlobalConfig(paths: Paths, config: Partial<GlobalConfig>): void {
189
+ ensureGlobalDir(paths);
190
+ fs.writeFileSync(paths.globalConfig, JSON.stringify(config, null, 2) + '\n');
191
+ }
192
+
193
+ /**
194
+ * Load raw local config file (for editing).
195
+ */
196
+ export function loadLocalConfigFile(paths: Paths): LocalConfigFile {
197
+ return loadJsonFile<LocalConfigFile>(paths.localConfig) ?? {};
198
+ }
199
+
200
+ /**
201
+ * Save local config file (preserves both $config and pane entries).
202
+ */
203
+ export function saveLocalConfigFile(paths: Paths, configFile: LocalConfigFile): void {
204
+ fs.writeFileSync(paths.localConfig, JSON.stringify(configFile, null, 2) + '\n');
205
+ }
206
+
207
+ /**
208
+ * Update local settings (creates $config if needed).
209
+ */
210
+ export function updateLocalSettings(paths: Paths, settings: LocalSettings): void {
211
+ const configFile = loadLocalConfigFile(paths);
212
+ configFile.$config = { ...configFile.$config, ...settings };
213
+ saveLocalConfigFile(paths, configFile);
214
+ }
215
+
216
+ /**
217
+ * Clear local settings.
218
+ */
219
+ export function clearLocalSettings(paths: Paths): void {
220
+ const configFile = loadLocalConfigFile(paths);
221
+ delete configFile.$config;
222
+ saveLocalConfigFile(paths, configFile);
223
+ }
package/src/context.ts ADDED
@@ -0,0 +1,38 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Context object - passed to all commands
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context, Flags } from './types.js';
6
+ import { resolvePaths, loadConfig } from './config.js';
7
+ import { createUI } from './ui.js';
8
+ import { createTmux } from './tmux.js';
9
+ import { ExitCodes } from './exits.js';
10
+
11
+ export interface CreateContextOptions {
12
+ argv: string[];
13
+ flags: Flags;
14
+ cwd?: string;
15
+ }
16
+
17
+ export function createContext(options: CreateContextOptions): Context {
18
+ const { argv, flags, cwd = process.cwd() } = options;
19
+
20
+ const paths = resolvePaths(cwd);
21
+ const config = loadConfig(paths);
22
+ const ui = createUI(flags.json);
23
+ const tmux = createTmux();
24
+
25
+ return {
26
+ argv,
27
+ flags,
28
+ ui,
29
+ config,
30
+ tmux,
31
+ paths,
32
+ exit(code: number): never {
33
+ process.exit(code);
34
+ },
35
+ };
36
+ }
37
+
38
+ export { ExitCodes };
package/src/exits.ts ADDED
@@ -0,0 +1,14 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Exit code registry
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ export const ExitCodes = {
6
+ SUCCESS: 0,
7
+ ERROR: 1,
8
+ CONFIG_MISSING: 2,
9
+ PANE_NOT_FOUND: 3,
10
+ TIMEOUT: 4,
11
+ CONFLICT: 5,
12
+ } as const;
13
+
14
+ export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes];