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/README.md +195 -22
- package/bin/tmux-team +31 -430
- package/package.json +28 -6
- package/src/cli.ts +222 -0
- package/src/commands/add.ts +38 -0
- package/src/commands/check.ts +34 -0
- package/src/commands/completion.ts +118 -0
- package/src/commands/config.ts +187 -0
- package/src/commands/help.ts +66 -0
- package/src/commands/init.ts +24 -0
- package/src/commands/list.ts +27 -0
- package/src/commands/preamble.ts +153 -0
- package/src/commands/remove.ts +25 -0
- package/src/commands/talk.test.ts +679 -0
- package/src/commands/talk.ts +274 -0
- package/src/commands/update.ts +47 -0
- package/src/config.test.ts +246 -0
- package/src/config.ts +223 -0
- package/src/context.ts +38 -0
- package/src/exits.ts +14 -0
- package/src/pm/commands.test.ts +1127 -0
- package/src/pm/commands.ts +723 -0
- package/src/pm/manager.test.ts +377 -0
- package/src/pm/manager.ts +146 -0
- package/src/pm/permissions.test.ts +332 -0
- package/src/pm/permissions.ts +278 -0
- package/src/pm/storage/adapter.ts +55 -0
- package/src/pm/storage/fs.test.ts +384 -0
- package/src/pm/storage/fs.ts +256 -0
- package/src/pm/storage/github.ts +763 -0
- package/src/pm/types.ts +85 -0
- package/src/state.test.ts +311 -0
- package/src/state.ts +83 -0
- package/src/tmux.test.ts +205 -0
- package/src/tmux.ts +27 -0
- package/src/types.ts +97 -0
- package/src/ui.ts +76 -0
- package/src/version.ts +21 -0
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];
|