Skip to content

Commit 2ed51a1

Browse files
authored
Merge pull request #34 from K9i-0/feature/bundled-node-server
feat: バンドル内Node.jsでサーバーを自動起動
2 parents adffbb9 + 87d5853 commit 2ed51a1

File tree

5 files changed

+116
-227
lines changed

5 files changed

+116
-227
lines changed

Sources/ClaudeUsageMonitor/AppDelegate.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1010
private var usageMonitor: UsageMonitor!
1111

1212
func applicationDidFinishLaunching(_ notification: Notification) {
13+
// Start the local server first
14+
ServerManager.shared.checkAndStartServer()
15+
1316
usageMonitor = UsageMonitor()
1417

1518
// 通知機能は初回リリースでは無効化

Sources/ClaudeUsageMonitor/ServerManager.swift

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,33 +45,53 @@ class ServerManager: ObservableObject {
4545
private func startServer() {
4646
print("[ServerManager] Attempting to start server")
4747

48-
// Find the server directory
49-
let appPath = Bundle.main.bundlePath
50-
let serverPath = (appPath as NSString).deletingLastPathComponent
51-
.appending("/server")
48+
// First, try to find server in the app bundle (production)
49+
var serverPath: String?
50+
if let bundledServerPath = Bundle.main.path(forResource: "server", ofType: nil) {
51+
serverPath = bundledServerPath
52+
print("[ServerManager] Using bundled server at: \(bundledServerPath)")
53+
} else {
54+
// Fallback to development location
55+
let appPath = Bundle.main.bundlePath
56+
let devServerPath = (appPath as NSString).deletingLastPathComponent
57+
.appending("/server")
58+
if FileManager.default.fileExists(atPath: devServerPath) {
59+
serverPath = devServerPath
60+
print("[ServerManager] Using development server at: \(devServerPath)")
61+
}
62+
}
5263

5364
// Check if server directory exists
54-
guard FileManager.default.fileExists(atPath: serverPath) else {
55-
print("[ServerManager] Server directory not found at: \(serverPath)")
65+
guard let validServerPath = serverPath else {
66+
print("[ServerManager] Server directory not found")
5667
return
5768
}
5869

5970
// Create process to start server
6071
let process = Process()
6172

62-
// Find node/npm
63-
let nodePaths = [
64-
"/Users/\(NSUserName())/.local/share/mise/shims/node",
65-
"/opt/homebrew/bin/node",
66-
"/usr/local/bin/node"
67-
]
68-
73+
// Find Node.js
6974
var nodePath: String?
70-
for path in nodePaths {
71-
let expandedPath = (path as NSString).expandingTildeInPath
72-
if FileManager.default.fileExists(atPath: expandedPath) {
73-
nodePath = expandedPath
74-
break
75+
76+
// First, try to use bundled Node.js (production)
77+
if let bundledNodePath = Bundle.main.path(forResource: "node/node", ofType: nil) {
78+
nodePath = bundledNodePath
79+
print("[ServerManager] Using bundled Node.js at: \(bundledNodePath)")
80+
} else {
81+
// Fallback to system Node.js (development)
82+
let systemNodePaths = [
83+
"/Users/\(NSUserName())/.local/share/mise/shims/node",
84+
"/opt/homebrew/bin/node",
85+
"/usr/local/bin/node"
86+
]
87+
88+
for path in systemNodePaths {
89+
let expandedPath = (path as NSString).expandingTildeInPath
90+
if FileManager.default.fileExists(atPath: expandedPath) {
91+
nodePath = expandedPath
92+
print("[ServerManager] Using system Node.js at: \(expandedPath)")
93+
break
94+
}
7595
}
7696
}
7797

@@ -81,8 +101,9 @@ class ServerManager: ObservableObject {
81101
}
82102

83103
process.executableURL = URL(fileURLWithPath: node)
84-
process.arguments = ["server.js"]
85-
process.currentDirectoryURL = URL(fileURLWithPath: serverPath)
104+
// Use server-fixed.js as specified in package.json
105+
process.arguments = ["server-fixed.js"]
106+
process.currentDirectoryURL = URL(fileURLWithPath: validServerPath)
86107

87108
// Set up environment
88109
var environment = ProcessInfo.processInfo.environment

Sources/ClaudeUsageMonitor/UsageMonitor.swift

Lines changed: 0 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -134,87 +134,6 @@ class UsageMonitor: ObservableObject, UsageMonitoring {
134134
return
135135
}
136136

137-
// App Sandbox prevents direct command execution
138-
// The following code only works when App Sandbox is disabled
139-
guard ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] == nil else {
140-
print("[DEBUG] App Sandbox is enabled - cannot execute external commands")
141-
return
142-
}
143-
144-
// Fallback to npx command (only when App Sandbox is disabled)
145-
print("[DEBUG] App Sandbox is disabled - falling back to npx command")
146-
do {
147-
let process = Process()
148-
149-
// Try to use the same npx detection as in fetchSessionData
150-
let npxSearchPaths = [
151-
"/Users/\(NSUserName())/.local/share/mise/shims/npx",
152-
"/opt/homebrew/bin/npx",
153-
"/usr/local/bin/npx"
154-
]
155-
156-
var foundNpx = false
157-
for path in npxSearchPaths {
158-
let expandedPath = (path as NSString).expandingTildeInPath
159-
if FileManager.default.fileExists(atPath: expandedPath) {
160-
process.executableURL = URL(fileURLWithPath: expandedPath)
161-
process.arguments = ["ccusage@latest", "--json"]
162-
foundNpx = true
163-
print("[DEBUG] Using npx at: \(expandedPath)")
164-
break
165-
}
166-
}
167-
168-
if !foundNpx {
169-
// Use shell with extended PATH
170-
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
171-
process.arguments = ["-l", "-c",
172-
"export PATH=\"$HOME/.local/share/mise/shims:/opt/homebrew/bin:/usr/local/bin:$PATH\" && npx ccusage@latest --json"]
173-
print("[DEBUG] Using shell with extended PATH for daily data")
174-
}
175-
176-
let pipe = Pipe()
177-
let errorPipe = Pipe()
178-
process.standardOutput = pipe
179-
process.standardError = errorPipe
180-
181-
try process.run()
182-
process.waitUntilExit()
183-
184-
// Check for errors
185-
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
186-
if !errorData.isEmpty {
187-
let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
188-
print("ccusage error: \(errorString)")
189-
}
190-
191-
let data = pipe.fileHandleForReading.readDataToEndOfFile()
192-
if !data.isEmpty {
193-
let response = try JSONDecoder().decode(CcusageResponse.self, from: data)
194-
// Get today's date in YYYY-MM-DD format
195-
let formatter = DateFormatter()
196-
formatter.dateFormat = "yyyy-MM-dd"
197-
let today = formatter.string(from: Date())
198-
199-
// Find today's usage from the array
200-
if let todayData = response.daily.first(where: { $0.date == today }) {
201-
usageData.todayUsage = todayData
202-
print("[DEBUG] Today's totalTokens: \(todayData.totalTokens) (billable: \(todayData.inputTokens + todayData.outputTokens))")
203-
} else {
204-
// If no data for today, create empty data
205-
usageData.todayUsage = nil
206-
}
207-
208-
// Store monthly total
209-
usageData.monthlyTotal = response.totals
210-
print("[DEBUG] Monthly totalTokens: \(response.totals.totalTokens) (billable: \(response.totals.inputTokens + response.totals.outputTokens))")
211-
usageData.lastUpdated = Date()
212-
}
213-
} catch {
214-
self.error = ClaudeMonitorError.unknownError("Failed to fetch usage data: \(error.localizedDescription)")
215-
print("Error details: \(error)")
216-
}
217-
218137
isLoading = false
219138
}
220139

@@ -297,132 +216,6 @@ class UsageMonitor: ObservableObject, UsageMonitoring {
297216
print("[DEBUG] Server connection failed: \(error.localizedDescription)")
298217
self.error = ClaudeMonitorError.networkError(L10n.Error.serverNotRunning)
299218
}
300-
301-
// App Sandbox prevents direct command execution
302-
guard ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] == nil else {
303-
print("[DEBUG] App Sandbox is enabled - cannot execute external commands")
304-
return
305-
}
306-
307-
// Fallback: Try multiple methods to run ccusage (only when App Sandbox is disabled)
308-
print("[DEBUG] App Sandbox is disabled - falling back to direct ccusage execution")
309-
310-
// Method 1: Try to find npx in common locations
311-
let npxSearchPaths = [
312-
"/Users/\(NSUserName())/.local/share/mise/shims/npx",
313-
"/opt/homebrew/bin/npx",
314-
"/usr/local/bin/npx",
315-
"/Users/\(NSUserName())/.nvm/default/bin/npx",
316-
"/Users/\(NSUserName())/.volta/bin/npx"
317-
]
318-
319-
var npxPath: String? = nil
320-
for path in npxSearchPaths {
321-
let expandedPath = (path as NSString).expandingTildeInPath
322-
if FileManager.default.fileExists(atPath: expandedPath) {
323-
npxPath = expandedPath
324-
print("[DEBUG] Found npx at: \(expandedPath)")
325-
break
326-
}
327-
}
328-
329-
// Method 2: Use which command to find npx
330-
if npxPath == nil {
331-
let whichProcess = Process()
332-
whichProcess.executableURL = URL(fileURLWithPath: "/usr/bin/which")
333-
whichProcess.arguments = ["npx"]
334-
let whichPipe = Pipe()
335-
whichProcess.standardOutput = whichPipe
336-
whichProcess.standardError = Pipe()
337-
338-
do {
339-
try whichProcess.run()
340-
whichProcess.waitUntilExit()
341-
342-
let data = whichPipe.fileHandleForReading.readDataToEndOfFile()
343-
if let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
344-
!output.isEmpty {
345-
npxPath = output
346-
print("[DEBUG] Found npx via which: \(output)")
347-
}
348-
} catch {
349-
print("[DEBUG] which command failed: \(error)")
350-
}
351-
}
352-
353-
// Method 3: Try with full shell initialization
354-
do {
355-
let process = Process()
356-
357-
if let npxPath = npxPath {
358-
// Use found npx directly
359-
process.executableURL = URL(fileURLWithPath: npxPath)
360-
process.arguments = ["ccusage", "blocks", "--active", "--json"]
361-
print("[DEBUG] Using direct npx: \(npxPath)")
362-
} else {
363-
// Last resort: use shell with full environment
364-
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
365-
process.arguments = ["-l", "-c",
366-
"export PATH=\"$HOME/.local/share/mise/shims:$HOME/.volta/bin:/opt/homebrew/bin:/usr/local/bin:$PATH\" && npx ccusage blocks --active --json"]
367-
print("[DEBUG] Using shell with extended PATH")
368-
}
369-
370-
let pipe = Pipe()
371-
let errorPipe = Pipe()
372-
process.standardOutput = pipe
373-
process.standardError = errorPipe
374-
375-
try process.run()
376-
process.waitUntilExit()
377-
378-
// Check for errors
379-
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
380-
if !errorData.isEmpty {
381-
let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
382-
print("[DEBUG] ccusage stderr: \(errorString)")
383-
}
384-
385-
let data = pipe.fileHandleForReading.readDataToEndOfFile()
386-
if !data.isEmpty {
387-
if let jsonString = String(data: data, encoding: .utf8) {
388-
print("[DEBUG] ccusage output length: \(jsonString.count) characters")
389-
}
390-
391-
let blocksResponse = try JSONDecoder().decode(BlocksResponse.self, from: data)
392-
393-
// 過去のセッションから最大トークン使用量を検出
394-
var maxTokens = 0
395-
for block in blocksResponse.blocks {
396-
if !block.isGap && block.totalTokens > maxTokens {
397-
maxTokens = block.totalTokens
398-
}
399-
}
400-
usageData.historicalMaxTokens = maxTokens
401-
402-
// プランタイプを自動判定して保存
403-
if maxTokens > UsageData.max5SessionTokenLimit {
404-
updateDetectedPlan("Max20")
405-
} else if maxTokens > UsageData.proSessionTokenLimit {
406-
updateDetectedPlan("Max5")
407-
} else {
408-
if usageData.detectedPlanType == nil {
409-
updateDetectedPlan("Pro")
410-
}
411-
}
412-
413-
if let activeBlock = blocksResponse.blocks.first(where: { $0.isActive }) {
414-
usageData.activeSession = activeBlock
415-
print("[DEBUG] Session data loaded via fallback")
416-
print("[DEBUG] Historical max tokens: \(maxTokens)")
417-
print("[DEBUG] Detected plan: \(usageData.detectedPlan)")
418-
}
419-
} else {
420-
print("[DEBUG] No data from ccusage command")
421-
}
422-
} catch {
423-
print("[DEBUG] All fallback methods failed: \(error)")
424-
print("[DEBUG] Error details: \(error.localizedDescription)")
425-
}
426219
}
427220

428221
func formatTokens(_ tokens: Int) -> String {

node.entitlements

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.inherit</key>
8+
<true/>
9+
</dict>
10+
</plist>

0 commit comments

Comments
 (0)