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.
- package/CLAUDE.md +106 -1
- package/dist/src/cli/add-child-pid.d.ts +11 -0
- package/dist/src/cli/add-child-pid.d.ts.map +1 -0
- package/dist/src/cli/add-child-pid.js +42 -0
- package/dist/src/cli/add-child-pid.js.map +1 -0
- package/dist/src/cli/add-child-process.d.ts +15 -0
- package/dist/src/cli/add-child-process.d.ts.map +1 -0
- package/dist/src/cli/add-child-process.js +40 -0
- package/dist/src/cli/add-child-process.js.map +1 -0
- package/dist/src/cli/check-watchdog.d.ts +15 -0
- package/dist/src/cli/check-watchdog.d.ts.map +1 -0
- package/dist/src/cli/check-watchdog.js +47 -0
- package/dist/src/cli/check-watchdog.js.map +1 -0
- package/dist/src/cli/cleanup-zombies.d.ts +14 -0
- package/dist/src/cli/cleanup-zombies.d.ts.map +1 -0
- package/dist/src/cli/cleanup-zombies.js +268 -0
- package/dist/src/cli/cleanup-zombies.js.map +1 -0
- package/dist/src/cli/commands/jobs.js +9 -2
- package/dist/src/cli/commands/jobs.js.map +1 -1
- package/dist/src/cli/find-session-by-pid.d.ts +14 -0
- package/dist/src/cli/find-session-by-pid.d.ts.map +1 -0
- package/dist/src/cli/find-session-by-pid.js +45 -0
- package/dist/src/cli/find-session-by-pid.js.map +1 -0
- package/dist/src/cli/get-stale-sessions.d.ts +17 -0
- package/dist/src/cli/get-stale-sessions.d.ts.map +1 -0
- package/dist/src/cli/get-stale-sessions.js +36 -0
- package/dist/src/cli/get-stale-sessions.js.map +1 -0
- package/dist/src/cli/register-session.d.ts +16 -0
- package/dist/src/cli/register-session.d.ts.map +1 -0
- package/dist/src/cli/register-session.js +48 -0
- package/dist/src/cli/register-session.js.map +1 -0
- package/dist/src/cli/remove-session.d.ts +11 -0
- package/dist/src/cli/remove-session.d.ts.map +1 -0
- package/dist/src/cli/remove-session.js +36 -0
- package/dist/src/cli/remove-session.js.map +1 -0
- package/dist/src/cli/update-heartbeat.d.ts +11 -0
- package/dist/src/cli/update-heartbeat.d.ts.map +1 -0
- package/dist/src/cli/update-heartbeat.js +36 -0
- package/dist/src/cli/update-heartbeat.js.map +1 -0
- package/dist/src/cli/workers/living-docs-worker.js +6 -0
- package/dist/src/cli/workers/living-docs-worker.js.map +1 -1
- package/dist/src/config/types.d.ts +1208 -203
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/core/background/job-launcher.d.ts.map +1 -1
- package/dist/src/core/background/job-launcher.js +7 -4
- package/dist/src/core/background/job-launcher.js.map +1 -1
- package/dist/src/core/background/job-manager.d.ts +25 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.js +136 -15
- package/dist/src/core/background/job-manager.js.map +1 -1
- package/dist/src/core/increment/increment-utils.d.ts +26 -1
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +66 -4
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +3 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +5 -2
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +48 -12
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts +70 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js +188 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts +33 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js +290 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +114 -11
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts +23 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js +283 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts +44 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts +126 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js +378 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +57 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts +82 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js +430 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts +84 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js +387 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js +174 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.d.ts +3 -0
- package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.js +40 -1
- package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
- package/dist/src/core/qa/qa-runner.js +1 -1
- package/dist/src/core/qa/qa-runner.js.map +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-updater.js +4 -3
- package/dist/src/core/status-line/status-line-updater.js.map +1 -1
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +26 -10
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +140 -33
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +27 -30
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +34 -11
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +82 -15
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +93 -38
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +42 -4
- package/dist/src/init/team/types.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +1 -1
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/github-reconciler.js +1 -1
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.js +1 -1
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/types/session.d.ts +65 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +8 -0
- package/dist/src/types/session.js.map +1 -0
- package/dist/src/utils/lock-manager.d.ts +48 -0
- package/dist/src/utils/lock-manager.d.ts.map +1 -0
- package/dist/src/utils/lock-manager.js +195 -0
- package/dist/src/utils/lock-manager.js.map +1 -0
- package/dist/src/utils/notification-manager.d.ts +45 -0
- package/dist/src/utils/notification-manager.d.ts.map +1 -0
- package/dist/src/utils/notification-manager.js +130 -0
- package/dist/src/utils/notification-manager.js.map +1 -0
- package/dist/src/utils/platform-utils.d.ts +136 -0
- package/dist/src/utils/platform-utils.d.ts.map +1 -0
- package/dist/src/utils/platform-utils.js +366 -0
- package/dist/src/utils/platform-utils.js.map +1 -0
- package/dist/src/utils/session-registry.d.ts +142 -0
- package/dist/src/utils/session-registry.d.ts.map +1 -0
- package/dist/src/utils/session-registry.js +480 -0
- package/dist/src/utils/session-registry.js.map +1 -0
- package/package.json +5 -2
- package/plugins/specweave/commands/specweave-living-docs.md +42 -0
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
- package/plugins/specweave/hooks/lib/update-status-line.sh +1 -1
- package/plugins/specweave/hooks/post-increment-status-change.sh +3 -3
- package/plugins/specweave/hooks/post-metadata-change.sh +1 -1
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- package/plugins/specweave/hooks/user-prompt-submit.sh +2 -2
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +61 -0
- package/plugins/specweave/hooks/v2/session-end.sh +69 -0
- package/plugins/specweave/hooks/v2/session-start.sh +81 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/scripts/heartbeat.sh +110 -0
- package/plugins/specweave/scripts/progress.js +34 -4
- package/plugins/specweave/scripts/read-jobs.sh +1 -1
- package/plugins/specweave/scripts/read-progress.sh +50 -5
- package/plugins/specweave/scripts/read-workflow.sh +1 -1
- package/plugins/specweave/scripts/session-watchdog.sh +65 -0
- package/plugins/specweave/scripts/status.js +28 -11
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
- 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":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAU,aAAa,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,OAAO,mBAAmB;IAG9B,YAAY,UAA+B,EAAE;QAC3C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC;IAChD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,gBAAgB,CAAC,KAAa,EAAE,IAAY,EAAE,KAAc;QAChE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAElC,IAAI,CAAC;YACH,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACvD,CAAC;iBAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;gBAChC,MAAM,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAChD,CAAC;iBAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;gBAChC,MAAM,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAClD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,4CAA4C,QAAQ,EAAE,CAAC,CAAC;gBACzE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;YACvD,sBAAsB;YACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,qBAAqB,CACjC,KAAa,EACb,IAAY,EACZ,KAAc;QAEd,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,gBAAgB,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,MAAM,MAAM,GAAG,yBAAyB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;YAEzI,IAAI,CAAC,iBAAiB,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;gBACzC,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChB,CAAC;qBAAM,CAAC;oBACN,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,qBAAqB,CAAC,KAAa,EAAE,IAAY;QAC7D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,oCAAoC;YACpC,IAAI,CAAC,wBAAwB,EAAE,CAAC,KAAK,EAAE,EAAE;gBACvC,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;oBACjE,OAAO,EAAE,CAAC;oBACV,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,gBAAgB,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC3F,IAAI,KAAK,EAAE,CAAC;wBACV,MAAM,CAAC,KAAK,CAAC,CAAC;oBAChB,CAAC;yBAAM,CAAC;wBACN,OAAO,EAAE,CAAC;oBACZ,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,uBAAuB,CAAC,KAAa,EAAE,IAAY;QAC/D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,MAAM,GAAG;;;;;;0FAMqE,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC;0FAC/B,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC;;;;;;OAMjH,CAAC;YAEF,IAAI,CAAC,wBAAwB,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;gBAChD,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChB,CAAC;qBAAM,CAAC;oBACN,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,oBAAoB,CAAC,GAAW;QACtC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACzD,CAAC;IAED;;OAEG;IACK,cAAc,CAAC,GAAW;QAChC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC;IAED;;OAEG;IACK,mBAAmB,CAAC,GAAW;QACrC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;CACF"}
|
|
@@ -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
|