@nemus-cli/nemus 0.2.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.
Files changed (320) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/assets/README.md +53 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/banner.svg +66 -0
  7. package/assets/favicon-32.png +0 -0
  8. package/assets/favicon-64.png +0 -0
  9. package/assets/icon-256.png +0 -0
  10. package/assets/icon.svg +23 -0
  11. package/assets/logo-256.png +0 -0
  12. package/assets/logo-512.png +0 -0
  13. package/assets/logo-wordmark.svg +35 -0
  14. package/assets/logo.svg +32 -0
  15. package/bin/workspace.js +130 -0
  16. package/dist/cli/ai-prompt.js +421 -0
  17. package/dist/cli/dashboard/AgentList.js +61 -0
  18. package/dist/cli/dashboard/DashboardSidebar.js +280 -0
  19. package/dist/cli/dashboard/HelpFooter.js +73 -0
  20. package/dist/cli/dashboard/StatusBadge.js +50 -0
  21. package/dist/cli/dashboard/index.js +11 -0
  22. package/dist/cli/dashboard/types.js +2 -0
  23. package/dist/cli/tui.js +48 -0
  24. package/dist/commands/ai-prompt.js +22 -0
  25. package/dist/commands/analyze-deps.js +138 -0
  26. package/dist/commands/archive.js +113 -0
  27. package/dist/commands/branch/create.js +119 -0
  28. package/dist/commands/branch/index.js +104 -0
  29. package/dist/commands/branch/merge.js +90 -0
  30. package/dist/commands/branch/rebase.js +84 -0
  31. package/dist/commands/branch/switch.js +165 -0
  32. package/dist/commands/cache/index.js +80 -0
  33. package/dist/commands/cache/manager.js +198 -0
  34. package/dist/commands/cleanup.js +153 -0
  35. package/dist/commands/configure-claude.js +55 -0
  36. package/dist/commands/configure.js +231 -0
  37. package/dist/commands/create.js +204 -0
  38. package/dist/commands/dashboard/hook-handler.js +162 -0
  39. package/dist/commands/dashboard/index.js +258 -0
  40. package/dist/commands/dashboard/launcher.js +40 -0
  41. package/dist/commands/dashboard/session-picker.js +106 -0
  42. package/dist/commands/dashboard/sidebar.js +39 -0
  43. package/dist/commands/dashboard/workspace-picker.js +95 -0
  44. package/dist/commands/delete.js +194 -0
  45. package/dist/commands/deprecated-aliases.js +44 -0
  46. package/dist/commands/diff.js +129 -0
  47. package/dist/commands/doctor.js +141 -0
  48. package/dist/commands/generate-docs.js +77 -0
  49. package/dist/commands/ghq-status.js +72 -0
  50. package/dist/commands/go.js +154 -0
  51. package/dist/commands/history.js +124 -0
  52. package/dist/commands/list.js +191 -0
  53. package/dist/commands/mcp/index.js +69 -0
  54. package/dist/commands/migrate.js +296 -0
  55. package/dist/commands/remove-repo.js +217 -0
  56. package/dist/commands/report-bug.js +112 -0
  57. package/dist/commands/run.js +147 -0
  58. package/dist/commands/save-context.js +171 -0
  59. package/dist/commands/sessions.js +129 -0
  60. package/dist/commands/status.js +138 -0
  61. package/dist/commands/suite/create.js +212 -0
  62. package/dist/commands/suite/delete.js +64 -0
  63. package/dist/commands/suite/export.js +126 -0
  64. package/dist/commands/suite/import.js +128 -0
  65. package/dist/commands/suite/index.js +94 -0
  66. package/dist/commands/suite/list.js +49 -0
  67. package/dist/commands/suite/use.js +227 -0
  68. package/dist/commands/sync.js +117 -0
  69. package/dist/commands/update.js +203 -0
  70. package/dist/mcp/install.js +550 -0
  71. package/dist/mcp/server.js +398 -0
  72. package/dist/mcp/tools.js +922 -0
  73. package/dist/pi-extensions/permission-sync.ts.template +41 -0
  74. package/dist/pi-extensions/permissions-gate.ts.template +69 -0
  75. package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
  76. package/dist/pi-extensions/review-reminder.ts.template +58 -0
  77. package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
  78. package/dist/program.js +136 -0
  79. package/dist/scripts/workspace-table.py +394 -0
  80. package/dist/types/dashboard.js +11 -0
  81. package/dist/types/index.js +2 -0
  82. package/dist/utils/agent-config.js +263 -0
  83. package/dist/utils/agent-state.js +178 -0
  84. package/dist/utils/banner.js +24 -0
  85. package/dist/utils/branch-operations.js +232 -0
  86. package/dist/utils/bug-report.js +355 -0
  87. package/dist/utils/cache.js +168 -0
  88. package/dist/utils/claude-integration.js +617 -0
  89. package/dist/utils/claude-sessions.js +260 -0
  90. package/dist/utils/cleanup-operations.js +120 -0
  91. package/dist/utils/colors.js +26 -0
  92. package/dist/utils/command-helpers.js +44 -0
  93. package/dist/utils/config.js +150 -0
  94. package/dist/utils/context-file.js +33 -0
  95. package/dist/utils/dashboard-hooks.js +172 -0
  96. package/dist/utils/dependency-analyzer.js +234 -0
  97. package/dist/utils/diff-operations.js +80 -0
  98. package/dist/utils/doc-generator.js +184 -0
  99. package/dist/utils/ghq-integration.js +332 -0
  100. package/dist/utils/git-operations.js +237 -0
  101. package/dist/utils/git-status.js +187 -0
  102. package/dist/utils/github.js +68 -0
  103. package/dist/utils/health-checks.js +345 -0
  104. package/dist/utils/history.js +150 -0
  105. package/dist/utils/hooks.js +100 -0
  106. package/dist/utils/logger.js +40 -0
  107. package/dist/utils/permission-sync.js +681 -0
  108. package/dist/utils/pi-extensions.js +224 -0
  109. package/dist/utils/progress.js +92 -0
  110. package/dist/utils/prompts.js +279 -0
  111. package/dist/utils/repo-resolver.js +260 -0
  112. package/dist/utils/retry.js +65 -0
  113. package/dist/utils/run-operations.js +30 -0
  114. package/dist/utils/suite.js +159 -0
  115. package/dist/utils/sync-operations.js +107 -0
  116. package/dist/utils/tmux-dashboard.js +184 -0
  117. package/dist/utils/validation.js +153 -0
  118. package/dist/utils/version-check.js +121 -0
  119. package/dist/utils/workspace-meta.js +185 -0
  120. package/install-shell-integration.sh +532 -0
  121. package/install.sh +401 -0
  122. package/package.json +90 -0
  123. package/remove-shell-block.awk +44 -0
  124. package/scripts/postinstall.js +85 -0
  125. package/skills/analyze-deps.md +23 -0
  126. package/skills/archive-workspace.md +26 -0
  127. package/skills/branch-create.md +18 -0
  128. package/skills/create-workspace.md +39 -0
  129. package/skills/delete-workspace.md +37 -0
  130. package/skills/list-org-repos.md +26 -0
  131. package/skills/list-suites.md +24 -0
  132. package/skills/list-workspaces.md +27 -0
  133. package/skills/refresh-workspace-docs.md +89 -0
  134. package/skills/remove-repo.md +25 -0
  135. package/skills/run-command.md +23 -0
  136. package/skills/save-context.md +68 -0
  137. package/skills/search-repos.md +31 -0
  138. package/skills/snapshot-list.md +18 -0
  139. package/skills/snapshot-save.md +12 -0
  140. package/skills/switch-branch.md +18 -0
  141. package/skills/update-cache.md +21 -0
  142. package/skills/update-workspace.md +66 -0
  143. package/skills/workspace-cleanup.md +18 -0
  144. package/skills/workspace-diff.md +23 -0
  145. package/skills/workspace-doctor.md +23 -0
  146. package/skills/workspace-info.md +45 -0
  147. package/skills/workspace-manager/SKILL.md +201 -0
  148. package/skills/workspace-manager/references/ai-prompt.md +44 -0
  149. package/skills/workspace-manager/references/analyze-deps.md +45 -0
  150. package/skills/workspace-manager/references/archive-workspace.md +38 -0
  151. package/skills/workspace-manager/references/branch-create.md +31 -0
  152. package/skills/workspace-manager/references/branch-merge.md +22 -0
  153. package/skills/workspace-manager/references/branch-rebase.md +22 -0
  154. package/skills/workspace-manager/references/cache-clear.md +18 -0
  155. package/skills/workspace-manager/references/cache-info.md +22 -0
  156. package/skills/workspace-manager/references/configure-claude.md +31 -0
  157. package/skills/workspace-manager/references/configure.md +35 -0
  158. package/skills/workspace-manager/references/create-workspace.md +45 -0
  159. package/skills/workspace-manager/references/dashboard.md +61 -0
  160. package/skills/workspace-manager/references/delete-workspace.md +33 -0
  161. package/skills/workspace-manager/references/generate-docs.md +36 -0
  162. package/skills/workspace-manager/references/ghq-status.md +28 -0
  163. package/skills/workspace-manager/references/go.md +33 -0
  164. package/skills/workspace-manager/references/history.md +41 -0
  165. package/skills/workspace-manager/references/list-org-repos.md +26 -0
  166. package/skills/workspace-manager/references/list-suites.md +21 -0
  167. package/skills/workspace-manager/references/list-workspaces.md +27 -0
  168. package/skills/workspace-manager/references/mcp.md +49 -0
  169. package/skills/workspace-manager/references/remove-repo.md +26 -0
  170. package/skills/workspace-manager/references/run-command.md +44 -0
  171. package/skills/workspace-manager/references/search-repos.md +24 -0
  172. package/skills/workspace-manager/references/sessions.md +27 -0
  173. package/skills/workspace-manager/references/suite-create.md +22 -0
  174. package/skills/workspace-manager/references/suite-delete.md +21 -0
  175. package/skills/workspace-manager/references/suite-export.md +19 -0
  176. package/skills/workspace-manager/references/suite-import.md +19 -0
  177. package/skills/workspace-manager/references/suite-use.md +27 -0
  178. package/skills/workspace-manager/references/switch-branch.md +25 -0
  179. package/skills/workspace-manager/references/update-cache.md +22 -0
  180. package/skills/workspace-manager/references/update-workspace.md +50 -0
  181. package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
  182. package/skills/workspace-manager/references/workspace-diff.md +27 -0
  183. package/skills/workspace-manager/references/workspace-doctor.md +27 -0
  184. package/skills/workspace-manager/references/workspace-info.md +33 -0
  185. package/skills/workspace-manager/references/workspace-status.md +43 -0
  186. package/skills/workspace-manager/references/workspace-sync.md +26 -0
  187. package/skills/workspace-status.md +26 -0
  188. package/skills/workspace-sync.md +23 -0
  189. package/src/cli/ai-prompt.test.ts +667 -0
  190. package/src/cli/ai-prompt.ts +421 -0
  191. package/src/cli/dashboard/AgentList.tsx +44 -0
  192. package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
  193. package/src/cli/dashboard/HelpFooter.tsx +22 -0
  194. package/src/cli/dashboard/StatusBadge.tsx +23 -0
  195. package/src/cli/dashboard/index.ts +5 -0
  196. package/src/cli/dashboard/types.ts +16 -0
  197. package/src/cli/tui.tsx +51 -0
  198. package/src/cli/workspace-cli.test.ts +30 -0
  199. package/src/commands/ai-prompt.ts +21 -0
  200. package/src/commands/analyze-deps.ts +119 -0
  201. package/src/commands/archive.ts +114 -0
  202. package/src/commands/branch/create.ts +94 -0
  203. package/src/commands/branch/index.ts +74 -0
  204. package/src/commands/branch/merge.ts +61 -0
  205. package/src/commands/branch/rebase.ts +54 -0
  206. package/src/commands/branch/switch.ts +150 -0
  207. package/src/commands/cache/index.ts +51 -0
  208. package/src/commands/cache/manager.ts +168 -0
  209. package/src/commands/cleanup.ts +132 -0
  210. package/src/commands/configure-claude.ts +55 -0
  211. package/src/commands/configure.ts +204 -0
  212. package/src/commands/create.ts +196 -0
  213. package/src/commands/dashboard/hook-handler.ts +138 -0
  214. package/src/commands/dashboard/index.ts +248 -0
  215. package/src/commands/dashboard/launcher.ts +42 -0
  216. package/src/commands/dashboard/session-picker.ts +73 -0
  217. package/src/commands/dashboard/sidebar.js +39 -0
  218. package/src/commands/dashboard/workspace-picker.ts +62 -0
  219. package/src/commands/delete.test.ts +250 -0
  220. package/src/commands/delete.ts +171 -0
  221. package/src/commands/deprecated-aliases.ts +55 -0
  222. package/src/commands/diff.ts +106 -0
  223. package/src/commands/doctor.ts +110 -0
  224. package/src/commands/generate-docs.ts +45 -0
  225. package/src/commands/ghq-status.ts +70 -0
  226. package/src/commands/go.ts +125 -0
  227. package/src/commands/history.ts +136 -0
  228. package/src/commands/list.test.ts +219 -0
  229. package/src/commands/list.ts +176 -0
  230. package/src/commands/mcp/index.ts +39 -0
  231. package/src/commands/migrate.ts +277 -0
  232. package/src/commands/remove-repo.ts +194 -0
  233. package/src/commands/report-bug.ts +88 -0
  234. package/src/commands/run.ts +125 -0
  235. package/src/commands/save-context.test.ts +113 -0
  236. package/src/commands/save-context.ts +141 -0
  237. package/src/commands/sessions.ts +99 -0
  238. package/src/commands/status.ts +111 -0
  239. package/src/commands/suite/create.ts +240 -0
  240. package/src/commands/suite/delete.ts +65 -0
  241. package/src/commands/suite/export.ts +97 -0
  242. package/src/commands/suite/import.ts +100 -0
  243. package/src/commands/suite/index.ts +66 -0
  244. package/src/commands/suite/list.ts +54 -0
  245. package/src/commands/suite/use.ts +232 -0
  246. package/src/commands/sync.ts +97 -0
  247. package/src/commands/update.ts +197 -0
  248. package/src/mcp/install.ts +529 -0
  249. package/src/mcp/server.ts +579 -0
  250. package/src/mcp/tools.test.ts +545 -0
  251. package/src/mcp/tools.ts +1019 -0
  252. package/src/mcp/update-workspace.test.ts +220 -0
  253. package/src/pi-extensions/permission-sync.ts.template +41 -0
  254. package/src/pi-extensions/permissions-gate.ts.template +69 -0
  255. package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
  256. package/src/pi-extensions/review-reminder.ts.template +58 -0
  257. package/src/pi-extensions/workspace-input-status.ts.template +919 -0
  258. package/src/program.ts +112 -0
  259. package/src/scripts/workspace-table.py +394 -0
  260. package/src/types/dashboard.ts +29 -0
  261. package/src/types/index.ts +109 -0
  262. package/src/types/ink.d.ts +171 -0
  263. package/src/utils/agent-config.test.ts +303 -0
  264. package/src/utils/agent-config.ts +264 -0
  265. package/src/utils/agent-state.test.ts +166 -0
  266. package/src/utils/agent-state.ts +135 -0
  267. package/src/utils/banner.ts +24 -0
  268. package/src/utils/branch-operations.ts +237 -0
  269. package/src/utils/bug-report.test.ts +174 -0
  270. package/src/utils/bug-report.ts +355 -0
  271. package/src/utils/cache.ts +150 -0
  272. package/src/utils/claude-integration.test.ts +381 -0
  273. package/src/utils/claude-integration.ts +649 -0
  274. package/src/utils/claude-sessions.test.ts +262 -0
  275. package/src/utils/claude-sessions.ts +251 -0
  276. package/src/utils/cleanup-operations.ts +94 -0
  277. package/src/utils/colors.ts +25 -0
  278. package/src/utils/command-helpers.ts +46 -0
  279. package/src/utils/config.test.ts +203 -0
  280. package/src/utils/config.ts +133 -0
  281. package/src/utils/context-file.ts +36 -0
  282. package/src/utils/dashboard-hooks.test.ts +168 -0
  283. package/src/utils/dashboard-hooks.ts +163 -0
  284. package/src/utils/dependency-analyzer.ts +243 -0
  285. package/src/utils/diff-operations.ts +59 -0
  286. package/src/utils/doc-generator.ts +187 -0
  287. package/src/utils/ghq-integration.test.ts +195 -0
  288. package/src/utils/ghq-integration.ts +317 -0
  289. package/src/utils/git-operations.test.ts +265 -0
  290. package/src/utils/git-operations.ts +233 -0
  291. package/src/utils/git-status.ts +171 -0
  292. package/src/utils/github.ts +74 -0
  293. package/src/utils/health-checks.ts +359 -0
  294. package/src/utils/history.ts +140 -0
  295. package/src/utils/hooks.ts +92 -0
  296. package/src/utils/logger.ts +37 -0
  297. package/src/utils/permission-sync.test.ts +593 -0
  298. package/src/utils/permission-sync.ts +720 -0
  299. package/src/utils/pi-extensions.test.ts +182 -0
  300. package/src/utils/pi-extensions.ts +226 -0
  301. package/src/utils/progress.ts +73 -0
  302. package/src/utils/prompts.test.ts +169 -0
  303. package/src/utils/prompts.ts +296 -0
  304. package/src/utils/repo-resolver.test.ts +313 -0
  305. package/src/utils/repo-resolver.ts +294 -0
  306. package/src/utils/retry.test.ts +124 -0
  307. package/src/utils/retry.ts +85 -0
  308. package/src/utils/run-operations.ts +38 -0
  309. package/src/utils/suite.test.ts +256 -0
  310. package/src/utils/suite.ts +144 -0
  311. package/src/utils/sync-operations.ts +87 -0
  312. package/src/utils/tmux-dashboard.ts +193 -0
  313. package/src/utils/validation.test.ts +139 -0
  314. package/src/utils/validation.ts +128 -0
  315. package/src/utils/version-check.test.ts +108 -0
  316. package/src/utils/version-check.ts +96 -0
  317. package/src/utils/workspace-meta.ts +173 -0
  318. package/sync-permissions.sh +137 -0
  319. package/tsconfig.json +20 -0
  320. package/uninstall.sh +162 -0
@@ -0,0 +1,681 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getPermissionSyncHookCommand = getPermissionSyncHookCommand;
37
+ exports.readClaudeSettings = readClaudeSettings;
38
+ exports.installPermissionSyncHook = installPermissionSyncHook;
39
+ exports.uninstallPermissionSyncHook = uninstallPermissionSyncHook;
40
+ exports.isReusablePermission = isReusablePermission;
41
+ exports.mergePermissions = mergePermissions;
42
+ exports.syncAllWorkspacePermissions = syncAllWorkspacePermissions;
43
+ exports.installWorkspaceSkills = installWorkspaceSkills;
44
+ exports.uninstallWorkspaceSkills = uninstallWorkspaceSkills;
45
+ exports.installWorkspaceStatusLine = installWorkspaceStatusLine;
46
+ exports.uninstallWorkspaceStatusLine = uninstallWorkspaceStatusLine;
47
+ exports.repairStaleHooks = repairStaleHooks;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const os = __importStar(require("os"));
51
+ const logger_1 = require("./logger");
52
+ const agent_config_1 = require("./agent-config");
53
+ function getDefaultClaudeSettingsPath() {
54
+ return path.join(os.homedir(), '.claude', 'settings.json');
55
+ }
56
+ /**
57
+ * Returns the shell command used for the Stop hook entry.
58
+ * Resolves to the sync-permissions.sh script bundled with this package.
59
+ */
60
+ function getPermissionSyncHookCommand() {
61
+ // __dirname is dist/utils/ or src/utils/ — either way, two levels up is project root
62
+ const scriptPath = path.resolve(__dirname, '..', '..', 'sync-permissions.sh');
63
+ if (fs.existsSync(scriptPath)) {
64
+ return `bash "${scriptPath}"`;
65
+ }
66
+ throw new Error('Could not find sync-permissions.sh. Make sure the package is installed correctly.');
67
+ }
68
+ function readClaudeSettings(settingsPath) {
69
+ if (!fs.existsSync(settingsPath)) {
70
+ return {};
71
+ }
72
+ try {
73
+ const content = fs.readFileSync(settingsPath, 'utf-8');
74
+ return JSON.parse(content);
75
+ }
76
+ catch {
77
+ return {};
78
+ }
79
+ }
80
+ function writeClaudeSettings(settingsPath, settings) {
81
+ const dir = path.dirname(settingsPath);
82
+ if (!fs.existsSync(dir)) {
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ }
85
+ const tmp = settingsPath + '.tmp';
86
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
87
+ fs.renameSync(tmp, settingsPath);
88
+ }
89
+ function matcherEntryContainsCommand(entry, needle) {
90
+ return entry.hooks?.some((h) => h.command.includes(needle)) ?? false;
91
+ }
92
+ function getCommandFromMatcherEntry(entry) {
93
+ return entry.hooks?.[0]?.command;
94
+ }
95
+ /**
96
+ * Install the permission-sync Stop hook into ~/.claude/settings.json.
97
+ * Idempotent — if a sync-permissions hook already exists with the same command,
98
+ * it's left as-is. If it exists with a different path (stale from a previous
99
+ * install location), the command is updated in place.
100
+ *
101
+ * Uses the Claude Code hooks format:
102
+ * { hooks: [{ type: "command", command: "..." }] }
103
+ *
104
+ * @param settingsPath - Override the settings file path (for testing).
105
+ */
106
+ function installPermissionSyncHook(settingsPath) {
107
+ const filePath = settingsPath ?? getDefaultClaudeSettingsPath();
108
+ let command;
109
+ try {
110
+ command = getPermissionSyncHookCommand();
111
+ }
112
+ catch (err) {
113
+ (0, logger_1.logWarning)(`Could not locate sync-permissions.sh — skipping hook installation. ${err instanceof Error ? err.message : ''}`);
114
+ return;
115
+ }
116
+ const settings = readClaudeSettings(filePath);
117
+ if (!settings.hooks) {
118
+ settings.hooks = {};
119
+ }
120
+ if (!settings.hooks.Stop) {
121
+ settings.hooks.Stop = [];
122
+ }
123
+ const existingIndex = settings.hooks.Stop.findIndex((entry) => matcherEntryContainsCommand(entry, 'sync-permissions'));
124
+ if (existingIndex !== -1) {
125
+ if (getCommandFromMatcherEntry(settings.hooks.Stop[existingIndex]) === command) {
126
+ (0, logger_1.logInfo)('Permission sync hook already installed');
127
+ return;
128
+ }
129
+ // Stale path from a previous install — update in place
130
+ settings.hooks.Stop[existingIndex].hooks = [{ type: 'command', command }];
131
+ writeClaudeSettings(filePath, settings);
132
+ (0, logger_1.logSuccess)('Permission sync hook updated in ~/.claude/settings.json');
133
+ return;
134
+ }
135
+ settings.hooks.Stop.push({
136
+ hooks: [{ type: 'command', command }],
137
+ });
138
+ writeClaudeSettings(filePath, settings);
139
+ (0, logger_1.logSuccess)('Permission sync hook installed in ~/.claude/settings.json');
140
+ }
141
+ /**
142
+ * Remove the permission-sync Stop hook from ~/.claude/settings.json.
143
+ *
144
+ * @param settingsPath - Override the settings file path (for testing).
145
+ */
146
+ function uninstallPermissionSyncHook(settingsPath) {
147
+ const filePath = settingsPath ?? getDefaultClaudeSettingsPath();
148
+ const settings = readClaudeSettings(filePath);
149
+ if (!settings.hooks?.Stop) {
150
+ (0, logger_1.logInfo)('No permission sync hook found to remove');
151
+ return;
152
+ }
153
+ const before = settings.hooks.Stop.length;
154
+ settings.hooks.Stop = settings.hooks.Stop.filter((entry) => !matcherEntryContainsCommand(entry, 'sync-permissions'));
155
+ const after = settings.hooks.Stop.length;
156
+ if (before === after) {
157
+ (0, logger_1.logInfo)('No permission sync hook found to remove');
158
+ return;
159
+ }
160
+ // Clean up empty arrays/objects
161
+ if (settings.hooks.Stop.length === 0) {
162
+ delete settings.hooks.Stop;
163
+ }
164
+ if (Object.keys(settings.hooks).length === 0) {
165
+ delete settings.hooks;
166
+ }
167
+ writeClaudeSettings(filePath, settings);
168
+ (0, logger_1.logSuccess)('Permission sync hook removed from ~/.claude/settings.json');
169
+ }
170
+ /**
171
+ * Returns true if a permission entry is reusable (should be promoted to global).
172
+ * Filters out absolute-path entries and heredoc commit messages.
173
+ *
174
+ * IMPORTANT: Keep in sync with the equivalent jq filter (IS_REUSABLE) in
175
+ * sync-permissions.sh — both must apply the same rules.
176
+ */
177
+ function isReusablePermission(entry) {
178
+ // Skip entries with absolute user home paths
179
+ if (/\/Users\/|\/home\//.test(entry)) {
180
+ return false;
181
+ }
182
+ // Skip heredoc commit messages
183
+ if (/cat <<.*EOF/.test(entry)) {
184
+ return false;
185
+ }
186
+ return true;
187
+ }
188
+ /**
189
+ * Synchronous sleep using Atomics.wait (no busy-wait CPU burn).
190
+ * Falls back to a short busy-wait if SharedArrayBuffer is unavailable.
191
+ */
192
+ function sleepMs(ms) {
193
+ try {
194
+ const buf = new SharedArrayBuffer(4);
195
+ const arr = new Int32Array(buf);
196
+ Atomics.wait(arr, 0, 0, ms);
197
+ }
198
+ catch {
199
+ const end = Date.now() + ms;
200
+ while (Date.now() < end) { /* fallback busy wait */ }
201
+ }
202
+ }
203
+ /**
204
+ * Simple file-based lock using mkdir (atomic on all platforms).
205
+ * Returns a release function, or null if the lock could not be acquired.
206
+ */
207
+ function acquireFileLock(lockPath, timeoutMs = 5000) {
208
+ const start = Date.now();
209
+ while (true) {
210
+ try {
211
+ fs.mkdirSync(lockPath);
212
+ return () => {
213
+ try {
214
+ fs.rmdirSync(lockPath);
215
+ }
216
+ catch { /* ignore */ }
217
+ };
218
+ }
219
+ catch {
220
+ if (Date.now() - start > timeoutMs) {
221
+ // Stale lock — force-remove and retry once
222
+ try {
223
+ fs.rmdirSync(lockPath);
224
+ }
225
+ catch { /* ignore */ }
226
+ try {
227
+ fs.mkdirSync(lockPath);
228
+ return () => {
229
+ try {
230
+ fs.rmdirSync(lockPath);
231
+ }
232
+ catch { /* ignore */ }
233
+ };
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }
239
+ sleepMs(50);
240
+ }
241
+ }
242
+ }
243
+ /**
244
+ * Merge permissions from a project settings.local.json into the global settings.json.
245
+ * - Syncs both allow and deny entries
246
+ * - Skips non-reusable entries (absolute paths, heredoc commits)
247
+ * - Skips allow entries that conflict with the global deny list
248
+ * - Uses file locking to prevent concurrent write corruption
249
+ *
250
+ * Returns the total number of new entries added (allow + deny).
251
+ */
252
+ function mergePermissions(projectSettingsPath, globalSettingsPath) {
253
+ if (!fs.existsSync(projectSettingsPath)) {
254
+ return 0;
255
+ }
256
+ let projectSettings;
257
+ try {
258
+ projectSettings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf-8'));
259
+ }
260
+ catch {
261
+ return 0;
262
+ }
263
+ const projectAllow = projectSettings?.permissions?.allow ?? [];
264
+ const projectDeny = projectSettings?.permissions?.deny ?? [];
265
+ if (projectAllow.length === 0 && projectDeny.length === 0) {
266
+ return 0;
267
+ }
268
+ // Acquire lock for the read-modify-write cycle
269
+ const lockPath = globalSettingsPath + '.lock';
270
+ const releaseLock = acquireFileLock(lockPath);
271
+ if (!releaseLock) {
272
+ return 0;
273
+ }
274
+ try {
275
+ let globalSettings;
276
+ if (fs.existsSync(globalSettingsPath)) {
277
+ try {
278
+ globalSettings = JSON.parse(fs.readFileSync(globalSettingsPath, 'utf-8'));
279
+ }
280
+ catch {
281
+ globalSettings = {};
282
+ }
283
+ }
284
+ else {
285
+ globalSettings = {};
286
+ }
287
+ if (!globalSettings.permissions) {
288
+ globalSettings.permissions = {};
289
+ }
290
+ if (!globalSettings.permissions.allow) {
291
+ globalSettings.permissions.allow = [];
292
+ }
293
+ if (!globalSettings.permissions.deny) {
294
+ globalSettings.permissions.deny = [];
295
+ }
296
+ const globalAllowSet = new Set(globalSettings.permissions.allow);
297
+ const globalDenySet = new Set(globalSettings.permissions.deny);
298
+ let added = 0;
299
+ // Merge allow entries (skip if in global deny)
300
+ for (const entry of projectAllow) {
301
+ if (!isReusablePermission(entry))
302
+ continue;
303
+ if (globalAllowSet.has(entry))
304
+ continue;
305
+ if (globalDenySet.has(entry))
306
+ continue;
307
+ globalSettings.permissions.allow.push(entry);
308
+ globalAllowSet.add(entry);
309
+ added++;
310
+ }
311
+ // Merge deny entries
312
+ for (const entry of projectDeny) {
313
+ if (!isReusablePermission(entry))
314
+ continue;
315
+ if (globalDenySet.has(entry))
316
+ continue;
317
+ globalSettings.permissions.deny.push(entry);
318
+ globalDenySet.add(entry);
319
+ added++;
320
+ }
321
+ // Clean up empty arrays
322
+ if (globalSettings.permissions.deny.length === 0) {
323
+ delete globalSettings.permissions.deny;
324
+ }
325
+ if (added > 0) {
326
+ const dir = path.dirname(globalSettingsPath);
327
+ if (!fs.existsSync(dir)) {
328
+ fs.mkdirSync(dir, { recursive: true });
329
+ }
330
+ const tmp = globalSettingsPath + '.tmp';
331
+ fs.writeFileSync(tmp, JSON.stringify(globalSettings, null, 2) + '\n', 'utf-8');
332
+ fs.renameSync(tmp, globalSettingsPath);
333
+ }
334
+ return added;
335
+ }
336
+ finally {
337
+ releaseLock();
338
+ }
339
+ }
340
+ /**
341
+ * Scan all workspaces under a directory and merge their permissions into global.
342
+ *
343
+ * Directory structure: workspacesDir/workspace-name/repo-name/.claude/settings.local.json
344
+ * We check both the workspace level (for workspace-scoped settings) and one level deeper
345
+ * (for repo-scoped settings within each workspace).
346
+ */
347
+ function syncAllWorkspacePermissions(workspacesDir) {
348
+ // Write to settings.json (not settings.local.json) because Claude Code only
349
+ // reads ~/.claude/settings.json at the user level. The .local.json variant
350
+ // is only recognized at the project level.
351
+ const globalSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
352
+ if (!fs.existsSync(workspacesDir)) {
353
+ (0, logger_1.logWarning)(`Workspaces directory not found: ${workspacesDir}`);
354
+ return;
355
+ }
356
+ let totalAdded = 0;
357
+ const workspaces = fs.readdirSync(workspacesDir, { withFileTypes: true });
358
+ for (const workspace of workspaces) {
359
+ if (!workspace.isDirectory())
360
+ continue;
361
+ const workspacePath = path.join(workspacesDir, workspace.name);
362
+ // Check workspace-level settings
363
+ const workspaceSettings = path.join(workspacePath, '.claude', 'settings.local.json');
364
+ const wsAdded = mergePermissions(workspaceSettings, globalSettingsPath);
365
+ if (wsAdded > 0) {
366
+ (0, logger_1.logInfo)(`Merged ${wsAdded} permission(s) from ${workspace.name}`);
367
+ totalAdded += wsAdded;
368
+ }
369
+ // Check repo-level settings within the workspace
370
+ let repos;
371
+ try {
372
+ repos = fs.readdirSync(workspacePath, { withFileTypes: true });
373
+ }
374
+ catch {
375
+ continue;
376
+ }
377
+ for (const repo of repos) {
378
+ if (!repo.isDirectory())
379
+ continue;
380
+ const repoSettings = path.join(workspacePath, repo.name, '.claude', 'settings.local.json');
381
+ const repoAdded = mergePermissions(repoSettings, globalSettingsPath);
382
+ if (repoAdded > 0) {
383
+ (0, logger_1.logInfo)(`Merged ${repoAdded} permission(s) from ${workspace.name}/${repo.name}`);
384
+ totalAdded += repoAdded;
385
+ }
386
+ }
387
+ }
388
+ if (totalAdded > 0) {
389
+ (0, logger_1.logSuccess)(`Synced ${totalAdded} new permission(s) to global settings`);
390
+ }
391
+ else {
392
+ (0, logger_1.logInfo)('No new permissions to sync');
393
+ }
394
+ }
395
+ // ---------------------------------------------------------------------------
396
+ // Workspace-manager skills (MCP + CLI command skills)
397
+ // ---------------------------------------------------------------------------
398
+ function getSkillsSourceDir() {
399
+ // __dirname is dist/utils/ or src/utils/ — either way, two levels up is project root
400
+ const dir = path.resolve(__dirname, '..', '..', 'skills');
401
+ if (fs.existsSync(dir)) {
402
+ return dir;
403
+ }
404
+ throw new Error('Could not find skills/ directory. Make sure the package is installed correctly.');
405
+ }
406
+ /**
407
+ * Install all workspace-manager skills from the skills/ directory.
408
+ * Each .md file becomes a skill under <skillsDir>/<skill-name>/SKILL.md.
409
+ * Installs to all active agent skills directories.
410
+ * Idempotent — safe to call multiple times.
411
+ */
412
+ function installWorkspaceSkills(skillsDir) {
413
+ let sourceDir;
414
+ try {
415
+ sourceDir = getSkillsSourceDir();
416
+ }
417
+ catch {
418
+ return; // skills dir not found — skip silently (non-critical)
419
+ }
420
+ // If a specific dir is provided (e.g. from tests), install only there.
421
+ // Otherwise install to all active agent directories.
422
+ const targetDirs = skillsDir ? [skillsDir] : (0, agent_config_1.getSkillsTargetDirs)();
423
+ // Collect installable skills from source:
424
+ // 1. Flat .md files → installed as <name>/SKILL.md
425
+ // 2. Directories containing SKILL.md → copied recursively (includes references/)
426
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
427
+ const flatFiles = entries.filter(e => e.isFile() && e.name.endsWith('.md'));
428
+ const skillDirs = entries.filter(e => e.isDirectory() && fs.existsSync(path.join(sourceDir, e.name, 'SKILL.md')));
429
+ for (const targetBase of targetDirs) {
430
+ let installed = 0;
431
+ // Install flat .md files as <name>/SKILL.md
432
+ for (const entry of flatFiles) {
433
+ const skillName = entry.name.replace(/\.md$/, '');
434
+ const targetDir = path.join(targetBase, skillName);
435
+ const targetPath = path.join(targetDir, 'SKILL.md');
436
+ const sourcePath = path.join(sourceDir, entry.name);
437
+ if (!fs.existsSync(targetDir)) {
438
+ fs.mkdirSync(targetDir, { recursive: true });
439
+ }
440
+ const content = fs.readFileSync(sourcePath, 'utf-8');
441
+ fs.writeFileSync(targetPath, content, 'utf-8');
442
+ installed++;
443
+ }
444
+ // Install skill directories (copy SKILL.md + references/ etc.)
445
+ for (const entry of skillDirs) {
446
+ const skillSourceDir = path.join(sourceDir, entry.name);
447
+ const targetDir = path.join(targetBase, entry.name);
448
+ copyDirRecursive(skillSourceDir, targetDir);
449
+ installed++;
450
+ }
451
+ if (installed > 0) {
452
+ (0, logger_1.logSuccess)(`Installed ${installed} workspace-manager skills in ${targetBase}`);
453
+ }
454
+ }
455
+ }
456
+ /** Recursively copy a directory, creating target dirs as needed. */
457
+ function copyDirRecursive(src, dest) {
458
+ if (!fs.existsSync(dest)) {
459
+ fs.mkdirSync(dest, { recursive: true });
460
+ }
461
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
462
+ const srcPath = path.join(src, entry.name);
463
+ const destPath = path.join(dest, entry.name);
464
+ if (entry.isDirectory()) {
465
+ copyDirRecursive(srcPath, destPath);
466
+ }
467
+ else {
468
+ fs.copyFileSync(srcPath, destPath);
469
+ }
470
+ }
471
+ }
472
+ /**
473
+ * Remove all workspace-manager skills from all possible agent directories.
474
+ * Reads the skills/ source directory to know which skill names to remove.
475
+ * Removes from both Claude and Pi dirs regardless of current config to avoid orphans.
476
+ */
477
+ function uninstallWorkspaceSkills(skillsDir) {
478
+ let sourceDir;
479
+ try {
480
+ sourceDir = getSkillsSourceDir();
481
+ }
482
+ catch {
483
+ return;
484
+ }
485
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
486
+ const flatFiles = entries.filter(e => e.isFile() && e.name.endsWith('.md'));
487
+ const skillDirs = entries.filter(e => e.isDirectory() && fs.existsSync(path.join(sourceDir, e.name, 'SKILL.md')));
488
+ // Collect all skill names to remove
489
+ const skillNames = [
490
+ ...flatFiles.map(e => e.name.replace(/\.md$/, '')),
491
+ ...skillDirs.map(e => e.name),
492
+ ];
493
+ // If specific dir provided, use that. Otherwise, uninstall from ALL agent dirs
494
+ // to avoid orphan skills when user changes config.
495
+ // Note: OpenCode reads from ~/.claude/skills/ natively, so no separate dir needed.
496
+ const targetDirs = skillsDir ? [skillsDir] : [
497
+ (0, agent_config_1.getAgentPaths)('claude').skillsDir,
498
+ (0, agent_config_1.getAgentPaths)('pi').skillsDir,
499
+ ];
500
+ for (const targetBase of targetDirs) {
501
+ for (const skillName of skillNames) {
502
+ const targetDir = path.join(targetBase, skillName);
503
+ if (fs.existsSync(targetDir)) {
504
+ fs.rmSync(targetDir, { recursive: true, force: true });
505
+ }
506
+ }
507
+ }
508
+ }
509
+ // ─── Workspace status-line ────────────────────────────────────────────────────
510
+ /** Absolute install path for the workspace-table Python script. */
511
+ const WORKSPACE_TABLE_SCRIPT = path.join(os.homedir(), '.workspace-manager-table.py');
512
+ /**
513
+ * Resolve the bundled workspace-table.py path from the installed package.
514
+ * When compiled, __dirname is dist/utils/; the scripts/ dir is at dist/scripts/.
515
+ */
516
+ function getWorkspaceTableSourcePath() {
517
+ const distPath = path.join(__dirname, '..', 'scripts', 'workspace-table.py');
518
+ const srcPath = path.join(__dirname, '..', '..', 'src', 'scripts', 'workspace-table.py');
519
+ if (fs.existsSync(distPath))
520
+ return distPath;
521
+ if (fs.existsSync(srcPath))
522
+ return srcPath;
523
+ throw new Error('Could not find workspace-table.py in package');
524
+ }
525
+ /**
526
+ * Install the workspace repo-table as an *additional* statusLine in Claude Code.
527
+ *
528
+ * The script only outputs content when the cwd is inside a workspace AND at
529
+ * least one repo has a non-default branch. Otherwise it exits silently and
530
+ * Claude Code shows its default footer — the user's existing styling is
531
+ * never affected.
532
+ *
533
+ * The script is copied to ~/.workspace-manager-table.py and registered in
534
+ * ~/.claude/settings.json only when no statusLine is already configured —
535
+ * we never overwrite a custom status line the user already has.
536
+ *
537
+ * @param settingsPath Override ~/.claude/settings.json (for testing).
538
+ */
539
+ function installWorkspaceStatusLine(settingsPath) {
540
+ const filePath = settingsPath ?? getDefaultClaudeSettingsPath();
541
+ const settings = readClaudeSettings(filePath);
542
+ if (settings.statusLine) {
543
+ (0, logger_1.logInfo)('statusLine already configured in ~/.claude/settings.json — skipping workspace-table install');
544
+ (0, logger_1.logInfo)(` To use workspace-manager\'s table, replace your statusLine command with: ${WORKSPACE_TABLE_SCRIPT}`);
545
+ return;
546
+ }
547
+ // Copy the script
548
+ let sourcePath;
549
+ try {
550
+ sourcePath = getWorkspaceTableSourcePath();
551
+ }
552
+ catch {
553
+ (0, logger_1.logWarning)('Could not locate workspace-table.py — skipping status-line install');
554
+ return;
555
+ }
556
+ fs.copyFileSync(sourcePath, WORKSPACE_TABLE_SCRIPT);
557
+ fs.chmodSync(WORKSPACE_TABLE_SCRIPT, 0o755);
558
+ // Register in settings.json
559
+ settings.statusLine = {
560
+ type: 'command',
561
+ command: WORKSPACE_TABLE_SCRIPT,
562
+ padding: 0,
563
+ };
564
+ writeClaudeSettings(filePath, settings);
565
+ (0, logger_1.logSuccess)(`Workspace status-line installed (${WORKSPACE_TABLE_SCRIPT})`);
566
+ }
567
+ /**
568
+ * Remove the workspace status-line from Claude Code\'s settings.json.
569
+ * Only removes it if the command points to workspace-manager\'s own script.
570
+ *
571
+ * @param settingsPath Override ~/.claude/settings.json (for testing).
572
+ */
573
+ function uninstallWorkspaceStatusLine(settingsPath) {
574
+ const filePath = settingsPath ?? getDefaultClaudeSettingsPath();
575
+ const settings = readClaudeSettings(filePath);
576
+ const existing = settings.statusLine;
577
+ if (!existing) {
578
+ (0, logger_1.logInfo)('No statusLine configured — nothing to remove');
579
+ return;
580
+ }
581
+ const cmd = existing.command ?? '';
582
+ if (!cmd.includes('workspace-manager-table')) {
583
+ (0, logger_1.logInfo)('statusLine is not managed by workspace-manager — leaving it unchanged');
584
+ return;
585
+ }
586
+ delete settings.statusLine;
587
+ writeClaudeSettings(filePath, settings);
588
+ // Remove the installed script if it exists
589
+ if (fs.existsSync(WORKSPACE_TABLE_SCRIPT)) {
590
+ try {
591
+ fs.unlinkSync(WORKSPACE_TABLE_SCRIPT);
592
+ }
593
+ catch { /* ignore */ }
594
+ }
595
+ (0, logger_1.logSuccess)('Workspace status-line removed from ~/.claude/settings.json');
596
+ }
597
+ // ─── Self-heal stale hook paths ──────────────────────────────────────────────
598
+ /** Extract the script path (…/foo.sh or …/foo.py) referenced by a hook command. */
599
+ function extractScriptPath(command) {
600
+ // Commands look like: bash "/abs/path/sync-permissions.sh" or /abs/path/table.py
601
+ const quoted = command.match(/"([^"]+\.(?:sh|py|js))"/);
602
+ if (quoted)
603
+ return quoted[1];
604
+ const bare = command.match(/(\/[^\s"']+\.(?:sh|py|js))/);
605
+ return bare ? bare[1] : null;
606
+ }
607
+ /**
608
+ * Detect and repair workspace-manager hook commands in ~/.claude/settings.json
609
+ * that point to script paths which no longer exist on disk.
610
+ *
611
+ * This happens when the package is installed via a manager (e.g. Volta) whose
612
+ * postinstall runs from an ephemeral temp staging dir
613
+ * (~/.volta/tmp/image/packages/.tmpXXXX/...). That dead path gets baked into
614
+ * settings.json; once the temp dir is cleaned up the Stop/SessionStart/
615
+ * PostToolUse hooks fail with "No such file or directory".
616
+ *
617
+ * Called best-effort on CLI startup. Because the CLI runs from the package's
618
+ * STABLE install location (the bin shim resolves there), re-resolving the hook
619
+ * commands here produces correct, durable paths.
620
+ *
621
+ * Returns the number of hooks repaired. Silent and non-throwing.
622
+ *
623
+ * @param settingsPath Override ~/.claude/settings.json (for testing).
624
+ */
625
+ function repairStaleHooks(settingsPath) {
626
+ const filePath = settingsPath ?? getDefaultClaudeSettingsPath();
627
+ let settings;
628
+ try {
629
+ if (!fs.existsSync(filePath))
630
+ return 0;
631
+ settings = readClaudeSettings(filePath);
632
+ }
633
+ catch {
634
+ return 0;
635
+ }
636
+ if (!settings.hooks)
637
+ return 0;
638
+ // (hook category, command-substring identifying our hook, fresh-command resolver)
639
+ const specs = [
640
+ { category: 'Stop', needle: 'sync-permissions', resolve: getPermissionSyncHookCommand },
641
+ ];
642
+ let repaired = 0;
643
+ for (const spec of specs) {
644
+ const entries = settings.hooks[spec.category];
645
+ if (!Array.isArray(entries))
646
+ continue;
647
+ for (const entry of entries) {
648
+ if (!matcherEntryContainsCommand(entry, spec.needle))
649
+ continue;
650
+ const current = getCommandFromMatcherEntry(entry);
651
+ if (!current)
652
+ continue;
653
+ const scriptPath = extractScriptPath(current);
654
+ // Only repair when the referenced script is genuinely missing.
655
+ if (scriptPath && fs.existsSync(scriptPath))
656
+ continue;
657
+ let fresh;
658
+ try {
659
+ fresh = spec.resolve();
660
+ }
661
+ catch {
662
+ continue; // can't resolve a stable path right now — leave as-is
663
+ }
664
+ if (fresh === current)
665
+ continue;
666
+ if (entry.hooks) {
667
+ entry.hooks = entry.hooks.map((h) => h.command.includes(spec.needle) ? { ...h, command: fresh } : h);
668
+ repaired++;
669
+ }
670
+ }
671
+ }
672
+ if (repaired > 0) {
673
+ try {
674
+ writeClaudeSettings(filePath, settings);
675
+ }
676
+ catch {
677
+ return 0;
678
+ }
679
+ }
680
+ return repaired;
681
+ }