tmux-team 1.1.0 → 2.0.0-alpha.1

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,159 @@
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 { GlobalConfig, LocalConfig, ResolvedConfig, Paths } from './types.js';
9
+
10
+ const CONFIG_FILENAME = 'config.json';
11
+ const LOCAL_CONFIG_FILENAME = 'tmux-team.json';
12
+ const STATE_FILENAME = 'state.json';
13
+
14
+ // Default configuration values
15
+ const DEFAULT_CONFIG: Omit<GlobalConfig, 'agents'> & { agents: Record<string, never> } = {
16
+ mode: 'polling',
17
+ preambleMode: 'always',
18
+ defaults: {
19
+ timeout: 60,
20
+ pollInterval: 1,
21
+ captureLines: 100,
22
+ },
23
+ agents: {},
24
+ };
25
+
26
+ /**
27
+ * Resolve the global config directory path using XDG spec with smart detection.
28
+ *
29
+ * Priority:
30
+ * 1. TMUX_TEAM_HOME env (escape hatch)
31
+ * 2. XDG_CONFIG_HOME env → ${XDG_CONFIG_HOME}/tmux-team
32
+ * 3. ~/.config/tmux-team/ exists → use XDG style
33
+ * 4. ~/.tmux-team/ exists → use legacy
34
+ * 5. Else (new install) → default to XDG
35
+ */
36
+ export function resolveGlobalDir(): string {
37
+ const home = os.homedir();
38
+
39
+ // 1. Explicit override
40
+ if (process.env.TMUX_TEAM_HOME) {
41
+ return process.env.TMUX_TEAM_HOME;
42
+ }
43
+
44
+ // 2. XDG_CONFIG_HOME is set
45
+ if (process.env.XDG_CONFIG_HOME) {
46
+ return path.join(process.env.XDG_CONFIG_HOME, 'tmux-team');
47
+ }
48
+
49
+ const xdgPath = path.join(home, '.config', 'tmux-team');
50
+ const legacyPath = path.join(home, '.tmux-team');
51
+
52
+ // 3. XDG path exists
53
+ if (fs.existsSync(xdgPath)) {
54
+ // Edge case: both exist - prefer the one with config.json
55
+ if (fs.existsSync(legacyPath)) {
56
+ const xdgHasConfig = fs.existsSync(path.join(xdgPath, CONFIG_FILENAME));
57
+ const legacyHasConfig = fs.existsSync(path.join(legacyPath, CONFIG_FILENAME));
58
+
59
+ if (legacyHasConfig && !xdgHasConfig) {
60
+ return legacyPath;
61
+ }
62
+ // If both have config or only XDG has it, prefer XDG
63
+ }
64
+ return xdgPath;
65
+ }
66
+
67
+ // 4. Legacy path exists
68
+ if (fs.existsSync(legacyPath)) {
69
+ return legacyPath;
70
+ }
71
+
72
+ // 5. New install - default to XDG
73
+ return xdgPath;
74
+ }
75
+
76
+ export function resolvePaths(cwd: string = process.cwd()): Paths {
77
+ const globalDir = resolveGlobalDir();
78
+ return {
79
+ globalDir,
80
+ globalConfig: path.join(globalDir, CONFIG_FILENAME),
81
+ localConfig: path.join(cwd, LOCAL_CONFIG_FILENAME),
82
+ stateFile: path.join(globalDir, STATE_FILENAME),
83
+ };
84
+ }
85
+
86
+ export class ConfigParseError extends Error {
87
+ constructor(
88
+ public readonly filePath: string,
89
+ public readonly cause: Error
90
+ ) {
91
+ super(`Invalid JSON in ${filePath}: ${cause.message}`);
92
+ this.name = 'ConfigParseError';
93
+ }
94
+ }
95
+
96
+ function loadJsonFile<T>(filePath: string): T | null {
97
+ if (!fs.existsSync(filePath)) {
98
+ return null;
99
+ }
100
+ try {
101
+ const content = fs.readFileSync(filePath, 'utf-8');
102
+ return JSON.parse(content) as T;
103
+ } catch (err) {
104
+ // Throw on parse errors - don't silently ignore invalid config
105
+ throw new ConfigParseError(filePath, err as Error);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Load and merge configuration from all tiers.
111
+ *
112
+ * Precedence (lowest → highest):
113
+ * Defaults → Global config → Local config → CLI flags
114
+ *
115
+ * Note: CLI flags are applied by the caller after this function returns.
116
+ */
117
+ export function loadConfig(paths: Paths): ResolvedConfig {
118
+ // Start with defaults
119
+ const config: ResolvedConfig = {
120
+ ...DEFAULT_CONFIG,
121
+ defaults: { ...DEFAULT_CONFIG.defaults },
122
+ agents: {},
123
+ paneRegistry: {},
124
+ };
125
+
126
+ // Merge global config
127
+ const globalConfig = loadJsonFile<Partial<GlobalConfig>>(paths.globalConfig);
128
+ if (globalConfig) {
129
+ if (globalConfig.mode) config.mode = globalConfig.mode;
130
+ if (globalConfig.preambleMode) config.preambleMode = globalConfig.preambleMode;
131
+ if (globalConfig.defaults) {
132
+ config.defaults = { ...config.defaults, ...globalConfig.defaults };
133
+ }
134
+ if (globalConfig.agents) {
135
+ config.agents = { ...config.agents, ...globalConfig.agents };
136
+ }
137
+ }
138
+
139
+ // Load local config (pane registry)
140
+ const localConfig = loadJsonFile<LocalConfig>(paths.localConfig);
141
+ if (localConfig) {
142
+ config.paneRegistry = localConfig;
143
+ }
144
+
145
+ return config;
146
+ }
147
+
148
+ export function saveLocalConfig(
149
+ paths: Paths,
150
+ paneRegistry: Record<string, { pane: string; remark?: string }>
151
+ ): void {
152
+ fs.writeFileSync(paths.localConfig, JSON.stringify(paneRegistry, null, 2) + '\n');
153
+ }
154
+
155
+ export function ensureGlobalDir(paths: Paths): void {
156
+ if (!fs.existsSync(paths.globalDir)) {
157
+ fs.mkdirSync(paths.globalDir, { recursive: true });
158
+ }
159
+ }
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];
@@ -0,0 +1,158 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // PM Commands Tests
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { describe, it } from 'vitest';
6
+
7
+ describe('requireTeam', () => {
8
+ // Test finds team from .tmux-team-id file
9
+ it.todo('finds team ID from .tmux-team-id file in cwd');
10
+
11
+ // Test finds team from TMUX_TEAM_ID env
12
+ it.todo('finds team ID from TMUX_TEAM_ID environment variable');
13
+
14
+ // Test validates team.json exists
15
+ it.todo('validates team.json exists for team ID');
16
+
17
+ // Test error for missing team link
18
+ it.todo('exits with error when no .tmux-team-id found');
19
+
20
+ // Test error for stale team ID
21
+ it.todo('exits with error when team.json does not exist (stale ID)');
22
+ });
23
+
24
+ describe('cmdPmInit', () => {
25
+ // Test creates team with UUID
26
+ it.todo('creates team with generated UUID');
27
+
28
+ // Test creates team with custom name
29
+ it.todo('uses --name flag for team name');
30
+
31
+ // Test creates .tmux-team-id link file
32
+ it.todo('creates .tmux-team-id file in current directory');
33
+
34
+ // Test logs team_created event
35
+ it.todo('logs team_created event to audit log');
36
+
37
+ // Test JSON output
38
+ it.todo('outputs team info in JSON when --json flag set');
39
+ });
40
+
41
+ describe('cmdPmMilestone', () => {
42
+ // Test milestone add
43
+ it.todo('creates milestone with given name');
44
+
45
+ // Test milestone list
46
+ it.todo('lists all milestones in table format');
47
+
48
+ // Test milestone done
49
+ it.todo('marks milestone as done');
50
+
51
+ // Test milestone not found error
52
+ it.todo('exits with error for non-existent milestone');
53
+
54
+ // Test shorthand "m" routing
55
+ it.todo('routes "pm m add" to milestone add');
56
+ });
57
+
58
+ describe('cmdPmTask', () => {
59
+ // Test task add
60
+ it.todo('creates task with given title');
61
+
62
+ // Test task add with --milestone
63
+ it.todo('creates task with milestone reference');
64
+
65
+ // Test task add with --assignee
66
+ it.todo('creates task with assignee');
67
+
68
+ // Test task list
69
+ it.todo('lists all tasks in table format');
70
+
71
+ // Test task list with --status filter
72
+ it.todo('filters task list by status');
73
+
74
+ // Test task list with --milestone filter
75
+ it.todo('filters task list by milestone');
76
+
77
+ // Test task show
78
+ it.todo('displays task details');
79
+
80
+ // Test task update --status
81
+ it.todo('updates task status');
82
+
83
+ // Test task update --assignee
84
+ it.todo('updates task assignee');
85
+
86
+ // Test task done
87
+ it.todo('marks task as done');
88
+
89
+ // Test task not found error
90
+ it.todo('exits with error for non-existent task');
91
+
92
+ // Test shorthand "t" routing
93
+ it.todo('routes "pm t add" to task add');
94
+ });
95
+
96
+ describe('cmdPmDoc', () => {
97
+ // Test doc print mode
98
+ it.todo('prints task documentation with --print flag');
99
+
100
+ // Test doc edit mode (spawns editor)
101
+ it.todo('opens documentation in $EDITOR');
102
+
103
+ // Test doc for non-existent task
104
+ it.todo('exits with error for non-existent task');
105
+ });
106
+
107
+ describe('cmdPmLog', () => {
108
+ // Test log display
109
+ it.todo('displays audit events');
110
+
111
+ // Test log with --limit
112
+ it.todo('limits number of events displayed');
113
+
114
+ // Test log JSON output
115
+ it.todo('outputs events in JSON when --json flag set');
116
+
117
+ // Test empty log message
118
+ it.todo('shows info message when no events');
119
+ });
120
+
121
+ describe('cmdPmList', () => {
122
+ // Test lists all teams
123
+ it.todo('lists all teams in table format');
124
+
125
+ // Test no teams message
126
+ it.todo('shows info message when no teams');
127
+
128
+ // Test JSON output
129
+ it.todo('outputs teams in JSON when --json flag set');
130
+ });
131
+
132
+ describe('cmdPm router', () => {
133
+ // Test command routing
134
+ it.todo('routes to correct subcommand');
135
+
136
+ // Test shorthand expansion
137
+ it.todo('expands m to milestone, t to task');
138
+
139
+ // Test unknown command error
140
+ it.todo('exits with error for unknown subcommand');
141
+
142
+ // Test help command
143
+ it.todo('displays help for pm help');
144
+ });
145
+
146
+ describe('parseStatus', () => {
147
+ // Test valid statuses
148
+ it.todo('parses pending, in_progress, done');
149
+
150
+ // Test hyphen to underscore normalization
151
+ it.todo('normalizes in-progress to in_progress');
152
+
153
+ // Test case insensitivity
154
+ it.todo('handles case insensitive input');
155
+
156
+ // Test invalid status error
157
+ it.todo('throws error for invalid status');
158
+ });