@openturtle/cli 0.3.2 → 0.3.4

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.
@@ -1,158 +1,117 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { hookCommand } from '../commands/hook.js';
5
- import { readJsonFile, writeJsonFile } from '../core/json.js';
6
- import { appendCodexHookBlocks, claudeMcpPath, claudeSettingsPath, codexConfigPath, cursorMcpPath, kiroMcpPath, mergeCodexMcpServer, mergeJsonMcpServer, qoderMcpPath, removeCodexManagedConfig, removeJsonMcpServer, } from '../core/mcp-config.js';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { readJsonFile } from '../core/json.js';
6
+ import { claudeLegacyServerPath, claudeSettingsPath, codexConfigPath, cursorLegacyServerPath, kiroLegacyServerPath, qoderLegacyServerPath, removeLegacyCodexConfig, removeLegacyJsonServer, } from '../core/legacy-agent-cleanup.js';
7
7
  import { ensureFileDir } from '../core/paths.js';
8
8
  import { removeCursorHookEntries, removeDirIfExists, removeFileIfExists, removeManagedHookEntries, writeJsonOrRemoveEmpty, } from './uninstall.js';
9
9
  export function installClaude(options) {
10
- mergeJsonMcpServer(claudeMcpPath(options.scope, options.workspace, options.homeDir));
11
- writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Claude');
12
- if (options.withAgentHooks) {
13
- mergeJsonMcpServerInSettings(claudeSettingsPath(options.scope, options.workspace, options.homeDir));
14
- const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
15
- const settings = readJsonFile(settingsPath, {});
16
- const hooks = settings.hooks || {};
17
- settings.hooks = {
18
- ...hooks,
19
- UserPromptSubmit: mergeManagedHookArray(hooks.UserPromptSubmit, managedHook('claude', 'UserPromptSubmit')),
20
- PreToolUse: mergeManagedHookArray(hooks.PreToolUse, managedHook('claude', 'PreToolUse', '*')),
21
- PostToolUse: mergeManagedHookArray(hooks.PostToolUse, managedHook('claude', 'PostToolUse', '*')),
22
- Stop: mergeManagedHookArray(hooks.Stop, managedHook('claude', 'Stop')),
23
- };
24
- writeJsonFile(settingsPath, settings);
25
- }
10
+ cleanupClaudeLegacy(options);
11
+ writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md'));
26
12
  }
27
13
  export function installCodex(options) {
28
- const configPath = codexConfigPath(options.scope, options.workspace, options.homeDir);
29
- mergeCodexMcpServer(configPath);
30
- writeSkill(path.join(rootForScope(options, '.codex'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Codex');
31
- appendAgentsSnippet(options.workspace);
32
- if (options.withAgentHooks) {
33
- appendCodexHookBlocks(configPath, [
34
- codexHookBlock('PreToolUse', 'codex', 'PreToolUse'),
35
- codexHookBlock('PostToolUse', 'codex', 'PostToolUse'),
36
- codexHookBlock('Stop', 'codex', 'Stop'),
37
- ].join('\n\n'));
38
- }
14
+ cleanupCodexLegacy(options);
15
+ writeSkill(path.join(rootForScope(options, '.codex'), 'skills', 'openturtle-cli', 'SKILL.md'));
16
+ if (options.scope === 'project')
17
+ appendAgentsSnippet(options.workspace);
39
18
  }
40
19
  export function installCursor(options) {
41
- mergeJsonMcpServer(cursorMcpPath(options.workspace));
20
+ cleanupCursorLegacy(options);
42
21
  writeTextFile(path.join(options.workspace, '.cursor', 'rules', 'openturtle-cli.mdc'), [
43
22
  '---',
44
- 'description: Use OpenTurtle CLI MCP tools for goals, todos, reusable knowledge, source files, worklogs, and report drafts.',
23
+ 'description: Use the OpenTurtle CLI directly for projects, goals, todos, knowledge, meetings, and worklogs.',
45
24
  'alwaysApply: true',
46
25
  '---',
47
26
  '',
48
- 'Prefer the `ot-cli` MCP server when reading or updating OpenTurtle goals, todos, worklogs, reports, or reusable workspace knowledge.',
49
- 'Use knowledge source-file tools only when you need the original files behind a knowledge object.',
50
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
27
+ 'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
28
+ 'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
29
+ 'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
30
+ 'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
51
31
  '',
52
32
  ].join('\n'));
53
- if (options.withAgentHooks) {
54
- const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
55
- const hooks = readJsonFile(hooksPath, {});
56
- hooks.preToolUse = mergeManagedHookArray(hooks.preToolUse, cursorHook('cursor', 'pre_tool_use'));
57
- hooks.postToolUse = mergeManagedHookArray(hooks.postToolUse, cursorHook('cursor', 'post_tool_use'));
58
- writeJsonFile(hooksPath, hooks);
59
- }
60
33
  }
61
34
  export function installQoder(options) {
62
- mergeJsonMcpServer(qoderMcpPath(options.scope, options.workspace, options.homeDir));
63
- writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Qoder');
64
- if (options.withAgentHooks) {
65
- const settingsPath = options.scope === 'user'
66
- ? path.join(options.homeDir || os.homedir(), '.qoder', 'settings.json')
67
- : path.join(options.workspace, '.qoder', 'settings.local.json');
68
- const settings = readJsonFile(settingsPath, {});
69
- const hooks = settings.hooks || {};
70
- settings.hooks = {
71
- ...hooks,
72
- UserPromptSubmit: mergeManagedHookArray(hooks.UserPromptSubmit, managedHook('qoder', 'UserPromptSubmit')),
73
- PreToolUse: mergeManagedHookArray(hooks.PreToolUse, managedHook('qoder', 'PreToolUse', '*')),
74
- PostToolUse: mergeManagedHookArray(hooks.PostToolUse, managedHook('qoder', 'PostToolUse', '*')),
75
- PostToolUseFailure: mergeManagedHookArray(hooks.PostToolUseFailure, managedHook('qoder', 'PostToolUseFailure', '*')),
76
- Stop: mergeManagedHookArray(hooks.Stop, managedHook('qoder', 'Stop')),
77
- };
78
- writeJsonFile(settingsPath, settings);
79
- }
35
+ cleanupQoderLegacy(options);
36
+ writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md'));
80
37
  }
81
38
  export function installKiro(options) {
82
- mergeJsonMcpServer(kiroMcpPath(options.scope, options.workspace, options.homeDir));
39
+ cleanupKiroLegacy(options);
83
40
  writeTextFile(path.join(rootForScope(options, '.kiro'), 'steering', 'openturtle-cli.md'), [
84
41
  '# OpenTurtle CLI',
85
42
  '',
86
- 'Use the `ot-cli` MCP server for OpenTurtle goals, todos, reusable knowledge, source files, worklogs, and daily report drafts.',
87
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
88
- 'Hooks installed by OpenTurtle are audit-only and must not block the user workflow.',
43
+ 'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
44
+ 'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
45
+ 'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
46
+ 'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
89
47
  '',
90
48
  ].join('\n'));
91
- if (options.withAgentHooks) {
92
- const agentPath = path.join(rootForScope(options, '.kiro'), 'agents', 'openturtle-cli.json');
93
- const agent = readJsonFile(agentPath, {});
94
- const hooks = agent.hooks || {};
95
- agent.name = 'openturtle-cli';
96
- agent.description = 'OpenTurtle local audit and MCP integration';
97
- agent.hooks = {
98
- ...hooks,
99
- agentSpawn: managedKiroHookList('agentSpawn'),
100
- userPromptSubmit: managedKiroHookList('userPromptSubmit'),
101
- preToolUse: managedKiroHookList('preToolUse', '*'),
102
- postToolUse: managedKiroHookList('postToolUse', '*'),
103
- stop: managedKiroHookList('stop'),
104
- };
105
- writeJsonFile(agentPath, agent);
106
- writeKiroIdeHook(path.join(rootForScope(options, '.kiro'), 'hooks', 'openturtle-cli-audit.kiro.hook'));
107
- }
108
49
  }
109
50
  export function uninstallClaude(options) {
51
+ cleanupClaudeLegacy(options);
110
52
  const root = rootForScope(options, '.claude');
111
- removeJsonMcpServer(claudeMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', root);
112
- const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
113
- if (fs.existsSync(settingsPath)) {
114
- const settings = readJsonFile(settingsPath, {});
115
- if (settings.mcpServers && typeof settings.mcpServers === 'object') {
116
- delete settings.mcpServers['ot-cli'];
117
- }
118
- writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
119
- }
120
53
  removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
121
54
  }
122
55
  export function uninstallCodex(options) {
56
+ cleanupCodexLegacy(options);
123
57
  const root = rootForScope(options, '.codex');
124
- removeCodexManagedConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
125
58
  removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
126
- removeAgentsSnippet(options.workspace);
59
+ if (options.scope === 'project')
60
+ removeAgentsSnippet(options.workspace);
127
61
  }
128
62
  export function uninstallCursor(options) {
129
- removeJsonMcpServer(cursorMcpPath(options.workspace), 'ot-cli', options.workspace);
63
+ cleanupCursorLegacy(options);
130
64
  const root = path.join(options.workspace, '.cursor');
131
65
  removeFileIfExists(path.join(root, 'rules', 'openturtle-cli.mdc'), root);
132
- const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
133
- if (fs.existsSync(hooksPath)) {
134
- const hooks = readJsonFile(hooksPath, {});
135
- writeJsonOrRemoveEmpty(hooksPath, removeCursorHookEntries(hooks), path.dirname(hooksPath));
136
- }
137
66
  }
138
67
  export function uninstallQoder(options) {
68
+ cleanupQoderLegacy(options);
139
69
  const root = rootForScope(options, '.qoder');
140
- removeJsonMcpServer(qoderMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', options.scope === 'user'
141
- ? path.dirname(qoderMcpPath(options.scope, options.workspace, options.homeDir))
142
- : options.workspace);
143
70
  removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
71
+ }
72
+ export function uninstallKiro(options) {
73
+ cleanupKiroLegacy(options);
74
+ const root = rootForScope(options, '.kiro');
75
+ removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
76
+ }
77
+ function cleanupClaudeLegacy(options) {
78
+ const root = rootForScope(options, '.claude');
79
+ removeLegacyJsonServer(claudeLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
80
+ const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
81
+ if (!fs.existsSync(settingsPath))
82
+ return;
83
+ const settings = readJsonFile(settingsPath, {});
84
+ if (settings.mcpServers && typeof settings.mcpServers === 'object')
85
+ delete settings.mcpServers['ot-cli'];
86
+ writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
87
+ }
88
+ function cleanupCodexLegacy(options) {
89
+ const root = rootForScope(options, '.codex');
90
+ removeLegacyCodexConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
91
+ }
92
+ function cleanupCursorLegacy(options) {
93
+ removeLegacyJsonServer(cursorLegacyServerPath(options.workspace), options.workspace);
94
+ const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
95
+ if (!fs.existsSync(hooksPath))
96
+ return;
97
+ const hooks = readJsonFile(hooksPath, {});
98
+ writeJsonOrRemoveEmpty(hooksPath, removeCursorHookEntries(hooks), path.dirname(hooksPath));
99
+ }
100
+ function cleanupQoderLegacy(options) {
101
+ const root = rootForScope(options, '.qoder');
102
+ const mcpPath = qoderLegacyServerPath(options.scope, options.workspace, options.homeDir);
103
+ removeLegacyJsonServer(mcpPath, options.scope === 'user' ? path.dirname(mcpPath) : options.workspace);
144
104
  const settingsPath = options.scope === 'user'
145
105
  ? path.join(options.homeDir || os.homedir(), '.qoder', 'settings.json')
146
106
  : path.join(options.workspace, '.qoder', 'settings.local.json');
147
- if (fs.existsSync(settingsPath)) {
148
- const settings = readJsonFile(settingsPath, {});
149
- writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
150
- }
107
+ if (!fs.existsSync(settingsPath))
108
+ return;
109
+ const settings = readJsonFile(settingsPath, {});
110
+ writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
151
111
  }
152
- export function uninstallKiro(options) {
112
+ function cleanupKiroLegacy(options) {
153
113
  const root = rootForScope(options, '.kiro');
154
- removeJsonMcpServer(kiroMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', root);
155
- removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
114
+ removeLegacyJsonServer(kiroLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
156
115
  removeFileIfExists(path.join(root, 'agents', 'openturtle-cli.json'), root);
157
116
  removeFileIfExists(path.join(root, 'hooks', 'openturtle-cli-audit.kiro.hook'), root);
158
117
  }
@@ -161,108 +120,26 @@ function rootForScope(options, dirName) {
161
120
  ? path.join(options.homeDir || os.homedir(), dirName)
162
121
  : path.join(options.workspace, dirName);
163
122
  }
164
- function mergeJsonMcpServerInSettings(settingsPath) {
165
- const settings = readJsonFile(settingsPath, {});
166
- const servers = settings.mcpServers && typeof settings.mcpServers === 'object'
167
- ? settings.mcpServers
168
- : {};
169
- servers['ot-cli'] = { command: 'ot', args: ['mcp', 'serve'], enabled: true };
170
- writeJsonFile(settingsPath, { ...settings, mcpServers: servers });
123
+ function writeSkill(filePath) {
124
+ writeTextFile(filePath, skillContent());
171
125
  }
172
- function managedHook(target, event, matcher) {
173
- return withoutUndefined({
174
- id: 'OPENTURTLE-CLI',
175
- name: 'OPENTURTLE-CLI audit',
176
- matcher,
177
- command: hookCommand(target, event),
178
- timeout_ms: 2000,
179
- continue_on_error: true,
180
- });
181
- }
182
- function managedKiroHookList(event, matcher) {
183
- return [
184
- withoutUndefined({
185
- id: 'OPENTURTLE-CLI',
186
- name: 'OPENTURTLE-CLI audit',
187
- matcher,
188
- command: hookCommand('kiro', event),
189
- timeout_ms: 2000,
190
- cache_ttl_seconds: 0,
191
- continue_on_error: true,
192
- }),
193
- ];
194
- }
195
- function cursorHook(target, event) {
196
- return {
197
- id: 'OPENTURTLE-CLI',
198
- command: hookCommand(target, event),
199
- timeout_ms: 2000,
200
- continue_on_error: true,
201
- };
202
- }
203
- function mergeManagedHookArray(existing, entry) {
204
- const values = Array.isArray(existing) ? existing.filter((item) => item?.id !== 'OPENTURTLE-CLI') : [];
205
- return [...values, entry];
206
- }
207
- function codexHookBlock(hook, target, event) {
208
- return [
209
- `[[hooks.${hook}]]`,
210
- `command = "ot"`,
211
- `args = ["hook", "ingest", "--target", "${target}", "--event", "${event}", "--stdin-json"]`,
212
- `timeout_ms = 2000`,
213
- ].join('\n');
214
- }
215
- function writeSkill(filePath, agentName) {
216
- writeTextFile(filePath, [
217
- '---',
218
- 'name: openturtle-cli',
219
- 'description: Use OpenTurtle projects, team roster, project context, knowledge, meetings, goals, todos, and worklogs from an agent or terminal without requiring the Desktop client.',
220
- '---',
221
- '',
222
- '# OpenTurtle CLI',
223
- '',
224
- `Use the \`ot-cli\` MCP server in ${agentName} when its tool covers the task. Use the direct \`ot\` command for projects, roster, project context, meetings, or any newer operation that is not exposed by MCP.`,
225
- 'If authentication is missing, ask the user to run `ot auth login --web`; never request or expose their browser token.',
226
- 'Run `ot --json doctor` before a remote workflow when authentication or server reachability is uncertain.',
227
- 'Read commands emit JSON. Before a write, run the same command with `--dry-run` and inspect `request.path` and `request.body`.',
228
- 'Treat knowledge as reusable memory objects. Treat source files as original materials referenced by knowledge, not as a second knowledge system.',
229
- '',
230
- '## Meeting workflow',
231
- '',
232
- '1. Resolve the project with `ot projects resolve`, then inspect `ot roster list` and `ot context get`.',
233
- '2. Upload audio with `ot meetings upload` or inspect an existing meeting with `ot meetings show`, `transcript`, and `minutes`.',
234
- '3. Publish complete minutes with `ot meetings publish <meeting-id>`. Meeting minutes belong in Knowledge, not WorkLog.',
235
- '4. Review drafts using `ot meetings updates list <meeting-id>` and edit them when needed.',
236
- '5. Confirm only reviewed updates with `ot meetings updates confirm ... --knowledge <knowledge-id>`. Never confirm without the saved meeting knowledge id.',
237
- '6. Use the materialized Todo or WorkLog ids returned by confirmation as the project update evidence.',
238
- '',
239
- '## Todo quality',
240
- '',
241
- 'When creating or proposing OpenTurtle todos through any available OpenTurtle MCP tool, include source context, scope/environment, concrete action steps, measurable acceptance criteria, and required evidence.',
242
- 'Source context means the goal, meeting, plan artifact, commit, PR, file, worklog, or user request that caused the todo.',
243
- 'If the source context, environment, owner, due date, measurable acceptance criteria, or evidence is unclear, ask the user first. Use AskUserQuestion when the host supports it; otherwise ask one concise question before creating the todo.',
244
- 'Avoid vague acceptance such as "no obvious delay", "works normally", or "verify it". Prefer concrete thresholds, environments, and evidence.',
245
- 'Use `--body-file <json>` or `--body-file -` for structured writes so shell quoting does not corrupt payloads.',
246
- 'Do not write to `.openturtle/`; CLI state lives in `.openturtle-cli/` and `~/.openturtle/cli/`.',
247
- '',
248
- ].join('\n'));
126
+ export function skillContent() {
127
+ return fs.readFileSync(fileURLToPath(new URL('../../skill/openturtle-cli/SKILL.md', import.meta.url)), 'utf8');
249
128
  }
250
129
  function appendAgentsSnippet(workspace) {
251
130
  const filePath = path.join(workspace, 'AGENTS.md');
252
131
  const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
253
- const marker = '<!-- OPENTURTLE-CLI -->';
254
- if (existing.includes(marker))
255
- return;
132
+ const withoutOld = existing.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
256
133
  const snippet = [
257
- marker,
134
+ '<!-- OPENTURTLE-CLI -->',
258
135
  '',
259
- 'When OpenTurtle goals, todos, reusable knowledge, source files, worklogs, or report drafts are needed, prefer the `ot-cli` MCP server.',
260
- 'Knowledge means reusable memory objects; source files are original materials behind knowledge.',
261
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
136
+ 'Use the direct `ot ... --json` CLI for OpenTurtle projects, goals, todos, knowledge, meetings, and worklogs. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
137
+ 'Run `ot --json doctor` when authentication or server reachability is uncertain, and use `--dry-run` before writes.',
138
+ 'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
262
139
  '',
263
140
  '<!-- /OPENTURTLE-CLI -->',
264
141
  ].join('\n');
265
- writeTextFile(filePath, `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${snippet}\n`);
142
+ writeTextFile(filePath, `${withoutOld}${withoutOld ? '\n\n' : ''}${snippet}\n`);
266
143
  }
267
144
  function removeAgentsSnippet(workspace) {
268
145
  const filePath = path.join(workspace, 'AGENTS.md');
@@ -270,32 +147,12 @@ function removeAgentsSnippet(workspace) {
270
147
  return;
271
148
  const content = fs.readFileSync(filePath, 'utf-8');
272
149
  const cleaned = content.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
273
- if (cleaned) {
150
+ if (cleaned)
274
151
  fs.writeFileSync(filePath, `${cleaned}\n`, 'utf-8');
275
- }
276
- else {
152
+ else
277
153
  fs.rmSync(filePath, { force: true });
278
- }
279
- }
280
- function writeKiroIdeHook(filePath) {
281
- writeTextFile(filePath, [
282
- '{',
283
- ' "name": "OpenTurtle CLI audit",',
284
- ' "description": "Record Kiro hook events into .openturtle-cli/state.db",',
285
- ' "event": "postToolUse",',
286
- ' "action": {',
287
- ' "type": "command",',
288
- ` "command": "${hookCommand('kiro', 'postToolUse').replaceAll('"', '\\"')}",`,
289
- ' "timeout_ms": 2000',
290
- ' }',
291
- '}',
292
- '',
293
- ].join('\n'));
294
154
  }
295
155
  function writeTextFile(filePath, content) {
296
156
  ensureFileDir(filePath);
297
157
  fs.writeFileSync(filePath, content, 'utf-8');
298
158
  }
299
- function withoutUndefined(value) {
300
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
301
- }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { projectWorklogDir } from '../core/paths.js';
4
+ const MARKER = 'OPENTURTLE-CLI';
5
+ export function uninstallLegacyGitHook(workspace) {
6
+ const hookPath = path.join(workspace, '.git', 'hooks', 'post-commit');
7
+ const originalPath = path.join(projectWorklogDir(workspace), 'hooks', 'git-post-commit.original');
8
+ if (!fs.existsSync(hookPath))
9
+ return;
10
+ const current = fs.readFileSync(hookPath, 'utf-8');
11
+ if (!current.includes(MARKER))
12
+ return;
13
+ if (fs.existsSync(originalPath)) {
14
+ fs.copyFileSync(originalPath, hookPath);
15
+ fs.chmodSync(hookPath, 0o755);
16
+ }
17
+ else {
18
+ fs.unlinkSync(hookPath);
19
+ }
20
+ fs.rmSync(originalPath, { force: true });
21
+ try {
22
+ if (fs.existsSync(path.dirname(originalPath)) && fs.readdirSync(path.dirname(originalPath)).length === 0) {
23
+ fs.rmdirSync(path.dirname(originalPath));
24
+ }
25
+ }
26
+ catch {
27
+ // Legacy cleanup should not fail because an auxiliary directory is not empty.
28
+ }
29
+ }