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/README.md +195 -22
- package/bin/tmux-team +31 -430
- package/package.json +25 -5
- package/src/cli.ts +212 -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/help.ts +51 -0
- package/src/commands/init.ts +24 -0
- package/src/commands/list.ts +27 -0
- package/src/commands/remove.ts +25 -0
- package/src/commands/talk.test.ts +652 -0
- package/src/commands/talk.ts +261 -0
- package/src/commands/update.ts +47 -0
- package/src/config.test.ts +246 -0
- package/src/config.ts +159 -0
- package/src/context.ts +38 -0
- package/src/exits.ts +14 -0
- package/src/pm/commands.test.ts +158 -0
- package/src/pm/commands.ts +654 -0
- package/src/pm/manager.test.ts +377 -0
- package/src/pm/manager.ts +140 -0
- package/src/pm/storage/adapter.ts +55 -0
- package/src/pm/storage/fs.test.ts +384 -0
- package/src/pm/storage/fs.ts +255 -0
- package/src/pm/storage/github.ts +751 -0
- package/src/pm/types.ts +79 -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 +86 -0
- package/src/ui.ts +67 -0
- package/src/version.ts +21 -0
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
|
+
});
|