specweave 0.32.9 → 0.33.2

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 (179) hide show
  1. package/CLAUDE.md +106 -1
  2. package/dist/src/cli/add-child-pid.d.ts +11 -0
  3. package/dist/src/cli/add-child-pid.d.ts.map +1 -0
  4. package/dist/src/cli/add-child-pid.js +42 -0
  5. package/dist/src/cli/add-child-pid.js.map +1 -0
  6. package/dist/src/cli/add-child-process.d.ts +15 -0
  7. package/dist/src/cli/add-child-process.d.ts.map +1 -0
  8. package/dist/src/cli/add-child-process.js +40 -0
  9. package/dist/src/cli/add-child-process.js.map +1 -0
  10. package/dist/src/cli/check-watchdog.d.ts +15 -0
  11. package/dist/src/cli/check-watchdog.d.ts.map +1 -0
  12. package/dist/src/cli/check-watchdog.js +47 -0
  13. package/dist/src/cli/check-watchdog.js.map +1 -0
  14. package/dist/src/cli/cleanup-zombies.d.ts +14 -0
  15. package/dist/src/cli/cleanup-zombies.d.ts.map +1 -0
  16. package/dist/src/cli/cleanup-zombies.js +268 -0
  17. package/dist/src/cli/cleanup-zombies.js.map +1 -0
  18. package/dist/src/cli/commands/jobs.js +9 -2
  19. package/dist/src/cli/commands/jobs.js.map +1 -1
  20. package/dist/src/cli/find-session-by-pid.d.ts +14 -0
  21. package/dist/src/cli/find-session-by-pid.d.ts.map +1 -0
  22. package/dist/src/cli/find-session-by-pid.js +45 -0
  23. package/dist/src/cli/find-session-by-pid.js.map +1 -0
  24. package/dist/src/cli/get-stale-sessions.d.ts +17 -0
  25. package/dist/src/cli/get-stale-sessions.d.ts.map +1 -0
  26. package/dist/src/cli/get-stale-sessions.js +36 -0
  27. package/dist/src/cli/get-stale-sessions.js.map +1 -0
  28. package/dist/src/cli/register-session.d.ts +16 -0
  29. package/dist/src/cli/register-session.d.ts.map +1 -0
  30. package/dist/src/cli/register-session.js +48 -0
  31. package/dist/src/cli/register-session.js.map +1 -0
  32. package/dist/src/cli/remove-session.d.ts +11 -0
  33. package/dist/src/cli/remove-session.d.ts.map +1 -0
  34. package/dist/src/cli/remove-session.js +36 -0
  35. package/dist/src/cli/remove-session.js.map +1 -0
  36. package/dist/src/cli/update-heartbeat.d.ts +11 -0
  37. package/dist/src/cli/update-heartbeat.d.ts.map +1 -0
  38. package/dist/src/cli/update-heartbeat.js +36 -0
  39. package/dist/src/cli/update-heartbeat.js.map +1 -0
  40. package/dist/src/cli/workers/living-docs-worker.js +6 -0
  41. package/dist/src/cli/workers/living-docs-worker.js.map +1 -1
  42. package/dist/src/config/types.d.ts +1208 -203
  43. package/dist/src/config/types.d.ts.map +1 -1
  44. package/dist/src/core/background/job-launcher.d.ts.map +1 -1
  45. package/dist/src/core/background/job-launcher.js +7 -4
  46. package/dist/src/core/background/job-launcher.js.map +1 -1
  47. package/dist/src/core/background/job-manager.d.ts +25 -0
  48. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  49. package/dist/src/core/background/job-manager.js +136 -15
  50. package/dist/src/core/background/job-manager.js.map +1 -1
  51. package/dist/src/core/increment/increment-utils.d.ts +26 -1
  52. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  53. package/dist/src/core/increment/increment-utils.js +66 -4
  54. package/dist/src/core/increment/increment-utils.js.map +1 -1
  55. package/dist/src/core/increment/status-change-sync-trigger.d.ts +3 -1
  56. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
  57. package/dist/src/core/increment/status-change-sync-trigger.js +5 -2
  58. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
  59. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
  60. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +48 -12
  61. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
  62. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts +70 -0
  63. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts.map +1 -0
  64. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js +188 -0
  65. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js.map +1 -0
  66. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts +33 -0
  67. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts.map +1 -0
  68. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js +290 -0
  69. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js.map +1 -0
  70. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -1
  71. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +114 -11
  72. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -1
  73. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts +23 -0
  74. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts.map +1 -0
  75. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js +283 -0
  76. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js.map +1 -0
  77. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts +44 -0
  78. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts.map +1 -0
  79. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js +61 -0
  80. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js.map +1 -0
  81. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts +126 -0
  82. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts.map +1 -0
  83. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js +378 -0
  84. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js.map +1 -0
  85. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
  86. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +57 -0
  87. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
  88. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts +82 -0
  89. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts.map +1 -0
  90. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js +430 -0
  91. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js.map +1 -0
  92. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts +84 -0
  93. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts.map +1 -0
  94. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js +387 -0
  95. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js.map +1 -0
  96. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts +61 -0
  97. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts.map +1 -0
  98. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js +174 -0
  99. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js.map +1 -0
  100. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +1 -1
  101. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
  102. package/dist/src/core/living-docs/module-analyzer.d.ts +3 -0
  103. package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
  104. package/dist/src/core/living-docs/module-analyzer.js +40 -1
  105. package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
  106. package/dist/src/core/qa/qa-runner.js +1 -1
  107. package/dist/src/core/qa/qa-runner.js.map +1 -1
  108. package/dist/src/core/scheduler/session-sync-executor.js +1 -1
  109. package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
  110. package/dist/src/core/status-line/status-line-updater.d.ts +1 -1
  111. package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -1
  112. package/dist/src/core/status-line/status-line-updater.js +4 -3
  113. package/dist/src/core/status-line/status-line-updater.js.map +1 -1
  114. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  115. package/dist/src/importers/jira-importer.js +26 -10
  116. package/dist/src/importers/jira-importer.js.map +1 -1
  117. package/dist/src/init/architecture/types.d.ts +140 -33
  118. package/dist/src/init/architecture/types.d.ts.map +1 -1
  119. package/dist/src/init/compliance/types.d.ts +27 -30
  120. package/dist/src/init/compliance/types.d.ts.map +1 -1
  121. package/dist/src/init/repo/types.d.ts +34 -11
  122. package/dist/src/init/repo/types.d.ts.map +1 -1
  123. package/dist/src/init/research/src/config/types.d.ts +82 -15
  124. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  125. package/dist/src/init/research/types.d.ts +93 -38
  126. package/dist/src/init/research/types.d.ts.map +1 -1
  127. package/dist/src/init/team/types.d.ts +42 -4
  128. package/dist/src/init/team/types.d.ts.map +1 -1
  129. package/dist/src/sync/ado-reconciler.js +1 -1
  130. package/dist/src/sync/ado-reconciler.js.map +1 -1
  131. package/dist/src/sync/github-reconciler.js +1 -1
  132. package/dist/src/sync/github-reconciler.js.map +1 -1
  133. package/dist/src/sync/jira-reconciler.js +1 -1
  134. package/dist/src/sync/jira-reconciler.js.map +1 -1
  135. package/dist/src/types/session.d.ts +65 -0
  136. package/dist/src/types/session.d.ts.map +1 -0
  137. package/dist/src/types/session.js +8 -0
  138. package/dist/src/types/session.js.map +1 -0
  139. package/dist/src/utils/lock-manager.d.ts +48 -0
  140. package/dist/src/utils/lock-manager.d.ts.map +1 -0
  141. package/dist/src/utils/lock-manager.js +195 -0
  142. package/dist/src/utils/lock-manager.js.map +1 -0
  143. package/dist/src/utils/notification-manager.d.ts +45 -0
  144. package/dist/src/utils/notification-manager.d.ts.map +1 -0
  145. package/dist/src/utils/notification-manager.js +130 -0
  146. package/dist/src/utils/notification-manager.js.map +1 -0
  147. package/dist/src/utils/platform-utils.d.ts +136 -0
  148. package/dist/src/utils/platform-utils.d.ts.map +1 -0
  149. package/dist/src/utils/platform-utils.js +366 -0
  150. package/dist/src/utils/platform-utils.js.map +1 -0
  151. package/dist/src/utils/session-registry.d.ts +142 -0
  152. package/dist/src/utils/session-registry.d.ts.map +1 -0
  153. package/dist/src/utils/session-registry.js +480 -0
  154. package/dist/src/utils/session-registry.js.map +1 -0
  155. package/package.json +5 -2
  156. package/plugins/specweave/commands/specweave-living-docs.md +42 -0
  157. package/plugins/specweave/hooks/hooks.json +10 -0
  158. package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
  159. package/plugins/specweave/hooks/lib/update-status-line.sh +1 -1
  160. package/plugins/specweave/hooks/post-increment-status-change.sh +3 -3
  161. package/plugins/specweave/hooks/post-metadata-change.sh +1 -1
  162. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  163. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  164. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  165. package/plugins/specweave/hooks/user-prompt-submit.sh +2 -2
  166. package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +61 -0
  167. package/plugins/specweave/hooks/v2/session-end.sh +69 -0
  168. package/plugins/specweave/hooks/v2/session-start.sh +81 -0
  169. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +1 -1
  170. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  171. package/plugins/specweave/scripts/heartbeat.sh +110 -0
  172. package/plugins/specweave/scripts/progress.js +34 -4
  173. package/plugins/specweave/scripts/read-jobs.sh +1 -1
  174. package/plugins/specweave/scripts/read-progress.sh +50 -5
  175. package/plugins/specweave/scripts/read-workflow.sh +1 -1
  176. package/plugins/specweave/scripts/session-watchdog.sh +65 -0
  177. package/plugins/specweave/scripts/status.js +28 -11
  178. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
  179. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1107 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Notification Manager - Cross-platform system notifications
3
+ *
4
+ * Sends native notifications on macOS, Linux, and Windows
5
+ */
6
+ import { Logger } from './logger.js';
7
+ export declare class NotificationManager {
8
+ private logger;
9
+ constructor(options?: {
10
+ logger?: Logger;
11
+ });
12
+ /**
13
+ * Sends a system notification (cross-platform)
14
+ *
15
+ * @param title - Notification title
16
+ * @param body - Notification message
17
+ * @param sound - Optional sound name (macOS only)
18
+ */
19
+ sendNotification(title: string, body: string, sound?: string): Promise<void>;
20
+ /**
21
+ * Sends notification on macOS using osascript
22
+ */
23
+ private sendMacOSNotification;
24
+ /**
25
+ * Sends notification on Linux using notify-send
26
+ */
27
+ private sendLinuxNotification;
28
+ /**
29
+ * Sends notification on Windows using PowerShell toast
30
+ */
31
+ private sendWindowsNotification;
32
+ /**
33
+ * Escapes string for AppleScript
34
+ */
35
+ private escapeForAppleScript;
36
+ /**
37
+ * Escapes string for shell
38
+ */
39
+ private escapeForShell;
40
+ /**
41
+ * Escapes string for PowerShell
42
+ */
43
+ private escapeForPowerShell;
44
+ }
45
+ //# sourceMappingURL=notification-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-manager.d.ts","sourceRoot":"","sources":["../../../src/utils/notification-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AAEpD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAI7C;;;;;;OAMG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBlF;;OAEG;YACW,qBAAqB;IAmBnC;;OAEG;YACW,qBAAqB;IAqBnC;;OAEG;YACW,uBAAuB;IA2BrC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAI5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAG5B"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Notification Manager - Cross-platform system notifications
3
+ *
4
+ * Sends native notifications on macOS, Linux, and Windows
5
+ */
6
+ import { exec } from 'child_process';
7
+ import { consoleLogger } from './logger.js';
8
+ export class NotificationManager {
9
+ constructor(options = {}) {
10
+ this.logger = options.logger ?? consoleLogger;
11
+ }
12
+ /**
13
+ * Sends a system notification (cross-platform)
14
+ *
15
+ * @param title - Notification title
16
+ * @param body - Notification message
17
+ * @param sound - Optional sound name (macOS only)
18
+ */
19
+ async sendNotification(title, body, sound) {
20
+ const platform = process.platform;
21
+ try {
22
+ if (platform === 'darwin') {
23
+ await this.sendMacOSNotification(title, body, sound);
24
+ }
25
+ else if (platform === 'linux') {
26
+ await this.sendLinuxNotification(title, body);
27
+ }
28
+ else if (platform === 'win32') {
29
+ await this.sendWindowsNotification(title, body);
30
+ }
31
+ else {
32
+ this.logger.warn(`Notifications not supported on platform: ${platform}`);
33
+ this.logger.info(`[Notification] ${title}: ${body}`);
34
+ }
35
+ }
36
+ catch (err) {
37
+ this.logger.error('Failed to send notification:', err);
38
+ // Fallback to console
39
+ this.logger.info(`[Notification] ${title}: ${body}`);
40
+ }
41
+ }
42
+ /**
43
+ * Sends notification on macOS using osascript
44
+ */
45
+ async sendMacOSNotification(title, body, sound) {
46
+ return new Promise((resolve, reject) => {
47
+ const soundParam = sound ? ` sound name "${sound}"` : '';
48
+ const script = `display notification "${this.escapeForAppleScript(body)}" with title "${this.escapeForAppleScript(title)}"${soundParam}`;
49
+ exec(`osascript -e '${script}'`, (error) => {
50
+ if (error) {
51
+ reject(error);
52
+ }
53
+ else {
54
+ resolve();
55
+ }
56
+ });
57
+ });
58
+ }
59
+ /**
60
+ * Sends notification on Linux using notify-send
61
+ */
62
+ async sendLinuxNotification(title, body) {
63
+ return new Promise((resolve, reject) => {
64
+ // Check if notify-send is available
65
+ exec('command -v notify-send', (error) => {
66
+ if (error) {
67
+ this.logger.warn('notify-send not found, skipping notification');
68
+ resolve();
69
+ return;
70
+ }
71
+ exec(`notify-send "${this.escapeForShell(title)}" "${this.escapeForShell(body)}"`, (error) => {
72
+ if (error) {
73
+ reject(error);
74
+ }
75
+ else {
76
+ resolve();
77
+ }
78
+ });
79
+ });
80
+ });
81
+ }
82
+ /**
83
+ * Sends notification on Windows using PowerShell toast
84
+ */
85
+ async sendWindowsNotification(title, body) {
86
+ return new Promise((resolve, reject) => {
87
+ const script = `
88
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
89
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
90
+
91
+ $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
92
+ $toastXml = [xml] $template.GetXml()
93
+ $toastXml.GetElementsByTagName("text")[0].AppendChild($toastXml.CreateTextNode("${this.escapeForPowerShell(title)}")) | Out-Null
94
+ $toastXml.GetElementsByTagName("text")[1].AppendChild($toastXml.CreateTextNode("${this.escapeForPowerShell(body)}")) | Out-Null
95
+
96
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
97
+ $xml.LoadXml($toastXml.OuterXml)
98
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
99
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("SpecWeave").Show($toast)
100
+ `;
101
+ exec(`powershell -Command "${script}"`, (error) => {
102
+ if (error) {
103
+ reject(error);
104
+ }
105
+ else {
106
+ resolve();
107
+ }
108
+ });
109
+ });
110
+ }
111
+ /**
112
+ * Escapes string for AppleScript
113
+ */
114
+ escapeForAppleScript(str) {
115
+ return str.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
116
+ }
117
+ /**
118
+ * Escapes string for shell
119
+ */
120
+ escapeForShell(str) {
121
+ return str.replace(/"/g, '\\"').replace(/\$/g, '\\$');
122
+ }
123
+ /**
124
+ * Escapes string for PowerShell
125
+ */
126
+ escapeForPowerShell(str) {
127
+ return str.replace(/"/g, '`"').replace(/\$/g, '`$');
128
+ }
129
+ }
130
+ //# sourceMappingURL=notification-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-manager.js","sourceRoot":"","sources":["../../../src/utils/notification-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Platform Utilities - Cross-platform OS operations
3
+ *
4
+ * Abstracts platform-specific operations for macOS, Linux, and Windows
5
+ * Enhanced for process lifecycle management with async API, logger injection,
6
+ * and system notifications
7
+ */
8
+ import { Logger } from './logger.js';
9
+ export type Platform = 'darwin' | 'linux' | 'win32';
10
+ /**
11
+ * Gets the current platform
12
+ */
13
+ export declare function getCurrentPlatform(): Platform;
14
+ /**
15
+ * Checks if a process exists (cross-platform)
16
+ *
17
+ * @param pid - Process ID to check
18
+ * @returns true if process exists
19
+ */
20
+ export declare function checkProcessExists(pid: number): boolean;
21
+ /**
22
+ * Gets file modification time in seconds since epoch (cross-platform)
23
+ *
24
+ * @param filePath - Path to file
25
+ * @returns Modification time in seconds since epoch
26
+ */
27
+ export declare function getFileMtime(filePath: string): number;
28
+ /**
29
+ * Kills a process (cross-platform)
30
+ *
31
+ * @param pid - Process ID to kill
32
+ * @param signal - Signal to send (default: SIGTERM)
33
+ */
34
+ export declare function killProcess(pid: number, signal?: string): void;
35
+ /**
36
+ * Acquires a file lock using atomic directory creation (cross-platform)
37
+ *
38
+ * @param lockPath - Path to lock directory
39
+ * @returns true if lock acquired
40
+ */
41
+ export declare function acquireFileLock(lockPath: string): boolean;
42
+ export interface PlatformUtilsOptions {
43
+ logger?: Logger;
44
+ }
45
+ /**
46
+ * Enhanced cross-platform utility functions with async API and logger injection
47
+ */
48
+ export declare class PlatformUtils {
49
+ private platform;
50
+ private logger;
51
+ constructor(options?: PlatformUtilsOptions);
52
+ /**
53
+ * Get current platform
54
+ */
55
+ getPlatform(): Platform;
56
+ /**
57
+ * Check if current platform is Windows
58
+ */
59
+ isWindows(): boolean;
60
+ /**
61
+ * Check if current platform is POSIX (macOS or Linux)
62
+ */
63
+ isPosix(): boolean;
64
+ /**
65
+ * Check if a process exists by PID (async)
66
+ *
67
+ * - macOS/Linux: kill -0 $pid (signal 0 = existence check)
68
+ * - Windows: tasklist /FI "PID eq $pid"
69
+ *
70
+ * @param pid Process ID to check
71
+ * @returns true if process exists, false otherwise
72
+ */
73
+ checkProcessExistsAsync(pid: number): Promise<boolean>;
74
+ /**
75
+ * Get file modification time (async, uses fs.stat for cross-platform)
76
+ *
77
+ * @param path File path
78
+ * @returns Modification time in seconds since epoch
79
+ */
80
+ getFileMtimeAsync(path: string): Promise<number>;
81
+ /**
82
+ * Acquire file lock using atomic mkdir (async)
83
+ *
84
+ * @param lockPath Path to lock directory
85
+ * @param timeoutMs Maximum time to wait for lock (default: 5000ms)
86
+ * @returns true if lock acquired, false if timeout
87
+ */
88
+ acquireFileLockAsync(lockPath: string, timeoutMs?: number): Promise<boolean>;
89
+ /**
90
+ * Release file lock (async)
91
+ *
92
+ * @param lockPath Path to lock directory
93
+ */
94
+ releaseFileLockAsync(lockPath: string): Promise<void>;
95
+ /**
96
+ * Kill process with signal (async)
97
+ *
98
+ * @param pid Process ID to kill
99
+ * @param signal Signal to send (default: SIGTERM)
100
+ */
101
+ killProcessAsync(pid: number, signal?: NodeJS.Signals): Promise<void>;
102
+ /**
103
+ * Kill process with graceful fallback (SIGTERM → SIGKILL)
104
+ *
105
+ * @param pid Process ID to kill
106
+ * @param gracePeriodMs Time to wait after SIGTERM before SIGKILL (default: 2000ms)
107
+ */
108
+ killProcessGracefully(pid: number, gracePeriodMs?: number): Promise<void>;
109
+ /**
110
+ * Send system notification
111
+ *
112
+ * - macOS: osascript (AppleScript)
113
+ * - Linux: notify-send (if available)
114
+ * - Windows: PowerShell toast notification
115
+ *
116
+ * Fails gracefully if notification system is unavailable
117
+ *
118
+ * @param title Notification title
119
+ * @param body Notification body text
120
+ */
121
+ sendNotification(title: string, body: string): Promise<void>;
122
+ /**
123
+ * macOS notification using osascript
124
+ */
125
+ private sendNotificationMacOS;
126
+ /**
127
+ * Linux notification using notify-send
128
+ */
129
+ private sendNotificationLinux;
130
+ /**
131
+ * Windows notification using PowerShell toast
132
+ */
133
+ private sendNotificationWindows;
134
+ }
135
+ export declare const platformUtils: PlatformUtils;
136
+ //# sourceMappingURL=platform-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/platform-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAEpD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,QAAQ,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAevD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBrD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAkB,GAAG,IAAI,CAUzE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWzD;AAID,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,GAAE,oBAAyB;IAK9C;;OAEG;IACH,WAAW,IAAI,QAAQ;IAIvB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;;;;;OAQG;IACG,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B5D;;;;;OAKG;IACG,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUtD;;;;;;OAMG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAsBxF;;;;OAIG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3D;;;;;OAKG;IACG,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,OAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBtF;;;;;OAKG;IACG,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,GAAE,MAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCrF;;;;;;;;;;;OAWG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAelE;;OAEG;YACW,qBAAqB;IAOnC;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;YACW,uBAAuB;CAiBtC;AAGD,eAAO,MAAM,aAAa,eAAsB,CAAC"}
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Platform Utilities - Cross-platform OS operations
3
+ *
4
+ * Abstracts platform-specific operations for macOS, Linux, and Windows
5
+ * Enhanced for process lifecycle management with async API, logger injection,
6
+ * and system notifications
7
+ */
8
+ import { execSync } from 'child_process';
9
+ import { stat, mkdir, rmdir } from 'fs/promises';
10
+ import { consoleLogger } from './logger.js';
11
+ /**
12
+ * Gets the current platform
13
+ */
14
+ export function getCurrentPlatform() {
15
+ return process.platform;
16
+ }
17
+ /**
18
+ * Checks if a process exists (cross-platform)
19
+ *
20
+ * @param pid - Process ID to check
21
+ * @returns true if process exists
22
+ */
23
+ export function checkProcessExists(pid) {
24
+ try {
25
+ if (process.platform === 'win32') {
26
+ const output = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
27
+ encoding: 'utf-8',
28
+ stdio: ['ignore', 'pipe', 'ignore'],
29
+ });
30
+ return output.includes(String(pid));
31
+ }
32
+ else {
33
+ execSync(`kill -0 ${pid}`, { stdio: 'ignore' });
34
+ return true;
35
+ }
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Gets file modification time in seconds since epoch (cross-platform)
43
+ *
44
+ * @param filePath - Path to file
45
+ * @returns Modification time in seconds since epoch
46
+ */
47
+ export function getFileMtime(filePath) {
48
+ try {
49
+ if (process.platform === 'darwin') {
50
+ const output = execSync(`stat -f %m "${filePath}"`, { encoding: 'utf-8' });
51
+ return parseInt(output.trim(), 10);
52
+ }
53
+ else if (process.platform === 'win32') {
54
+ const output = execSync(`powershell -Command "(Get-Item '${filePath}').LastWriteTime.ToFileTimeUtc()"`, { encoding: 'utf-8' });
55
+ const fileTime = parseInt(output.trim(), 10);
56
+ return Math.floor((fileTime - 116444736000000000) / 10000000);
57
+ }
58
+ else {
59
+ const output = execSync(`stat -c %Y "${filePath}"`, { encoding: 'utf-8' });
60
+ return parseInt(output.trim(), 10);
61
+ }
62
+ }
63
+ catch {
64
+ return 0;
65
+ }
66
+ }
67
+ /**
68
+ * Kills a process (cross-platform)
69
+ *
70
+ * @param pid - Process ID to kill
71
+ * @param signal - Signal to send (default: SIGTERM)
72
+ */
73
+ export function killProcess(pid, signal = 'SIGTERM') {
74
+ try {
75
+ if (process.platform === 'win32') {
76
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
77
+ }
78
+ else {
79
+ process.kill(pid, signal);
80
+ }
81
+ }
82
+ catch {
83
+ // Process may not exist, ignore
84
+ }
85
+ }
86
+ /**
87
+ * Acquires a file lock using atomic directory creation (cross-platform)
88
+ *
89
+ * @param lockPath - Path to lock directory
90
+ * @returns true if lock acquired
91
+ */
92
+ export function acquireFileLock(lockPath) {
93
+ const fs = require('fs');
94
+ try {
95
+ fs.mkdirSync(lockPath, { recursive: false });
96
+ return true;
97
+ }
98
+ catch (err) {
99
+ if (err.code === 'EEXIST') {
100
+ return false; // Lock already exists
101
+ }
102
+ throw err;
103
+ }
104
+ }
105
+ /**
106
+ * Enhanced cross-platform utility functions with async API and logger injection
107
+ */
108
+ export class PlatformUtils {
109
+ constructor(options = {}) {
110
+ this.platform = process.platform;
111
+ this.logger = options.logger ?? consoleLogger;
112
+ }
113
+ /**
114
+ * Get current platform
115
+ */
116
+ getPlatform() {
117
+ return this.platform;
118
+ }
119
+ /**
120
+ * Check if current platform is Windows
121
+ */
122
+ isWindows() {
123
+ return this.platform === 'win32';
124
+ }
125
+ /**
126
+ * Check if current platform is POSIX (macOS or Linux)
127
+ */
128
+ isPosix() {
129
+ return this.platform === 'darwin' || this.platform === 'linux';
130
+ }
131
+ /**
132
+ * Check if a process exists by PID (async)
133
+ *
134
+ * - macOS/Linux: kill -0 $pid (signal 0 = existence check)
135
+ * - Windows: tasklist /FI "PID eq $pid"
136
+ *
137
+ * @param pid Process ID to check
138
+ * @returns true if process exists, false otherwise
139
+ */
140
+ async checkProcessExistsAsync(pid) {
141
+ try {
142
+ if (this.isWindows()) {
143
+ const output = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
144
+ encoding: 'utf-8',
145
+ stdio: ['pipe', 'pipe', 'ignore'],
146
+ windowsHide: true
147
+ });
148
+ return output.includes(pid.toString());
149
+ }
150
+ else {
151
+ // Signal 0 = check existence without actually sending a signal
152
+ process.kill(pid, 0);
153
+ return true;
154
+ }
155
+ }
156
+ catch (err) {
157
+ if (err.code === 'ESRCH') {
158
+ // No such process
159
+ return false;
160
+ }
161
+ if (err.code === 'EPERM') {
162
+ // Process exists but we don't have permission to signal it
163
+ return true;
164
+ }
165
+ this.logger.error(`Error checking process ${pid}:`, err);
166
+ return false;
167
+ }
168
+ }
169
+ /**
170
+ * Get file modification time (async, uses fs.stat for cross-platform)
171
+ *
172
+ * @param path File path
173
+ * @returns Modification time in seconds since epoch
174
+ */
175
+ async getFileMtimeAsync(path) {
176
+ try {
177
+ const stats = await stat(path);
178
+ return Math.floor(stats.mtimeMs / 1000);
179
+ }
180
+ catch (err) {
181
+ this.logger.error(`Error getting mtime for ${path}:`, err);
182
+ throw err;
183
+ }
184
+ }
185
+ /**
186
+ * Acquire file lock using atomic mkdir (async)
187
+ *
188
+ * @param lockPath Path to lock directory
189
+ * @param timeoutMs Maximum time to wait for lock (default: 5000ms)
190
+ * @returns true if lock acquired, false if timeout
191
+ */
192
+ async acquireFileLockAsync(lockPath, timeoutMs = 5000) {
193
+ const startTime = Date.now();
194
+ while (Date.now() - startTime < timeoutMs) {
195
+ try {
196
+ await mkdir(lockPath, { recursive: false });
197
+ return true; // Lock acquired
198
+ }
199
+ catch (err) {
200
+ if (err.code === 'EEXIST') {
201
+ // Lock exists, wait and retry
202
+ await new Promise(resolve => setTimeout(resolve, 100));
203
+ continue;
204
+ }
205
+ this.logger.error(`Error acquiring lock at ${lockPath}:`, err);
206
+ throw err;
207
+ }
208
+ }
209
+ this.logger.warn(`Lock acquisition timeout after ${timeoutMs}ms: ${lockPath}`);
210
+ return false;
211
+ }
212
+ /**
213
+ * Release file lock (async)
214
+ *
215
+ * @param lockPath Path to lock directory
216
+ */
217
+ async releaseFileLockAsync(lockPath) {
218
+ try {
219
+ await rmdir(lockPath);
220
+ }
221
+ catch (err) {
222
+ if (err.code !== 'ENOENT') {
223
+ this.logger.error(`Error releasing lock at ${lockPath}:`, err);
224
+ throw err;
225
+ }
226
+ }
227
+ }
228
+ /**
229
+ * Kill process with signal (async)
230
+ *
231
+ * @param pid Process ID to kill
232
+ * @param signal Signal to send (default: SIGTERM)
233
+ */
234
+ async killProcessAsync(pid, signal = 'SIGTERM') {
235
+ try {
236
+ if (this.isWindows()) {
237
+ execSync(`taskkill /PID ${pid} /F`, {
238
+ stdio: 'ignore',
239
+ windowsHide: true
240
+ });
241
+ this.logger.debug(`Killed process ${pid} (Windows taskkill /F)`);
242
+ }
243
+ else {
244
+ process.kill(pid, signal);
245
+ this.logger.debug(`Killed process ${pid} with signal ${signal}`);
246
+ }
247
+ }
248
+ catch (err) {
249
+ if (err.code === 'ESRCH') {
250
+ this.logger.debug(`Process ${pid} doesn't exist (already terminated)`);
251
+ return;
252
+ }
253
+ this.logger.error(`Error killing process ${pid}:`, err);
254
+ throw err;
255
+ }
256
+ }
257
+ /**
258
+ * Kill process with graceful fallback (SIGTERM → SIGKILL)
259
+ *
260
+ * @param pid Process ID to kill
261
+ * @param gracePeriodMs Time to wait after SIGTERM before SIGKILL (default: 2000ms)
262
+ */
263
+ async killProcessGracefully(pid, gracePeriodMs = 2000) {
264
+ const exists = await this.checkProcessExistsAsync(pid);
265
+ if (!exists) {
266
+ this.logger.debug(`Process ${pid} doesn't exist, skipping kill`);
267
+ return;
268
+ }
269
+ try {
270
+ // Step 1: Send SIGTERM (graceful shutdown)
271
+ await this.killProcessAsync(pid, 'SIGTERM');
272
+ this.logger.debug(`Sent SIGTERM to process ${pid}, waiting ${gracePeriodMs}ms`);
273
+ // Step 2: Wait for grace period
274
+ await new Promise(resolve => setTimeout(resolve, gracePeriodMs));
275
+ // Step 3: Check if process still exists
276
+ const stillExists = await this.checkProcessExistsAsync(pid);
277
+ if (!stillExists) {
278
+ this.logger.debug(`Process ${pid} terminated gracefully after SIGTERM`);
279
+ return;
280
+ }
281
+ // Step 4: Force kill with SIGKILL
282
+ this.logger.warn(`Process ${pid} didn't terminate after SIGTERM, sending SIGKILL`);
283
+ await this.killProcessAsync(pid, 'SIGKILL');
284
+ this.logger.debug(`Sent SIGKILL to process ${pid}`);
285
+ }
286
+ catch (err) {
287
+ this.logger.error(`Error during graceful kill of process ${pid}:`, err);
288
+ throw err;
289
+ }
290
+ }
291
+ /**
292
+ * Send system notification
293
+ *
294
+ * - macOS: osascript (AppleScript)
295
+ * - Linux: notify-send (if available)
296
+ * - Windows: PowerShell toast notification
297
+ *
298
+ * Fails gracefully if notification system is unavailable
299
+ *
300
+ * @param title Notification title
301
+ * @param body Notification body text
302
+ */
303
+ async sendNotification(title, body) {
304
+ try {
305
+ if (this.platform === 'darwin') {
306
+ await this.sendNotificationMacOS(title, body);
307
+ }
308
+ else if (this.platform === 'linux') {
309
+ await this.sendNotificationLinux(title, body);
310
+ }
311
+ else if (this.platform === 'win32') {
312
+ await this.sendNotificationWindows(title, body);
313
+ }
314
+ this.logger.debug(`Sent notification: ${title}`);
315
+ }
316
+ catch (err) {
317
+ this.logger.warn(`Failed to send notification "${title}": ${err}`);
318
+ }
319
+ }
320
+ /**
321
+ * macOS notification using osascript
322
+ */
323
+ async sendNotificationMacOS(title, body) {
324
+ const escapedTitle = title.replace(/"/g, '\\"');
325
+ const escapedBody = body.replace(/"/g, '\\"');
326
+ const cmd = `osascript -e 'display notification "${escapedBody}" with title "${escapedTitle}"'`;
327
+ execSync(cmd, { stdio: 'ignore' });
328
+ }
329
+ /**
330
+ * Linux notification using notify-send
331
+ */
332
+ async sendNotificationLinux(title, body) {
333
+ try {
334
+ execSync('command -v notify-send', { stdio: 'ignore' });
335
+ const escapedTitle = title.replace(/"/g, '\\"');
336
+ const escapedBody = body.replace(/"/g, '\\"');
337
+ execSync(`notify-send "${escapedTitle}" "${escapedBody}"`, { stdio: 'ignore' });
338
+ }
339
+ catch (err) {
340
+ throw new Error('notify-send not available on this system');
341
+ }
342
+ }
343
+ /**
344
+ * Windows notification using PowerShell toast
345
+ */
346
+ async sendNotificationWindows(title, body) {
347
+ const escapedTitle = title.replace(/'/g, "''");
348
+ const escapedBody = body.replace(/'/g, "''");
349
+ const psScript = `
350
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;
351
+ $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);
352
+ $xml = [xml]$template.GetXml();
353
+ $xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${escapedTitle}')) | Out-Null;
354
+ $xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${escapedBody}')) | Out-Null;
355
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml);
356
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('SpecWeave').Show($toast);
357
+ `;
358
+ execSync(`powershell -Command "${psScript}"`, {
359
+ stdio: 'ignore',
360
+ windowsHide: true
361
+ });
362
+ }
363
+ }
364
+ // Export singleton instance for convenience
365
+ export const platformUtils = new PlatformUtils();
366
+ //# sourceMappingURL=platform-utils.js.map