@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf
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/dist/resource-loader.d.ts +5 -0
- package/dist/resource-loader.js +24 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +9 -4
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +5 -4
- package/packages/contracts/package.json +2 -2
- package/packages/daemon/bin/gsd-daemon.js +14 -0
- package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
- package/packages/daemon/bin/gsd-mcp.js +14 -0
- package/packages/daemon/dist/channel-manager.d.ts +53 -0
- package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
- package/packages/daemon/dist/channel-manager.js +167 -0
- package/packages/daemon/dist/channel-manager.js.map +1 -0
- package/packages/daemon/dist/cli.d.ts +3 -0
- package/packages/daemon/dist/cli.d.ts.map +1 -0
- package/packages/daemon/dist/cli.js +94 -0
- package/packages/daemon/dist/cli.js.map +1 -0
- package/packages/daemon/dist/cloud-cli.d.ts +7 -0
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-cli.js +96 -0
- package/packages/daemon/dist/cloud-cli.js.map +1 -0
- package/packages/daemon/dist/cloud-config.d.ts +18 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.js +209 -0
- package/packages/daemon/dist/cloud-config.js.map +1 -0
- package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.test.js +132 -0
- package/packages/daemon/dist/cloud-config.test.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
- package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.js +180 -0
- package/packages/daemon/dist/cloud-runtime.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.js +28 -0
- package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
- package/packages/daemon/dist/cloud-token.d.ts +3 -0
- package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-token.js +37 -0
- package/packages/daemon/dist/cloud-token.js.map +1 -0
- package/packages/daemon/dist/commands.d.ts +25 -0
- package/packages/daemon/dist/commands.d.ts.map +1 -0
- package/packages/daemon/dist/commands.js +81 -0
- package/packages/daemon/dist/commands.js.map +1 -0
- package/packages/daemon/dist/config.d.ts +17 -0
- package/packages/daemon/dist/config.d.ts.map +1 -0
- package/packages/daemon/dist/config.js +146 -0
- package/packages/daemon/dist/config.js.map +1 -0
- package/packages/daemon/dist/daemon.d.ts +38 -0
- package/packages/daemon/dist/daemon.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.js +194 -0
- package/packages/daemon/dist/daemon.js.map +1 -0
- package/packages/daemon/dist/daemon.test.d.ts +2 -0
- package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.test.js +692 -0
- package/packages/daemon/dist/daemon.test.js.map +1 -0
- package/packages/daemon/dist/discord-bot.d.ts +70 -0
- package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.js +433 -0
- package/packages/daemon/dist/discord-bot.js.map +1 -0
- package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
- package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.test.js +667 -0
- package/packages/daemon/dist/discord-bot.test.js.map +1 -0
- package/packages/daemon/dist/event-bridge.d.ts +72 -0
- package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.js +366 -0
- package/packages/daemon/dist/event-bridge.js.map +1 -0
- package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
- package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.test.js +528 -0
- package/packages/daemon/dist/event-bridge.test.js.map +1 -0
- package/packages/daemon/dist/event-formatter.d.ts +34 -0
- package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.js +355 -0
- package/packages/daemon/dist/event-formatter.js.map +1 -0
- package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
- package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.test.js +333 -0
- package/packages/daemon/dist/event-formatter.test.js.map +1 -0
- package/packages/daemon/dist/index.d.ts +25 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js +17 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/launchd.d.ts +49 -0
- package/packages/daemon/dist/launchd.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.js +188 -0
- package/packages/daemon/dist/launchd.js.map +1 -0
- package/packages/daemon/dist/launchd.test.d.ts +2 -0
- package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.test.js +296 -0
- package/packages/daemon/dist/launchd.test.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
- package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.js +307 -0
- package/packages/daemon/dist/local-tool-executor.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.js +111 -0
- package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
- package/packages/daemon/dist/logger.d.ts +25 -0
- package/packages/daemon/dist/logger.d.ts.map +1 -0
- package/packages/daemon/dist/logger.js +72 -0
- package/packages/daemon/dist/logger.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.js +8 -0
- package/packages/daemon/dist/mcp-cli.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.js +13 -0
- package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
- package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
- package/packages/daemon/dist/message-batcher.d.ts +78 -0
- package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.js +173 -0
- package/packages/daemon/dist/message-batcher.js.map +1 -0
- package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
- package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.test.js +242 -0
- package/packages/daemon/dist/message-batcher.test.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +98 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js +359 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
- package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.test.js +425 -0
- package/packages/daemon/dist/orchestrator.test.js.map +1 -0
- package/packages/daemon/dist/project-scanner.d.ts +18 -0
- package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.js +90 -0
- package/packages/daemon/dist/project-scanner.js.map +1 -0
- package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
- package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.test.js +183 -0
- package/packages/daemon/dist/project-scanner.test.js.map +1 -0
- package/packages/daemon/dist/session-manager.d.ts +70 -0
- package/packages/daemon/dist/session-manager.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.js +358 -0
- package/packages/daemon/dist/session-manager.js.map +1 -0
- package/packages/daemon/dist/session-manager.test.d.ts +9 -0
- package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.test.js +616 -0
- package/packages/daemon/dist/session-manager.test.js.map +1 -0
- package/packages/daemon/dist/types.d.ts +133 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js +8 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/verbosity.d.ts +27 -0
- package/packages/daemon/dist/verbosity.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.js +86 -0
- package/packages/daemon/dist/verbosity.js.map +1 -0
- package/packages/daemon/dist/verbosity.test.d.ts +2 -0
- package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.test.js +136 -0
- package/packages/daemon/dist/verbosity.test.js.map +1 -0
- package/packages/daemon/package.json +9 -8
- package/packages/gsd-agent-core/package.json +6 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +8 -8
- package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
- package/packages/mcp-server/package.json +6 -5
- package/packages/native/package.json +3 -3
- package/packages/pi-agent-core/package.json +4 -4
- package/packages/pi-ai/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +0 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +18 -35
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +5 -4
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +9 -9
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +3 -3
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +4 -4
- package/scripts/install/deps.js +10 -0
- package/src/resources/extensions/gsd/auto/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { writeFileSync, unlinkSync, existsSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
// --------------- constants ---------------
|
|
7
|
+
const LABEL = 'com.gsd.daemon';
|
|
8
|
+
const PLIST_FILENAME = `${LABEL}.plist`;
|
|
9
|
+
// --------------- helpers ---------------
|
|
10
|
+
/** Escape special XML characters in a string. */
|
|
11
|
+
export function escapeXml(str) {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
/** Return the canonical plist path under ~/Library/LaunchAgents/. */
|
|
20
|
+
export function getPlistPath() {
|
|
21
|
+
return resolve(homedir(), 'Library', 'LaunchAgents', PLIST_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the NVM-aware PATH string.
|
|
25
|
+
* Includes the directory containing the Node binary so that launchd can find node
|
|
26
|
+
* even when launched outside a shell session (where NVM isn't sourced).
|
|
27
|
+
*/
|
|
28
|
+
function buildEnvPath(nodePath) {
|
|
29
|
+
const nodeBinDir = dirname(nodePath);
|
|
30
|
+
// Keep system essentials and prepend the node binary's directory
|
|
31
|
+
return `${nodeBinDir}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
32
|
+
}
|
|
33
|
+
// --------------- plist generation ---------------
|
|
34
|
+
/** Generate valid launchd plist XML for the GSD daemon. */
|
|
35
|
+
export function generatePlist(opts) {
|
|
36
|
+
const home = homedir();
|
|
37
|
+
const workDir = opts.workingDirectory ?? home;
|
|
38
|
+
const stdoutPath = opts.stdoutPath ?? resolve(home, '.gsd', 'daemon-stdout.log');
|
|
39
|
+
const stderrPath = opts.stderrPath ?? resolve(home, '.gsd', 'daemon-stderr.log');
|
|
40
|
+
const envPath = buildEnvPath(opts.nodePath);
|
|
41
|
+
// Forward ANTHROPIC_API_KEY so the orchestrator LLM can authenticate.
|
|
42
|
+
// Captured at install time from the current process environment.
|
|
43
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
44
|
+
const anthropicKeyXml = anthropicKey
|
|
45
|
+
? `\n\t\t<key>ANTHROPIC_API_KEY</key>\n\t\t<string>${escapeXml(anthropicKey)}</string>`
|
|
46
|
+
: '';
|
|
47
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
48
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
49
|
+
<plist version="1.0">
|
|
50
|
+
<dict>
|
|
51
|
+
\t<key>Label</key>
|
|
52
|
+
\t<string>${escapeXml(LABEL)}</string>
|
|
53
|
+
|
|
54
|
+
\t<key>ProgramArguments</key>
|
|
55
|
+
\t<array>
|
|
56
|
+
\t\t<string>${escapeXml(opts.nodePath)}</string>
|
|
57
|
+
\t\t<string>${escapeXml(opts.scriptPath)}</string>
|
|
58
|
+
\t\t<string>--config</string>
|
|
59
|
+
\t\t<string>${escapeXml(opts.configPath)}</string>
|
|
60
|
+
\t</array>
|
|
61
|
+
|
|
62
|
+
\t<key>KeepAlive</key>
|
|
63
|
+
\t<dict>
|
|
64
|
+
\t\t<key>SuccessfulExit</key>
|
|
65
|
+
\t\t<false/>
|
|
66
|
+
\t</dict>
|
|
67
|
+
|
|
68
|
+
\t<key>RunAtLoad</key>
|
|
69
|
+
\t<true/>
|
|
70
|
+
|
|
71
|
+
\t<key>EnvironmentVariables</key>
|
|
72
|
+
\t<dict>
|
|
73
|
+
\t\t<key>PATH</key>
|
|
74
|
+
\t\t<string>${escapeXml(envPath)}</string>
|
|
75
|
+
\t\t<key>HOME</key>
|
|
76
|
+
\t\t<string>${escapeXml(home)}</string>${anthropicKeyXml}
|
|
77
|
+
\t</dict>
|
|
78
|
+
|
|
79
|
+
\t<key>WorkingDirectory</key>
|
|
80
|
+
\t<string>${escapeXml(workDir)}</string>
|
|
81
|
+
|
|
82
|
+
\t<key>StandardOutPath</key>
|
|
83
|
+
\t<string>${escapeXml(stdoutPath)}</string>
|
|
84
|
+
|
|
85
|
+
\t<key>StandardErrorPath</key>
|
|
86
|
+
\t<string>${escapeXml(stderrPath)}</string>
|
|
87
|
+
</dict>
|
|
88
|
+
</plist>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
// --------------- install / uninstall / status ---------------
|
|
92
|
+
/** Default runCommand using execSync. */
|
|
93
|
+
function defaultRunCommand(cmd) {
|
|
94
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Install the launchd agent: write plist and load it.
|
|
98
|
+
* Idempotent — unloads first if already loaded.
|
|
99
|
+
*/
|
|
100
|
+
export function install(opts, runCommand = defaultRunCommand) {
|
|
101
|
+
const plistPath = getPlistPath();
|
|
102
|
+
const xml = generatePlist(opts);
|
|
103
|
+
// Unload first if already present (ignore errors)
|
|
104
|
+
if (existsSync(plistPath)) {
|
|
105
|
+
try {
|
|
106
|
+
runCommand(`launchctl unload ${plistPath}`);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// already unloaded — fine
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
writeFileSync(plistPath, xml, 'utf-8');
|
|
113
|
+
chmodSync(plistPath, 0o644);
|
|
114
|
+
runCommand(`launchctl load ${plistPath}`);
|
|
115
|
+
// Verify it loaded
|
|
116
|
+
try {
|
|
117
|
+
runCommand(`launchctl list ${LABEL}`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Plist was written to ${plistPath} and launchctl load succeeded, but launchctl list ${LABEL} failed. The agent may not have started.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Uninstall the launchd agent: unload and remove plist.
|
|
125
|
+
* Graceful — does not throw if already uninstalled.
|
|
126
|
+
*/
|
|
127
|
+
export function uninstall(runCommand = defaultRunCommand) {
|
|
128
|
+
const plistPath = getPlistPath();
|
|
129
|
+
if (existsSync(plistPath)) {
|
|
130
|
+
try {
|
|
131
|
+
runCommand(`launchctl unload ${plistPath}`);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// already unloaded — that's fine
|
|
135
|
+
}
|
|
136
|
+
unlinkSync(plistPath);
|
|
137
|
+
}
|
|
138
|
+
// If plist doesn't exist, nothing to do — already uninstalled
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Query launchd for the daemon's status.
|
|
142
|
+
* Returns structured information about registration, PID, and last exit code.
|
|
143
|
+
*
|
|
144
|
+
* Handles two launchctl output formats:
|
|
145
|
+
* 1. Tabular: "PID\tStatus\tLabel" (older macOS)
|
|
146
|
+
* 2. JSON-style dict: `"PID" = 1234;` / `"LastExitStatus" = 0;` (newer macOS)
|
|
147
|
+
*/
|
|
148
|
+
export function status(runCommand = defaultRunCommand) {
|
|
149
|
+
try {
|
|
150
|
+
const output = runCommand(`launchctl list ${LABEL}`);
|
|
151
|
+
// --- Try tabular format first ---
|
|
152
|
+
const lines = output.trim().split('\n');
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const parts = line.trim().split(/\t+/);
|
|
155
|
+
if (parts.length >= 3 && parts[2] === LABEL) {
|
|
156
|
+
const pidStr = parts[0];
|
|
157
|
+
const statusStr = parts[1];
|
|
158
|
+
const pid = pidStr === '-' ? null : parseInt(pidStr, 10);
|
|
159
|
+
const lastExitStatus = statusStr != null ? parseInt(statusStr, 10) : null;
|
|
160
|
+
return {
|
|
161
|
+
registered: true,
|
|
162
|
+
pid: Number.isNaN(pid) ? null : pid,
|
|
163
|
+
lastExitStatus: Number.isNaN(lastExitStatus) ? null : lastExitStatus,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// --- Try JSON-style dict format ---
|
|
168
|
+
// Matches: "PID" = 1234; or "LastExitStatus" = 0;
|
|
169
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)\s*;/);
|
|
170
|
+
const exitMatch = output.match(/"LastExitStatus"\s*=\s*(\d+)\s*;/);
|
|
171
|
+
if (pidMatch || exitMatch) {
|
|
172
|
+
const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
|
|
173
|
+
const lastExitStatus = exitMatch ? parseInt(exitMatch[1], 10) : null;
|
|
174
|
+
return {
|
|
175
|
+
registered: true,
|
|
176
|
+
pid: Number.isNaN(pid) ? null : pid,
|
|
177
|
+
lastExitStatus: Number.isNaN(lastExitStatus) ? null : lastExitStatus,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Label resolved (no error) but no parseable output — still registered
|
|
181
|
+
return { registered: true, pid: null, lastExitStatus: null };
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// launchctl list exits non-zero when the label isn't found
|
|
185
|
+
return { registered: false, pid: null, lastExitStatus: null };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=launchd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchd.js","sourceRoot":"","sources":["../src/launchd.ts"],"names":[],"mappings":"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","sourcesContent":["import { writeFileSync, unlinkSync, existsSync, chmodSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\n// --------------- types ---------------\n\nexport interface PlistOptions {\n /** Absolute path to the Node.js binary */\n nodePath: string;\n /** Absolute path to the daemon script (cli.js) */\n scriptPath: string;\n /** Absolute path to the config file */\n configPath: string;\n /** Directory to use as WorkingDirectory in the plist (defaults to homedir) */\n workingDirectory?: string;\n /** Override stdout log path */\n stdoutPath?: string;\n /** Override stderr log path */\n stderrPath?: string;\n}\n\nexport interface LaunchdStatus {\n /** Whether the daemon is registered with launchd */\n registered: boolean;\n /** PID if currently running, null otherwise */\n pid: number | null;\n /** Last exit status code, null if never exited or not available */\n lastExitStatus: number | null;\n}\n\nexport type RunCommandFn = (cmd: string) => string;\n\n// --------------- constants ---------------\n\nconst LABEL = 'com.gsd.daemon';\nconst PLIST_FILENAME = `${LABEL}.plist`;\n\n// --------------- helpers ---------------\n\n/** Escape special XML characters in a string. */\nexport function escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/** Return the canonical plist path under ~/Library/LaunchAgents/. */\nexport function getPlistPath(): string {\n return resolve(homedir(), 'Library', 'LaunchAgents', PLIST_FILENAME);\n}\n\n/**\n * Build the NVM-aware PATH string.\n * Includes the directory containing the Node binary so that launchd can find node\n * even when launched outside a shell session (where NVM isn't sourced).\n */\nfunction buildEnvPath(nodePath: string): string {\n const nodeBinDir = dirname(nodePath);\n // Keep system essentials and prepend the node binary's directory\n return `${nodeBinDir}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;\n}\n\n// --------------- plist generation ---------------\n\n/** Generate valid launchd plist XML for the GSD daemon. */\nexport function generatePlist(opts: PlistOptions): string {\n const home = homedir();\n const workDir = opts.workingDirectory ?? home;\n const stdoutPath = opts.stdoutPath ?? resolve(home, '.gsd', 'daemon-stdout.log');\n const stderrPath = opts.stderrPath ?? resolve(home, '.gsd', 'daemon-stderr.log');\n const envPath = buildEnvPath(opts.nodePath);\n\n // Forward ANTHROPIC_API_KEY so the orchestrator LLM can authenticate.\n // Captured at install time from the current process environment.\n const anthropicKey = process.env.ANTHROPIC_API_KEY;\n const anthropicKeyXml = anthropicKey\n ? `\\n\\t\\t<key>ANTHROPIC_API_KEY</key>\\n\\t\\t<string>${escapeXml(anthropicKey)}</string>`\n : '';\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\\t<key>Label</key>\n\\t<string>${escapeXml(LABEL)}</string>\n\n\\t<key>ProgramArguments</key>\n\\t<array>\n\\t\\t<string>${escapeXml(opts.nodePath)}</string>\n\\t\\t<string>${escapeXml(opts.scriptPath)}</string>\n\\t\\t<string>--config</string>\n\\t\\t<string>${escapeXml(opts.configPath)}</string>\n\\t</array>\n\n\\t<key>KeepAlive</key>\n\\t<dict>\n\\t\\t<key>SuccessfulExit</key>\n\\t\\t<false/>\n\\t</dict>\n\n\\t<key>RunAtLoad</key>\n\\t<true/>\n\n\\t<key>EnvironmentVariables</key>\n\\t<dict>\n\\t\\t<key>PATH</key>\n\\t\\t<string>${escapeXml(envPath)}</string>\n\\t\\t<key>HOME</key>\n\\t\\t<string>${escapeXml(home)}</string>${anthropicKeyXml}\n\\t</dict>\n\n\\t<key>WorkingDirectory</key>\n\\t<string>${escapeXml(workDir)}</string>\n\n\\t<key>StandardOutPath</key>\n\\t<string>${escapeXml(stdoutPath)}</string>\n\n\\t<key>StandardErrorPath</key>\n\\t<string>${escapeXml(stderrPath)}</string>\n</dict>\n</plist>\n`;\n}\n\n// --------------- install / uninstall / status ---------------\n\n/** Default runCommand using execSync. */\nfunction defaultRunCommand(cmd: string): string {\n return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });\n}\n\n/**\n * Install the launchd agent: write plist and load it.\n * Idempotent — unloads first if already loaded.\n */\nexport function install(\n opts: PlistOptions,\n runCommand: RunCommandFn = defaultRunCommand,\n): void {\n const plistPath = getPlistPath();\n const xml = generatePlist(opts);\n\n // Unload first if already present (ignore errors)\n if (existsSync(plistPath)) {\n try {\n runCommand(`launchctl unload ${plistPath}`);\n } catch {\n // already unloaded — fine\n }\n }\n\n writeFileSync(plistPath, xml, 'utf-8');\n chmodSync(plistPath, 0o644);\n\n runCommand(`launchctl load ${plistPath}`);\n\n // Verify it loaded\n try {\n runCommand(`launchctl list ${LABEL}`);\n } catch {\n throw new Error(\n `Plist was written to ${plistPath} and launchctl load succeeded, but launchctl list ${LABEL} failed. The agent may not have started.`,\n );\n }\n}\n\n/**\n * Uninstall the launchd agent: unload and remove plist.\n * Graceful — does not throw if already uninstalled.\n */\nexport function uninstall(runCommand: RunCommandFn = defaultRunCommand): void {\n const plistPath = getPlistPath();\n\n if (existsSync(plistPath)) {\n try {\n runCommand(`launchctl unload ${plistPath}`);\n } catch {\n // already unloaded — that's fine\n }\n unlinkSync(plistPath);\n }\n // If plist doesn't exist, nothing to do — already uninstalled\n}\n\n/**\n * Query launchd for the daemon's status.\n * Returns structured information about registration, PID, and last exit code.\n *\n * Handles two launchctl output formats:\n * 1. Tabular: \"PID\\tStatus\\tLabel\" (older macOS)\n * 2. JSON-style dict: `\"PID\" = 1234;` / `\"LastExitStatus\" = 0;` (newer macOS)\n */\nexport function status(runCommand: RunCommandFn = defaultRunCommand): LaunchdStatus {\n try {\n const output = runCommand(`launchctl list ${LABEL}`);\n\n // --- Try tabular format first ---\n const lines = output.trim().split('\\n');\n for (const line of lines) {\n const parts = line.trim().split(/\\t+/);\n if (parts.length >= 3 && parts[2] === LABEL) {\n const pidStr = parts[0];\n const statusStr = parts[1];\n\n const pid = pidStr === '-' ? null : parseInt(pidStr, 10);\n const lastExitStatus = statusStr != null ? parseInt(statusStr, 10) : null;\n\n return {\n registered: true,\n pid: Number.isNaN(pid!) ? null : pid,\n lastExitStatus: Number.isNaN(lastExitStatus!) ? null : lastExitStatus,\n };\n }\n }\n\n // --- Try JSON-style dict format ---\n // Matches: \"PID\" = 1234; or \"LastExitStatus\" = 0;\n const pidMatch = output.match(/\"PID\"\\s*=\\s*(\\d+)\\s*;/);\n const exitMatch = output.match(/\"LastExitStatus\"\\s*=\\s*(\\d+)\\s*;/);\n\n if (pidMatch || exitMatch) {\n const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;\n const lastExitStatus = exitMatch ? parseInt(exitMatch[1], 10) : null;\n return {\n registered: true,\n pid: Number.isNaN(pid!) ? null : pid,\n lastExitStatus: Number.isNaN(lastExitStatus!) ? null : lastExitStatus,\n };\n }\n\n // Label resolved (no error) but no parseable output — still registered\n return { registered: true, pid: null, lastExitStatus: null };\n } catch {\n // launchctl list exits non-zero when the label isn't found\n return { registered: false, pid: null, lastExitStatus: null };\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchd.test.d.ts","sourceRoot":"","sources":["../src/launchd.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir, homedir } from 'node:os';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { escapeXml, generatePlist, getPlistPath, install, uninstall, status, } from './launchd.js';
|
|
8
|
+
// ---------- helpers ----------
|
|
9
|
+
function tmpDir() {
|
|
10
|
+
return mkdtempSync(join(tmpdir(), `launchd-test-${randomUUID().slice(0, 8)}-`));
|
|
11
|
+
}
|
|
12
|
+
const cleanupDirs = [];
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
while (cleanupDirs.length) {
|
|
15
|
+
const d = cleanupDirs.pop();
|
|
16
|
+
if (existsSync(d))
|
|
17
|
+
rmSync(d, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
function basePlistOpts(overrides) {
|
|
21
|
+
return {
|
|
22
|
+
nodePath: '/usr/local/bin/node',
|
|
23
|
+
scriptPath: '/usr/local/lib/gsd-daemon/dist/cli.js',
|
|
24
|
+
configPath: join(homedir(), '.gsd', 'daemon.yaml'),
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ---------- escapeXml ----------
|
|
29
|
+
describe('escapeXml', () => {
|
|
30
|
+
it('escapes & < > " \'', () => {
|
|
31
|
+
assert.equal(escapeXml('a&b<c>d"e\'f'), 'a&b<c>d"e'f');
|
|
32
|
+
});
|
|
33
|
+
it('leaves plain strings untouched', () => {
|
|
34
|
+
assert.equal(escapeXml('/usr/local/bin/node'), '/usr/local/bin/node');
|
|
35
|
+
});
|
|
36
|
+
it('escapes paths with spaces and special chars', () => {
|
|
37
|
+
const input = '/Users/John & Jane/my "project"/file.js';
|
|
38
|
+
const output = escapeXml(input);
|
|
39
|
+
assert.ok(output.includes('&'));
|
|
40
|
+
assert.ok(output.includes('"'));
|
|
41
|
+
// Verify no raw unescaped & remain (all & are part of & < etc.)
|
|
42
|
+
assert.equal(output, '/Users/John & Jane/my "project"/file.js');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// ---------- generatePlist ----------
|
|
46
|
+
describe('generatePlist', () => {
|
|
47
|
+
it('produces valid XML with plist header', () => {
|
|
48
|
+
const xml = generatePlist(basePlistOpts());
|
|
49
|
+
assert.ok(xml.startsWith('<?xml version="1.0"'));
|
|
50
|
+
assert.ok(xml.includes('<!DOCTYPE plist'));
|
|
51
|
+
assert.ok(xml.includes('<plist version="1.0">'));
|
|
52
|
+
assert.ok(xml.includes('</plist>'));
|
|
53
|
+
});
|
|
54
|
+
it('includes label com.gsd.daemon', () => {
|
|
55
|
+
const xml = generatePlist(basePlistOpts());
|
|
56
|
+
assert.ok(xml.includes('<string>com.gsd.daemon</string>'));
|
|
57
|
+
});
|
|
58
|
+
it('uses the absolute node path from opts', () => {
|
|
59
|
+
const opts = basePlistOpts({ nodePath: '/home/user/.nvm/versions/node/v22.0.0/bin/node' });
|
|
60
|
+
const xml = generatePlist(opts);
|
|
61
|
+
assert.ok(xml.includes('<string>/home/user/.nvm/versions/node/v22.0.0/bin/node</string>'));
|
|
62
|
+
});
|
|
63
|
+
it('includes NVM bin directory in PATH', () => {
|
|
64
|
+
const opts = basePlistOpts({ nodePath: '/home/user/.nvm/versions/node/v22.0.0/bin/node' });
|
|
65
|
+
const xml = generatePlist(opts);
|
|
66
|
+
assert.ok(xml.includes('/home/user/.nvm/versions/node/v22.0.0/bin'));
|
|
67
|
+
});
|
|
68
|
+
it('sets KeepAlive with SuccessfulExit false', () => {
|
|
69
|
+
const xml = generatePlist(basePlistOpts());
|
|
70
|
+
assert.ok(xml.includes('<key>KeepAlive</key>'));
|
|
71
|
+
assert.ok(xml.includes('<key>SuccessfulExit</key>'));
|
|
72
|
+
assert.ok(xml.includes('<false/>'));
|
|
73
|
+
});
|
|
74
|
+
it('sets RunAtLoad true', () => {
|
|
75
|
+
const xml = generatePlist(basePlistOpts());
|
|
76
|
+
assert.ok(xml.includes('<key>RunAtLoad</key>'));
|
|
77
|
+
assert.ok(xml.includes('<true/>'));
|
|
78
|
+
});
|
|
79
|
+
it('includes --config with the config path', () => {
|
|
80
|
+
const configPath = '/custom/path/daemon.yaml';
|
|
81
|
+
const xml = generatePlist(basePlistOpts({ configPath }));
|
|
82
|
+
assert.ok(xml.includes('<string>--config</string>'));
|
|
83
|
+
assert.ok(xml.includes(`<string>${configPath}</string>`));
|
|
84
|
+
});
|
|
85
|
+
it('includes HOME environment variable', () => {
|
|
86
|
+
const xml = generatePlist(basePlistOpts());
|
|
87
|
+
assert.ok(xml.includes('<key>HOME</key>'));
|
|
88
|
+
assert.ok(xml.includes(`<string>${homedir()}</string>`));
|
|
89
|
+
});
|
|
90
|
+
it('includes StandardOutPath and StandardErrorPath', () => {
|
|
91
|
+
const xml = generatePlist(basePlistOpts());
|
|
92
|
+
assert.ok(xml.includes('<key>StandardOutPath</key>'));
|
|
93
|
+
assert.ok(xml.includes('<key>StandardErrorPath</key>'));
|
|
94
|
+
});
|
|
95
|
+
it('escapes special characters in paths', () => {
|
|
96
|
+
const opts = basePlistOpts({
|
|
97
|
+
configPath: '/Users/John & Jane/config.yaml',
|
|
98
|
+
});
|
|
99
|
+
const xml = generatePlist(opts);
|
|
100
|
+
assert.ok(xml.includes('John & Jane'));
|
|
101
|
+
assert.ok(!xml.includes('John & Jane'));
|
|
102
|
+
});
|
|
103
|
+
it('uses custom stdout/stderr paths when provided', () => {
|
|
104
|
+
const opts = basePlistOpts({
|
|
105
|
+
stdoutPath: '/tmp/my-stdout.log',
|
|
106
|
+
stderrPath: '/tmp/my-stderr.log',
|
|
107
|
+
});
|
|
108
|
+
const xml = generatePlist(opts);
|
|
109
|
+
assert.ok(xml.includes('<string>/tmp/my-stdout.log</string>'));
|
|
110
|
+
assert.ok(xml.includes('<string>/tmp/my-stderr.log</string>'));
|
|
111
|
+
});
|
|
112
|
+
it('uses custom working directory when provided', () => {
|
|
113
|
+
const opts = basePlistOpts({
|
|
114
|
+
workingDirectory: '/custom/work/dir',
|
|
115
|
+
});
|
|
116
|
+
const xml = generatePlist(opts);
|
|
117
|
+
assert.ok(xml.includes('<string>/custom/work/dir</string>'));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// ---------- getPlistPath ----------
|
|
121
|
+
describe('getPlistPath', () => {
|
|
122
|
+
it('returns ~/Library/LaunchAgents/com.gsd.daemon.plist', () => {
|
|
123
|
+
const expected = join(homedir(), 'Library', 'LaunchAgents', 'com.gsd.daemon.plist');
|
|
124
|
+
assert.equal(getPlistPath(), expected);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ---------- install ----------
|
|
128
|
+
describe('install', () => {
|
|
129
|
+
let tmp;
|
|
130
|
+
let fakePlistPath;
|
|
131
|
+
// We can't mock getPlistPath directly, but we can verify the commands
|
|
132
|
+
// issued and the plist content by intercepting runCommand and filesystem ops.
|
|
133
|
+
// For filesystem testing, we test the functions that call writeFileSync indirectly
|
|
134
|
+
// by verifying the runCommand calls and returned values.
|
|
135
|
+
it('calls launchctl load with the plist path', () => {
|
|
136
|
+
const calls = [];
|
|
137
|
+
const mockRun = (cmd) => {
|
|
138
|
+
calls.push(cmd);
|
|
139
|
+
return '';
|
|
140
|
+
};
|
|
141
|
+
// install will try to write to the real plist path, so we need to be careful.
|
|
142
|
+
// We test the command flow by catching the writeFileSync error (dir may not exist in CI)
|
|
143
|
+
// or by letting it proceed in local dev.
|
|
144
|
+
try {
|
|
145
|
+
install(basePlistOpts(), mockRun);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// writeFileSync may fail if ~/Library/LaunchAgents doesn't exist in test env
|
|
149
|
+
}
|
|
150
|
+
const loadCalls = calls.filter(c => c.startsWith('launchctl load'));
|
|
151
|
+
const listCalls = calls.filter(c => c.startsWith('launchctl list'));
|
|
152
|
+
// Should have at least attempted launchctl load
|
|
153
|
+
assert.ok(loadCalls.length > 0 || calls.length > 0, 'Expected launchctl commands to be called');
|
|
154
|
+
});
|
|
155
|
+
it('generates valid plist content when called', () => {
|
|
156
|
+
// Test that the plist content would be correct by testing generatePlist
|
|
157
|
+
// (install is a thin wrapper around generatePlist + writeFile + launchctl)
|
|
158
|
+
const xml = generatePlist(basePlistOpts());
|
|
159
|
+
assert.ok(xml.includes('<key>Label</key>'));
|
|
160
|
+
assert.ok(xml.includes('<string>com.gsd.daemon</string>'));
|
|
161
|
+
});
|
|
162
|
+
it('handles idempotent install (unloads first if plist exists)', () => {
|
|
163
|
+
const calls = [];
|
|
164
|
+
const mockRun = (cmd) => {
|
|
165
|
+
calls.push(cmd);
|
|
166
|
+
return '';
|
|
167
|
+
};
|
|
168
|
+
// To simulate idempotent install, we need an existing plist file.
|
|
169
|
+
// Since install writes to getPlistPath(), we test the command sequence.
|
|
170
|
+
try {
|
|
171
|
+
install(basePlistOpts(), mockRun);
|
|
172
|
+
// Second install
|
|
173
|
+
install(basePlistOpts(), mockRun);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// filesystem may not be writable
|
|
177
|
+
}
|
|
178
|
+
// The second install should have tried to unload first
|
|
179
|
+
const unloadCalls = calls.filter(c => c.startsWith('launchctl unload'));
|
|
180
|
+
// If the plist path exists, we expect at least one unload attempt on second call
|
|
181
|
+
// This is a command-level check; filesystem existence depends on environment
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// ---------- uninstall ----------
|
|
185
|
+
describe('uninstall', () => {
|
|
186
|
+
it('calls launchctl unload when plist would exist', () => {
|
|
187
|
+
const calls = [];
|
|
188
|
+
const mockRun = (cmd) => {
|
|
189
|
+
calls.push(cmd);
|
|
190
|
+
return '';
|
|
191
|
+
};
|
|
192
|
+
// uninstall checks existsSync(plistPath) — if plist doesn't exist, it's a no-op
|
|
193
|
+
uninstall(mockRun);
|
|
194
|
+
// If plist doesn't exist in test environment, calls should be empty (graceful)
|
|
195
|
+
// That's the "handles missing plist gracefully" case
|
|
196
|
+
});
|
|
197
|
+
it('handles missing plist gracefully (no-op)', () => {
|
|
198
|
+
const calls = [];
|
|
199
|
+
const mockRun = (cmd) => {
|
|
200
|
+
calls.push(cmd);
|
|
201
|
+
return '';
|
|
202
|
+
};
|
|
203
|
+
// Shouldn't throw even if plist doesn't exist
|
|
204
|
+
assert.doesNotThrow(() => uninstall(mockRun));
|
|
205
|
+
});
|
|
206
|
+
it('handles already-unloaded agent gracefully', () => {
|
|
207
|
+
const mockRun = (cmd) => {
|
|
208
|
+
if (cmd.includes('launchctl unload')) {
|
|
209
|
+
throw new Error('Could not find specified service');
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
};
|
|
213
|
+
// Should not throw even if launchctl unload fails
|
|
214
|
+
assert.doesNotThrow(() => uninstall(mockRun));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
// ---------- status ----------
|
|
218
|
+
describe('status', () => {
|
|
219
|
+
it('parses running daemon output (PID present)', () => {
|
|
220
|
+
const mockRun = (_cmd) => {
|
|
221
|
+
return '{\n\t"PID" = 1234;\n\t"Label" = "com.gsd.daemon";\n}\nPID\tStatus\tLabel\n1234\t0\tcom.gsd.daemon\n';
|
|
222
|
+
};
|
|
223
|
+
const result = status(mockRun);
|
|
224
|
+
assert.equal(result.registered, true);
|
|
225
|
+
assert.equal(result.pid, 1234);
|
|
226
|
+
assert.equal(result.lastExitStatus, 0);
|
|
227
|
+
});
|
|
228
|
+
it('parses stopped daemon output (no PID)', () => {
|
|
229
|
+
const mockRun = (_cmd) => {
|
|
230
|
+
return 'PID\tStatus\tLabel\n-\t78\tcom.gsd.daemon\n';
|
|
231
|
+
};
|
|
232
|
+
const result = status(mockRun);
|
|
233
|
+
assert.equal(result.registered, true);
|
|
234
|
+
assert.equal(result.pid, null);
|
|
235
|
+
assert.equal(result.lastExitStatus, 78);
|
|
236
|
+
});
|
|
237
|
+
it('returns not-registered when launchctl list fails', () => {
|
|
238
|
+
const mockRun = (_cmd) => {
|
|
239
|
+
throw new Error('Could not find service "com.gsd.daemon" in domain for port');
|
|
240
|
+
};
|
|
241
|
+
const result = status(mockRun);
|
|
242
|
+
assert.equal(result.registered, false);
|
|
243
|
+
assert.equal(result.pid, null);
|
|
244
|
+
assert.equal(result.lastExitStatus, null);
|
|
245
|
+
});
|
|
246
|
+
it('returns structured result with all fields', () => {
|
|
247
|
+
const mockRun = (_cmd) => {
|
|
248
|
+
return 'PID\tStatus\tLabel\n5678\t0\tcom.gsd.daemon\n';
|
|
249
|
+
};
|
|
250
|
+
const result = status(mockRun);
|
|
251
|
+
assert.ok('registered' in result);
|
|
252
|
+
assert.ok('pid' in result);
|
|
253
|
+
assert.ok('lastExitStatus' in result);
|
|
254
|
+
});
|
|
255
|
+
it('parses JSON-style dict output (newer macOS)', () => {
|
|
256
|
+
const mockRun = (_cmd) => {
|
|
257
|
+
return `{
|
|
258
|
+
\t"StandardOutPath" = "/Users/me/.gsd/daemon-stdout.log";
|
|
259
|
+
\t"LimitLoadToSessionType" = "Aqua";
|
|
260
|
+
\t"StandardErrorPath" = "/Users/me/.gsd/daemon-stderr.log";
|
|
261
|
+
\t"Label" = "com.gsd.daemon";
|
|
262
|
+
\t"OnDemand" = true;
|
|
263
|
+
\t"LastExitStatus" = 0;
|
|
264
|
+
\t"PID" = 23802;
|
|
265
|
+
\t"Program" = "/usr/local/bin/node";
|
|
266
|
+
};`;
|
|
267
|
+
};
|
|
268
|
+
const result = status(mockRun);
|
|
269
|
+
assert.equal(result.registered, true);
|
|
270
|
+
assert.equal(result.pid, 23802);
|
|
271
|
+
assert.equal(result.lastExitStatus, 0);
|
|
272
|
+
});
|
|
273
|
+
it('parses JSON-style dict output when daemon stopped (no PID key)', () => {
|
|
274
|
+
const mockRun = (_cmd) => {
|
|
275
|
+
return `{
|
|
276
|
+
\t"Label" = "com.gsd.daemon";
|
|
277
|
+
\t"LastExitStatus" = 1;
|
|
278
|
+
\t"OnDemand" = true;
|
|
279
|
+
};`;
|
|
280
|
+
};
|
|
281
|
+
const result = status(mockRun);
|
|
282
|
+
assert.equal(result.registered, true);
|
|
283
|
+
assert.equal(result.pid, null);
|
|
284
|
+
assert.equal(result.lastExitStatus, 1);
|
|
285
|
+
});
|
|
286
|
+
it('handles unexpected output format gracefully', () => {
|
|
287
|
+
const mockRun = (_cmd) => {
|
|
288
|
+
return 'some unexpected output without the label';
|
|
289
|
+
};
|
|
290
|
+
// Should not throw — should return registered:true but with null fields
|
|
291
|
+
// since the command succeeded (label was found) but output didn't match
|
|
292
|
+
const result = status(mockRun);
|
|
293
|
+
assert.equal(result.registered, true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
//# sourceMappingURL=launchd.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchd.test.js","sourceRoot":"","sources":["../src/launchd.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, beforeEach, afterEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { mkdtempSync, existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, statSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { tmpdir, homedir } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport {\n escapeXml,\n generatePlist,\n getPlistPath,\n install,\n uninstall,\n status,\n} from './launchd.js';\nimport type { PlistOptions, RunCommandFn, LaunchdStatus } from './launchd.js';\n\n// ---------- helpers ----------\n\nfunction tmpDir(): string {\n return mkdtempSync(join(tmpdir(), `launchd-test-${randomUUID().slice(0, 8)}-`));\n}\n\nconst cleanupDirs: string[] = [];\nafterEach(() => {\n while (cleanupDirs.length) {\n const d = cleanupDirs.pop()!;\n if (existsSync(d)) rmSync(d, { recursive: true, force: true });\n }\n});\n\nfunction basePlistOpts(overrides?: Partial<PlistOptions>): PlistOptions {\n return {\n nodePath: '/usr/local/bin/node',\n scriptPath: '/usr/local/lib/gsd-daemon/dist/cli.js',\n configPath: join(homedir(), '.gsd', 'daemon.yaml'),\n ...overrides,\n };\n}\n\n// ---------- escapeXml ----------\n\ndescribe('escapeXml', () => {\n it('escapes & < > \" \\'', () => {\n assert.equal(escapeXml('a&b<c>d\"e\\'f'), 'a&b<c>d"e'f');\n });\n\n it('leaves plain strings untouched', () => {\n assert.equal(escapeXml('/usr/local/bin/node'), '/usr/local/bin/node');\n });\n\n it('escapes paths with spaces and special chars', () => {\n const input = '/Users/John & Jane/my \"project\"/file.js';\n const output = escapeXml(input);\n assert.ok(output.includes('&'));\n assert.ok(output.includes('"'));\n // Verify no raw unescaped & remain (all & are part of & < etc.)\n assert.equal(output, '/Users/John & Jane/my "project"/file.js');\n });\n});\n\n// ---------- generatePlist ----------\n\ndescribe('generatePlist', () => {\n it('produces valid XML with plist header', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.startsWith('<?xml version=\"1.0\"'));\n assert.ok(xml.includes('<!DOCTYPE plist'));\n assert.ok(xml.includes('<plist version=\"1.0\">'));\n assert.ok(xml.includes('</plist>'));\n });\n\n it('includes label com.gsd.daemon', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<string>com.gsd.daemon</string>'));\n });\n\n it('uses the absolute node path from opts', () => {\n const opts = basePlistOpts({ nodePath: '/home/user/.nvm/versions/node/v22.0.0/bin/node' });\n const xml = generatePlist(opts);\n assert.ok(xml.includes('<string>/home/user/.nvm/versions/node/v22.0.0/bin/node</string>'));\n });\n\n it('includes NVM bin directory in PATH', () => {\n const opts = basePlistOpts({ nodePath: '/home/user/.nvm/versions/node/v22.0.0/bin/node' });\n const xml = generatePlist(opts);\n assert.ok(xml.includes('/home/user/.nvm/versions/node/v22.0.0/bin'));\n });\n\n it('sets KeepAlive with SuccessfulExit false', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<key>KeepAlive</key>'));\n assert.ok(xml.includes('<key>SuccessfulExit</key>'));\n assert.ok(xml.includes('<false/>'));\n });\n\n it('sets RunAtLoad true', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<key>RunAtLoad</key>'));\n assert.ok(xml.includes('<true/>'));\n });\n\n it('includes --config with the config path', () => {\n const configPath = '/custom/path/daemon.yaml';\n const xml = generatePlist(basePlistOpts({ configPath }));\n assert.ok(xml.includes('<string>--config</string>'));\n assert.ok(xml.includes(`<string>${configPath}</string>`));\n });\n\n it('includes HOME environment variable', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<key>HOME</key>'));\n assert.ok(xml.includes(`<string>${homedir()}</string>`));\n });\n\n it('includes StandardOutPath and StandardErrorPath', () => {\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<key>StandardOutPath</key>'));\n assert.ok(xml.includes('<key>StandardErrorPath</key>'));\n });\n\n it('escapes special characters in paths', () => {\n const opts = basePlistOpts({\n configPath: '/Users/John & Jane/config.yaml',\n });\n const xml = generatePlist(opts);\n assert.ok(xml.includes('John & Jane'));\n assert.ok(!xml.includes('John & Jane'));\n });\n\n it('uses custom stdout/stderr paths when provided', () => {\n const opts = basePlistOpts({\n stdoutPath: '/tmp/my-stdout.log',\n stderrPath: '/tmp/my-stderr.log',\n });\n const xml = generatePlist(opts);\n assert.ok(xml.includes('<string>/tmp/my-stdout.log</string>'));\n assert.ok(xml.includes('<string>/tmp/my-stderr.log</string>'));\n });\n\n it('uses custom working directory when provided', () => {\n const opts = basePlistOpts({\n workingDirectory: '/custom/work/dir',\n });\n const xml = generatePlist(opts);\n assert.ok(xml.includes('<string>/custom/work/dir</string>'));\n });\n});\n\n// ---------- getPlistPath ----------\n\ndescribe('getPlistPath', () => {\n it('returns ~/Library/LaunchAgents/com.gsd.daemon.plist', () => {\n const expected = join(homedir(), 'Library', 'LaunchAgents', 'com.gsd.daemon.plist');\n assert.equal(getPlistPath(), expected);\n });\n});\n\n// ---------- install ----------\n\ndescribe('install', () => {\n let tmp: string;\n let fakePlistPath: string;\n\n // We can't mock getPlistPath directly, but we can verify the commands\n // issued and the plist content by intercepting runCommand and filesystem ops.\n // For filesystem testing, we test the functions that call writeFileSync indirectly\n // by verifying the runCommand calls and returned values.\n\n it('calls launchctl load with the plist path', () => {\n const calls: string[] = [];\n const mockRun: RunCommandFn = (cmd: string) => {\n calls.push(cmd);\n return '';\n };\n\n // install will try to write to the real plist path, so we need to be careful.\n // We test the command flow by catching the writeFileSync error (dir may not exist in CI)\n // or by letting it proceed in local dev.\n try {\n install(basePlistOpts(), mockRun);\n } catch {\n // writeFileSync may fail if ~/Library/LaunchAgents doesn't exist in test env\n }\n\n const loadCalls = calls.filter(c => c.startsWith('launchctl load'));\n const listCalls = calls.filter(c => c.startsWith('launchctl list'));\n // Should have at least attempted launchctl load\n assert.ok(loadCalls.length > 0 || calls.length > 0, 'Expected launchctl commands to be called');\n });\n\n it('generates valid plist content when called', () => {\n // Test that the plist content would be correct by testing generatePlist\n // (install is a thin wrapper around generatePlist + writeFile + launchctl)\n const xml = generatePlist(basePlistOpts());\n assert.ok(xml.includes('<key>Label</key>'));\n assert.ok(xml.includes('<string>com.gsd.daemon</string>'));\n });\n\n it('handles idempotent install (unloads first if plist exists)', () => {\n const calls: string[] = [];\n const mockRun: RunCommandFn = (cmd: string) => {\n calls.push(cmd);\n return '';\n };\n\n // To simulate idempotent install, we need an existing plist file.\n // Since install writes to getPlistPath(), we test the command sequence.\n try {\n install(basePlistOpts(), mockRun);\n // Second install\n install(basePlistOpts(), mockRun);\n } catch {\n // filesystem may not be writable\n }\n\n // The second install should have tried to unload first\n const unloadCalls = calls.filter(c => c.startsWith('launchctl unload'));\n // If the plist path exists, we expect at least one unload attempt on second call\n // This is a command-level check; filesystem existence depends on environment\n });\n});\n\n// ---------- uninstall ----------\n\ndescribe('uninstall', () => {\n it('calls launchctl unload when plist would exist', () => {\n const calls: string[] = [];\n const mockRun: RunCommandFn = (cmd: string) => {\n calls.push(cmd);\n return '';\n };\n\n // uninstall checks existsSync(plistPath) — if plist doesn't exist, it's a no-op\n uninstall(mockRun);\n\n // If plist doesn't exist in test environment, calls should be empty (graceful)\n // That's the \"handles missing plist gracefully\" case\n });\n\n it('handles missing plist gracefully (no-op)', () => {\n const calls: string[] = [];\n const mockRun: RunCommandFn = (cmd: string) => {\n calls.push(cmd);\n return '';\n };\n\n // Shouldn't throw even if plist doesn't exist\n assert.doesNotThrow(() => uninstall(mockRun));\n });\n\n it('handles already-unloaded agent gracefully', () => {\n const mockRun: RunCommandFn = (cmd: string) => {\n if (cmd.includes('launchctl unload')) {\n throw new Error('Could not find specified service');\n }\n return '';\n };\n\n // Should not throw even if launchctl unload fails\n assert.doesNotThrow(() => uninstall(mockRun));\n });\n});\n\n// ---------- status ----------\n\ndescribe('status', () => {\n it('parses running daemon output (PID present)', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return '{\\n\\t\"PID\" = 1234;\\n\\t\"Label\" = \"com.gsd.daemon\";\\n}\\nPID\\tStatus\\tLabel\\n1234\\t0\\tcom.gsd.daemon\\n';\n };\n\n const result = status(mockRun);\n assert.equal(result.registered, true);\n assert.equal(result.pid, 1234);\n assert.equal(result.lastExitStatus, 0);\n });\n\n it('parses stopped daemon output (no PID)', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return 'PID\\tStatus\\tLabel\\n-\\t78\\tcom.gsd.daemon\\n';\n };\n\n const result = status(mockRun);\n assert.equal(result.registered, true);\n assert.equal(result.pid, null);\n assert.equal(result.lastExitStatus, 78);\n });\n\n it('returns not-registered when launchctl list fails', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n throw new Error('Could not find service \"com.gsd.daemon\" in domain for port');\n };\n\n const result = status(mockRun);\n assert.equal(result.registered, false);\n assert.equal(result.pid, null);\n assert.equal(result.lastExitStatus, null);\n });\n\n it('returns structured result with all fields', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return 'PID\\tStatus\\tLabel\\n5678\\t0\\tcom.gsd.daemon\\n';\n };\n\n const result = status(mockRun);\n assert.ok('registered' in result);\n assert.ok('pid' in result);\n assert.ok('lastExitStatus' in result);\n });\n\n it('parses JSON-style dict output (newer macOS)', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return `{\n\\t\"StandardOutPath\" = \"/Users/me/.gsd/daemon-stdout.log\";\n\\t\"LimitLoadToSessionType\" = \"Aqua\";\n\\t\"StandardErrorPath\" = \"/Users/me/.gsd/daemon-stderr.log\";\n\\t\"Label\" = \"com.gsd.daemon\";\n\\t\"OnDemand\" = true;\n\\t\"LastExitStatus\" = 0;\n\\t\"PID\" = 23802;\n\\t\"Program\" = \"/usr/local/bin/node\";\n};`;\n };\n\n const result = status(mockRun);\n assert.equal(result.registered, true);\n assert.equal(result.pid, 23802);\n assert.equal(result.lastExitStatus, 0);\n });\n\n it('parses JSON-style dict output when daemon stopped (no PID key)', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return `{\n\\t\"Label\" = \"com.gsd.daemon\";\n\\t\"LastExitStatus\" = 1;\n\\t\"OnDemand\" = true;\n};`;\n };\n\n const result = status(mockRun);\n assert.equal(result.registered, true);\n assert.equal(result.pid, null);\n assert.equal(result.lastExitStatus, 1);\n });\n\n it('handles unexpected output format gracefully', () => {\n const mockRun: RunCommandFn = (_cmd: string) => {\n return 'some unexpected output without the label';\n };\n\n // Should not throw — should return registered:true but with null fields\n // since the command succeeded (label was found) but output didn't match\n const result = status(mockRun);\n assert.equal(result.registered, true);\n });\n});\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SessionManager } from "./session-manager.js";
|
|
2
|
+
import type { ProjectInfo } from "./types.js";
|
|
3
|
+
export declare class LocalToolExecutor {
|
|
4
|
+
private readonly sessionManager;
|
|
5
|
+
private readonly scanProjects;
|
|
6
|
+
private readonly workflowHandlers;
|
|
7
|
+
constructor(sessionManager: SessionManager, scanProjects: () => Promise<ProjectInfo[]>);
|
|
8
|
+
execute(toolName: string, rawArgs: Record<string, unknown>, projectAlias?: string): Promise<unknown>;
|
|
9
|
+
advertisedProjects(): Promise<Array<{
|
|
10
|
+
alias: string;
|
|
11
|
+
path: string;
|
|
12
|
+
repoIdentity: string;
|
|
13
|
+
remoteLabel?: string;
|
|
14
|
+
markers: string[];
|
|
15
|
+
}>>;
|
|
16
|
+
private requiredProjectDir;
|
|
17
|
+
private resolveProjectPath;
|
|
18
|
+
private executeWorkflowTool;
|
|
19
|
+
private invokeRegisteredWorkflowTool;
|
|
20
|
+
private executeGraph;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=local-tool-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-tool-executor.d.ts","sourceRoot":"","sources":["../src/local-tool-executor.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgB9C,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAJ/B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;gBAGhD,cAAc,EAAE,cAAc,EAC9B,YAAY,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;IAUvD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiFpG,kBAAkB,IAAI,OAAO,CAAC,KAAK,CAAC;QACxC,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC,CAAC;YAcW,kBAAkB;YAMlB,kBAAkB;IAOhC,OAAO,CAAC,mBAAmB;IAmD3B,OAAO,CAAC,4BAA4B;YAMtB,YAAY;CAgC3B"}
|