rvlib-shipkit 1.2.0 → 1.4.0
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/CHANGELOG.md +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -1,24 +1,94 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
1
|
+
// one web app window on macOS: its own app, so the dock and cmd-tab show its
|
|
2
|
+
// own icon, never a second browser. The same source is the shipkit window and
|
|
3
|
+
// every per-process app (home/gui-app.ts builds both with swiftc, no Xcode
|
|
4
|
+
// project): the bundle's Info.plist names the app (CFBundleName) and the url
|
|
5
|
+
// a bare launch opens (ShipkitStartURL). One WKWebView per window; every
|
|
6
|
+
// `open -a <app> <url>` opens one more window, cmd-n too. A link to another
|
|
7
|
+
// site opens in the default browser.
|
|
6
8
|
import AppKit
|
|
7
9
|
import WebKit
|
|
8
10
|
|
|
9
|
-
let
|
|
11
|
+
let info = Bundle.main.infoDictionary ?? [:]
|
|
12
|
+
let appName = (info["CFBundleName"] as? String) ?? "shipkit"
|
|
13
|
+
let startURL = URL(string: (info["ShipkitStartURL"] as? String) ?? "") ?? URL(string: "http://127.0.0.1:4799/")!
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/** a url that stays in the app: the machine itself, or the host the app is for */
|
|
16
|
+
func stays(_ url: URL) -> Bool {
|
|
17
|
+
url.host == "127.0.0.1" || url.host == "localhost" || url.host == startURL.host
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** a window's content: the web view, and the find bar above it when shown.
|
|
21
|
+
* WKWebView is an NSTextFinderClient, so the platform find bar searches the
|
|
22
|
+
* page; this view is the bar's container */
|
|
23
|
+
final class Pane: NSView, NSTextFinderBarContainer {
|
|
24
|
+
let web: WKWebView
|
|
25
|
+
let finder = NSTextFinder()
|
|
26
|
+
|
|
27
|
+
init(web: WKWebView) {
|
|
28
|
+
self.web = web
|
|
29
|
+
super.init(frame: .zero)
|
|
30
|
+
addSubview(web)
|
|
31
|
+
finder.client = web
|
|
32
|
+
finder.findBarContainer = self
|
|
33
|
+
finder.isIncrementalSearchingEnabled = true
|
|
34
|
+
finder.incrementalSearchingShouldDimContentView = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
required init?(coder: NSCoder) { fatalError("not from a nib") }
|
|
38
|
+
|
|
39
|
+
override var isFlipped: Bool { true }
|
|
40
|
+
|
|
41
|
+
var findBarView: NSView? {
|
|
42
|
+
didSet {
|
|
43
|
+
oldValue?.removeFromSuperview()
|
|
44
|
+
if let bar = findBarView { addSubview(bar) }
|
|
45
|
+
needsLayout = true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var isFindBarVisible = false {
|
|
50
|
+
didSet {
|
|
51
|
+
findBarView?.isHidden = !isFindBarVisible
|
|
52
|
+
needsLayout = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func findBarViewDidChangeHeight() { needsLayout = true }
|
|
57
|
+
|
|
58
|
+
func contentView() -> NSView? { web }
|
|
59
|
+
|
|
60
|
+
override func layout() {
|
|
61
|
+
super.layout()
|
|
62
|
+
var top: CGFloat = 0
|
|
63
|
+
if let bar = findBarView, isFindBarVisible {
|
|
64
|
+
let h = bar.frame.height
|
|
65
|
+
bar.frame = NSRect(x: 0, y: 0, width: bounds.width, height: h)
|
|
66
|
+
top = h
|
|
67
|
+
}
|
|
68
|
+
web.frame = NSRect(x: 0, y: top, width: bounds.width, height: max(0, bounds.height - top))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate, NSMenuItemValidation {
|
|
12
73
|
var windows: [NSWindow] = []
|
|
13
74
|
var opened = false
|
|
14
75
|
|
|
15
76
|
func applicationDidFinishLaunching(_ note: Notification) {
|
|
16
77
|
buildMenu()
|
|
78
|
+
// esc closes the find bar of the key window, and reaches the page otherwise
|
|
79
|
+
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { e in
|
|
80
|
+
if e.keyCode == 53, let pane = self.keyPane(), pane.isFindBarVisible {
|
|
81
|
+
pane.finder.performAction(.hideFindInterface)
|
|
82
|
+
pane.window?.makeFirstResponder(pane.web)
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
return e
|
|
86
|
+
}
|
|
17
87
|
// an open event may deliver the url right after launch: wait for it
|
|
18
88
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
19
89
|
if !self.opened {
|
|
20
90
|
let arg = CommandLine.arguments.dropFirst().first { $0.hasPrefix("http") }
|
|
21
|
-
self.open(URL(string:
|
|
91
|
+
self.open(arg.flatMap { URL(string: $0) } ?? startURL)
|
|
22
92
|
}
|
|
23
93
|
}
|
|
24
94
|
}
|
|
@@ -30,26 +100,27 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
30
100
|
func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { true }
|
|
31
101
|
|
|
32
102
|
func applicationShouldHandleReopen(_ app: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
|
33
|
-
if !hasVisibleWindows { open(
|
|
103
|
+
if !hasVisibleWindows { open(startURL) }
|
|
34
104
|
return true
|
|
35
105
|
}
|
|
36
106
|
|
|
107
|
+
func keyPane() -> Pane? { NSApp.keyWindow?.contentView as? Pane }
|
|
108
|
+
|
|
37
109
|
@objc func newWindow(_ sender: Any?) {
|
|
38
|
-
|
|
39
|
-
open(current ?? URL(string: homeFallback)!)
|
|
110
|
+
open(keyPane()?.web.url ?? startURL)
|
|
40
111
|
}
|
|
41
112
|
|
|
42
113
|
@objc func reload(_ sender: Any?) {
|
|
43
|
-
(
|
|
114
|
+
keyPane()?.web.reload()
|
|
44
115
|
}
|
|
45
116
|
|
|
46
|
-
// the zoom is one value for every window, kept across restarts
|
|
117
|
+
// the zoom is one value for every window of the app, kept across restarts
|
|
47
118
|
var zoom: CGFloat {
|
|
48
119
|
get { let z = UserDefaults.standard.double(forKey: "pageZoom"); return z > 0 ? CGFloat(z) : 1 }
|
|
49
120
|
set {
|
|
50
121
|
let z = min(3, max(0.3, newValue))
|
|
51
122
|
UserDefaults.standard.set(Double(z), forKey: "pageZoom")
|
|
52
|
-
for w in windows { (w.contentView as?
|
|
123
|
+
for w in windows { (w.contentView as? Pane)?.web.pageZoom = z }
|
|
53
124
|
}
|
|
54
125
|
}
|
|
55
126
|
|
|
@@ -57,19 +128,51 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
57
128
|
@objc func zoomOut(_ sender: Any?) { zoom = zoom / 1.1 }
|
|
58
129
|
@objc func zoomReset(_ sender: Any?) { zoom = 1 }
|
|
59
130
|
|
|
131
|
+
// the menu item's tag is the NSTextFinder.Action it performs
|
|
132
|
+
@objc func findAction(_ sender: NSMenuItem) {
|
|
133
|
+
guard let pane = keyPane(), let action = NSTextFinder.Action(rawValue: sender.tag) else { return }
|
|
134
|
+
pane.finder.performAction(action)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// no public api opens the inspector: `_inspector` then `show` is the
|
|
138
|
+
// WebKit call the context menu's Inspect Element makes. Where it is gone,
|
|
139
|
+
// say where the inspector still is
|
|
140
|
+
@objc func devTools(_ sender: Any?) {
|
|
141
|
+
guard let web = keyPane()?.web else { return }
|
|
142
|
+
let get = NSSelectorFromString("_inspector"), show = NSSelectorFromString("show")
|
|
143
|
+
if web.responds(to: get), let inspector = web.perform(get)?.takeUnretainedValue() as? NSObject, inspector.responds(to: show) {
|
|
144
|
+
inspector.perform(show)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
let alert = NSAlert()
|
|
148
|
+
alert.messageText = "This WebKit has no way to open the inspector from a menu"
|
|
149
|
+
alert.informativeText = "Right click the page and pick Inspect Element."
|
|
150
|
+
alert.runModal()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
|
154
|
+
if item.action == #selector(findAction(_:)) {
|
|
155
|
+
guard let pane = keyPane(), let action = NSTextFinder.Action(rawValue: item.tag) else { return false }
|
|
156
|
+
return pane.finder.validateAction(action)
|
|
157
|
+
}
|
|
158
|
+
if item.action == #selector(devTools(_:)) || item.action == #selector(reload(_:)) { return keyPane() != nil }
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
60
162
|
func open(_ url: URL) {
|
|
61
163
|
opened = true
|
|
62
164
|
let config = WKWebViewConfiguration()
|
|
63
165
|
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
64
166
|
let web = WKWebView(frame: .zero, configuration: config)
|
|
167
|
+
if #available(macOS 13.3, *) { web.isInspectable = true }
|
|
65
168
|
web.navigationDelegate = self
|
|
66
169
|
web.uiDelegate = self
|
|
67
170
|
web.allowsBackForwardNavigationGestures = true
|
|
68
171
|
web.pageZoom = zoom
|
|
69
172
|
let frame = NSRect(x: 0, y: 0, width: 1400, height: 900)
|
|
70
173
|
let win = NSWindow(contentRect: frame, styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
|
|
71
|
-
win.contentView = web
|
|
72
|
-
win.title =
|
|
174
|
+
win.contentView = Pane(web: web)
|
|
175
|
+
win.title = appName
|
|
73
176
|
win.delegate = self
|
|
74
177
|
win.isReleasedWhenClosed = false
|
|
75
178
|
win.setFrameAutosaveName("shipkit-window")
|
|
@@ -77,6 +180,7 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
77
180
|
windows.append(win)
|
|
78
181
|
web.load(URLRequest(url: url))
|
|
79
182
|
win.makeKeyAndOrderFront(nil)
|
|
183
|
+
win.makeFirstResponder(web)
|
|
80
184
|
NSApp.activate(ignoringOtherApps: true)
|
|
81
185
|
}
|
|
82
186
|
|
|
@@ -84,35 +188,40 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
84
188
|
windows.removeAll { $0 === note.object as? NSWindow }
|
|
85
189
|
}
|
|
86
190
|
|
|
87
|
-
// the page title names the window, so cmd-tab and the window menu tell
|
|
191
|
+
// the page title names the window, so cmd-tab and the window menu tell pages apart
|
|
88
192
|
func webView(_ web: WKWebView, didFinish nav: WKNavigation!) {
|
|
89
|
-
web.window?.title = (web.title?.isEmpty == false) ? web.title! :
|
|
193
|
+
web.window?.title = (web.title?.isEmpty == false) ? web.title! : appName
|
|
90
194
|
}
|
|
91
195
|
|
|
92
|
-
//
|
|
196
|
+
// the app's own host: stay in the window; anything else: the default browser
|
|
93
197
|
func webView(_ web: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
94
198
|
guard let url = action.request.url, action.navigationType == .linkActivated else { return decisionHandler(.allow) }
|
|
95
|
-
|
|
96
|
-
if local { return decisionHandler(.allow) }
|
|
199
|
+
if stays(url) { return decisionHandler(.allow) }
|
|
97
200
|
NSWorkspace.shared.open(url)
|
|
98
201
|
decisionHandler(.cancel)
|
|
99
202
|
}
|
|
100
203
|
|
|
101
|
-
// window.open and target=_blank: one more
|
|
204
|
+
// window.open and target=_blank: one more window of this app for its own pages
|
|
102
205
|
func webView(_ web: WKWebView, createWebViewWith config: WKWebViewConfiguration, for action: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
103
206
|
if let url = action.request.url {
|
|
104
|
-
if url
|
|
207
|
+
if stays(url) { open(url) } else { NSWorkspace.shared.open(url) }
|
|
105
208
|
}
|
|
106
209
|
return nil
|
|
107
210
|
}
|
|
108
211
|
|
|
212
|
+
func item(_ menu: NSMenu, _ title: String, _ action: Selector, _ key: String, _ mods: NSEvent.ModifierFlags = [.command], tag: Int = 0) {
|
|
213
|
+
let i = menu.addItem(withTitle: title, action: action, keyEquivalent: key)
|
|
214
|
+
i.keyEquivalentModifierMask = mods
|
|
215
|
+
i.tag = tag
|
|
216
|
+
}
|
|
217
|
+
|
|
109
218
|
func buildMenu() {
|
|
110
219
|
let main = NSMenu()
|
|
111
220
|
let appItem = NSMenuItem()
|
|
112
221
|
main.addItem(appItem)
|
|
113
222
|
let appMenu = NSMenu()
|
|
114
|
-
appMenu.addItem(withTitle: "Hide
|
|
115
|
-
appMenu.addItem(withTitle: "Quit
|
|
223
|
+
appMenu.addItem(withTitle: "Hide \(appName)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
|
224
|
+
appMenu.addItem(withTitle: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
116
225
|
appItem.submenu = appMenu
|
|
117
226
|
let fileItem = NSMenuItem()
|
|
118
227
|
main.addItem(fileItem)
|
|
@@ -130,6 +239,14 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
130
239
|
edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
131
240
|
edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
132
241
|
edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
242
|
+
edit.addItem(.separator())
|
|
243
|
+
let findItem = edit.addItem(withTitle: "Find", action: nil, keyEquivalent: "")
|
|
244
|
+
let find = NSMenu(title: "Find")
|
|
245
|
+
item(find, "Find…", #selector(findAction(_:)), "f", tag: NSTextFinder.Action.showFindInterface.rawValue)
|
|
246
|
+
item(find, "Find Next", #selector(findAction(_:)), "g", tag: NSTextFinder.Action.nextMatch.rawValue)
|
|
247
|
+
item(find, "Find Previous", #selector(findAction(_:)), "g", [.command, .shift], tag: NSTextFinder.Action.previousMatch.rawValue)
|
|
248
|
+
item(find, "Hide Find Bar", #selector(findAction(_:)), "", [], tag: NSTextFinder.Action.hideFindInterface.rawValue)
|
|
249
|
+
edit.setSubmenu(find, for: findItem)
|
|
133
250
|
editItem.submenu = edit
|
|
134
251
|
let viewItem = NSMenuItem()
|
|
135
252
|
main.addItem(viewItem)
|
|
@@ -141,6 +258,8 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
141
258
|
alsoIn.isAlternate = true
|
|
142
259
|
alsoIn.keyEquivalentModifierMask = [.command]
|
|
143
260
|
view.addItem(withTitle: "Zoom Out", action: #selector(zoomOut(_:)), keyEquivalent: "-")
|
|
261
|
+
view.addItem(.separator())
|
|
262
|
+
item(view, "Developer Tools", #selector(devTools(_:)), "i", [.command, .option])
|
|
144
263
|
viewItem.submenu = view
|
|
145
264
|
let winItem = NSMenuItem()
|
|
146
265
|
main.addItem(winItem)
|
package/src/home/gui-app.ts
CHANGED
|
@@ -1,90 +1,122 @@
|
|
|
1
|
-
// the macOS
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// the macOS apps shipkit opens web pages in: ONE source, ShipkitGui.swift,
|
|
2
|
+
// built with swiftc into a bundle per page it serves. `shipkit gui` is that
|
|
3
|
+
// app pointed at the home; each global process with a url gets its own, named
|
|
4
|
+
// and iconed after it, in its repo's `.shipkit/wrapper/`. The bundle's
|
|
5
|
+
// Info.plist carries what differs (name, bundle id, start url), so a fix to
|
|
6
|
+
// the window (find, the inspector, zoom) lands in every app at its next
|
|
7
|
+
// build. Rebuilt only when the source, the icon or the start url changed (a
|
|
8
|
+
// stamp in the bundle). No compiler, or not macOS: the caller falls back.
|
|
6
9
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
7
10
|
import { homedir } from 'os'
|
|
8
11
|
import { dirname, join } from 'path'
|
|
9
12
|
import { fileURLToPath } from 'url'
|
|
13
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
10
14
|
|
|
11
15
|
const SOURCE = fileURLToPath(new URL('./gui-app/ShipkitGui.swift', import.meta.url))
|
|
12
16
|
|
|
13
17
|
export const BUNDLE_ID = 'dev.shipkit.gui'
|
|
14
18
|
export const guiAppPath = (): string => process.env.SHIPKIT_GUI_APP ?? join(homedir(), 'Applications', 'Shipkit.app')
|
|
15
19
|
|
|
16
|
-
/** the icon the app wears: the
|
|
20
|
+
/** the icon the app wears: the repo's own mark, largest first. Only a
|
|
17
21
|
* png can become an icns */
|
|
18
|
-
export const APP_ICON_FILES = [
|
|
22
|
+
export const APP_ICON_FILES = [`${LAYOUT.identity}/icon.png`, 'public/logo.png', 'public/favicon.png']
|
|
19
23
|
|
|
20
24
|
export function appIconOf(root: string): string | null {
|
|
21
25
|
for (const f of APP_ICON_FILES) if (existsSync(join(root, f))) return join(root, f)
|
|
22
26
|
return null
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
/** what one app bundle is: the rest comes from the shared source */
|
|
30
|
+
export type AppSpec = {
|
|
31
|
+
/** CFBundleName: the dock, cmd-tab and the app menu say it */
|
|
32
|
+
name: string
|
|
33
|
+
bundleId: string
|
|
34
|
+
/** the page a launch with no url opens; null opens the home */
|
|
35
|
+
url: string | null
|
|
36
|
+
icon: string | null
|
|
37
|
+
/** where the bundle lands */
|
|
38
|
+
app: string
|
|
39
|
+
version: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const xml = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
43
|
+
|
|
44
|
+
/** plain http to a host that is not local needs its own ATS exception; the
|
|
45
|
+
* local network is allowed to every app */
|
|
46
|
+
function atsOf(url: string | null): string {
|
|
47
|
+
let u: URL | null = null
|
|
48
|
+
try { u = url == null ? null : new URL(url) } catch { /* not a url: the local network only */ }
|
|
49
|
+
const host = u?.protocol === 'http:' ? u.hostname : null
|
|
50
|
+
const local = host == null || host === 'localhost' || /^[\d.]+$/.test(host) || !host.includes('.') || host.endsWith('.local')
|
|
51
|
+
const exception = local ? '' : `<key>NSExceptionDomains</key><dict><key>${xml(host)}</key><dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict></dict>`
|
|
52
|
+
return `<dict><key>NSAllowsLocalNetworking</key><true/>${exception}</dict>`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function infoPlist(o: Omit<AppSpec, 'app' | 'icon'>): string {
|
|
26
56
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
27
57
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
28
58
|
<plist version="1.0">
|
|
29
59
|
<dict>
|
|
30
|
-
\t<key>CFBundleName</key><string
|
|
31
|
-
\t<key>CFBundleDisplayName</key><string
|
|
32
|
-
\t<key>CFBundleIdentifier</key><string>${
|
|
60
|
+
\t<key>CFBundleName</key><string>${xml(o.name)}</string>
|
|
61
|
+
\t<key>CFBundleDisplayName</key><string>${xml(o.name)}</string>
|
|
62
|
+
\t<key>CFBundleIdentifier</key><string>${xml(o.bundleId)}</string>
|
|
33
63
|
\t<key>CFBundleExecutable</key><string>shipkit-gui</string>
|
|
34
64
|
\t<key>CFBundleIconFile</key><string>AppIcon</string>
|
|
35
65
|
\t<key>CFBundlePackageType</key><string>APPL</string>
|
|
36
|
-
\t<key>CFBundleShortVersionString</key><string>${o.version}</string>
|
|
37
|
-
\t<key>CFBundleVersion</key><string>${o.version}</string>
|
|
38
|
-
\t<key>LSMinimumSystemVersion</key><string>12.0</string>
|
|
66
|
+
\t<key>CFBundleShortVersionString</key><string>${xml(o.version)}</string>
|
|
67
|
+
\t<key>CFBundleVersion</key><string>${xml(o.version)}</string>
|
|
68
|
+
${o.url == null ? '' : `\t<key>ShipkitStartURL</key><string>${xml(o.url)}</string>\n`}\t<key>LSMinimumSystemVersion</key><string>12.0</string>
|
|
39
69
|
\t<key>NSHighResolutionCapable</key><true/>
|
|
40
|
-
\t<key>NSAppTransportSecurity</key
|
|
70
|
+
\t<key>NSAppTransportSecurity</key>${atsOf(o.url)}
|
|
41
71
|
</dict>
|
|
42
72
|
</plist>
|
|
43
73
|
`
|
|
44
74
|
}
|
|
45
75
|
|
|
46
|
-
type Run = (cmd: string[]) => { code: number; err: string }
|
|
76
|
+
export type Run = (cmd: string[]) => { code: number; err: string }
|
|
47
77
|
const runSync: Run = (cmd) => {
|
|
48
78
|
const p = Bun.spawnSync(cmd, { stdout: 'ignore', stderr: 'pipe' })
|
|
49
79
|
return { code: p.exitCode ?? 1, err: p.stderr.toString().trim() }
|
|
50
80
|
}
|
|
51
81
|
|
|
52
|
-
/** the stamp a built bundle carries: what it was built from
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
/** the stamp a built bundle carries: what it was built from. The version is
|
|
83
|
+
* left out, so a shipkit release rebuilds nothing */
|
|
84
|
+
export function stampOf(source: string, icon: string | null, identity = ''): string {
|
|
85
|
+
const h = new Bun.CryptoHasher('sha256').update(source).update(identity)
|
|
55
86
|
if (icon != null) h.update(readFileSync(icon))
|
|
56
87
|
return h.digest('hex').slice(0, 16)
|
|
57
88
|
}
|
|
58
89
|
|
|
59
|
-
/**
|
|
90
|
+
/** one app, built or rebuilt when stale. Throws with the step that failed.
|
|
60
91
|
* `rebuilt` tells the caller a running copy is now the old one */
|
|
61
|
-
export function
|
|
62
|
-
const app
|
|
92
|
+
export function ensureApp(spec: AppSpec, o: { run?: Run; source?: string } = {}): { app: string; rebuilt: boolean } {
|
|
93
|
+
const { app, icon } = spec
|
|
63
94
|
const run = o.run ?? runSync
|
|
64
|
-
const
|
|
65
|
-
const stamp = stampOf(readFileSync(
|
|
95
|
+
const source = o.source ?? SOURCE
|
|
96
|
+
const stamp = stampOf(readFileSync(source, 'utf8'), icon, JSON.stringify([spec.name, spec.bundleId, spec.url]))
|
|
66
97
|
const stampFile = join(app, 'Contents', 'Resources', 'stamp')
|
|
67
98
|
if (existsSync(stampFile) && readFileSync(stampFile, 'utf8') === stamp) return { app, rebuilt: false }
|
|
68
99
|
const tmp = `${app}.building-${process.pid}`
|
|
100
|
+
const fail = (msg: string): never => { rmSync(tmp, { recursive: true, force: true }); throw new Error(msg) }
|
|
69
101
|
rmSync(tmp, { recursive: true, force: true })
|
|
70
102
|
const res = join(tmp, 'Contents', 'Resources')
|
|
71
103
|
mkdirSync(join(tmp, 'Contents', 'MacOS'), { recursive: true })
|
|
72
104
|
mkdirSync(res, { recursive: true })
|
|
73
|
-
writeFileSync(join(tmp, 'Contents', 'Info.plist'), infoPlist(
|
|
74
|
-
const built = run(['swiftc', '-O', '-o', join(tmp, 'Contents', 'MacOS', 'shipkit-gui'),
|
|
75
|
-
if (built.code !== 0)
|
|
105
|
+
writeFileSync(join(tmp, 'Contents', 'Info.plist'), infoPlist(spec))
|
|
106
|
+
const built = run(['swiftc', '-O', '-o', join(tmp, 'Contents', 'MacOS', 'shipkit-gui'), source])
|
|
107
|
+
if (built.code !== 0) fail(`swiftc could not build ${spec.name}.app: ${built.err.slice(0, 400)}`)
|
|
76
108
|
if (icon != null) {
|
|
77
109
|
const set = join(res, 'AppIcon.iconset')
|
|
78
110
|
mkdirSync(set, { recursive: true })
|
|
79
111
|
for (const n of [16, 32, 128, 256, 512]) {
|
|
80
112
|
for (const [scale, px] of [['', n], ['@2x', n * 2]] as const) {
|
|
81
|
-
const r = run(['sips', '-z', String(px), String(px), icon, '--out', join(set, `icon_${n}x${n}${scale}.png`)])
|
|
82
|
-
if (r.code !== 0)
|
|
113
|
+
const r = run(['sips', '-s', 'format', 'png', '-z', String(px), String(px), icon, '--out', join(set, `icon_${n}x${n}${scale}.png`)])
|
|
114
|
+
if (r.code !== 0) fail(`sips could not size the icon ${icon}: ${r.err.slice(0, 200)}`)
|
|
83
115
|
}
|
|
84
116
|
}
|
|
85
117
|
const r = run(['iconutil', '-c', 'icns', set, '-o', join(res, 'AppIcon.icns')])
|
|
86
118
|
rmSync(set, { recursive: true, force: true })
|
|
87
|
-
if (r.code !== 0)
|
|
119
|
+
if (r.code !== 0) fail(`iconutil could not build the icon: ${r.err.slice(0, 200)}`)
|
|
88
120
|
}
|
|
89
121
|
writeFileSync(join(res, 'stamp'), stamp)
|
|
90
122
|
mkdirSync(dirname(app), { recursive: true })
|
|
@@ -95,5 +127,36 @@ export function ensureGuiApp(o: { root: string; version: string; app?: string; r
|
|
|
95
127
|
return { app, rebuilt: true }
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
/** the shipkit app: the shared source on the home, the house repo's icon */
|
|
131
|
+
export function ensureGuiApp(o: { root: string | null; version: string; app?: string; run?: Run }): { app: string; rebuilt: boolean } {
|
|
132
|
+
return ensureApp({ name: 'shipkit', bundleId: BUNDLE_ID, url: null, icon: o.root == null ? null : appIconOf(o.root), app: o.app ?? guiAppPath(), version: o.version }, { run: o.run })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** what a global process gives its app */
|
|
136
|
+
export type WrapperOf = { id: string; label: string; url: string | null; icon: string | null; root: string }
|
|
137
|
+
|
|
138
|
+
/** the image icons sips reads; an emoji or an svg leaves the repo's mark */
|
|
139
|
+
const RASTER = /\.(png|jpe?g|gif|webp|bmp|tiff?)$/i
|
|
140
|
+
|
|
141
|
+
export const wrapperBundleId = (id: string): string => `dev.shipkit.app.${id.replace(/[^A-Za-z0-9-]+/g, '-')}`
|
|
142
|
+
export const wrapperPath = (root: string, id: string): string => join(root, LAYOUT.wrapper, `${id.replace(/[^A-Za-z0-9._-]+/g, '-')}.app`)
|
|
143
|
+
|
|
144
|
+
/** the spec of a process's own app: its label, its declared image icon or
|
|
145
|
+
* else its repo's mark, in its repo's `.shipkit/wrapper/`. Throws for a
|
|
146
|
+
* process with no url: there is no page to wrap */
|
|
147
|
+
export function wrapperSpec(d: WrapperOf, version: string): AppSpec {
|
|
148
|
+
if (d.url == null) throw new Error(`${d.id} declares no url: there is no page to open as an app`)
|
|
149
|
+
const declared = d.icon != null && RASTER.test(d.icon) && existsSync(join(d.root, d.icon)) ? join(d.root, d.icon) : null
|
|
150
|
+
return { name: d.label, bundleId: wrapperBundleId(d.id), url: d.url, icon: declared ?? appIconOf(d.root), app: wrapperPath(d.root, d.id), version }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const ensureWrapperApp = (d: WrapperOf, o: { version: string; run?: Run }): { app: string; rebuilt: boolean; bundleId: string } => {
|
|
154
|
+
const spec = wrapperSpec(d, o.version)
|
|
155
|
+
return { ...ensureApp(spec, { run: o.run }), bundleId: spec.bundleId }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** quits a running copy of an app, and launches nothing when none runs */
|
|
159
|
+
export const quitCmd = (bundleId: string): string[] => ['osascript', '-e', `if application id "${bundleId}" is running then quit application id "${bundleId}"`]
|
|
160
|
+
|
|
98
161
|
/** true when this machine can build the app */
|
|
99
162
|
export const canBuildGui = (platform = process.platform): boolean => platform === 'darwin' && Bun.which('swiftc') != null && Bun.which('iconutil') != null
|
package/src/home/home-launchd.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// dies. macOS only; launchd is the one supervisor of the one process that must
|
|
3
3
|
// always run, and pm supervises everything else (the daemon, the web, the
|
|
4
4
|
// machine resources).
|
|
5
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
6
6
|
import { homedir } from 'os'
|
|
7
7
|
import { join } from 'path'
|
|
8
8
|
import { homeDir, pmLog } from './home-paths.ts'
|
|
@@ -55,7 +55,15 @@ ${args}
|
|
|
55
55
|
`
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/** the house repo the installed pm runs from, read back from its agent: the
|
|
59
|
+
* one answer to "which house" that does not depend on cwd */
|
|
60
|
+
export function installedHouseRoot(file = plistPath()): string | null {
|
|
61
|
+
if (!existsSync(file)) return null
|
|
62
|
+
const m = /<key>WorkingDirectory<\/key>\s*<string>([^<]*)<\/string>/.exec(readFileSync(file, 'utf8'))
|
|
63
|
+
return m == null ? null : m[1]!.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Launchctl =(args: string[]) => Promise<{ code: number; out: string }>
|
|
59
67
|
|
|
60
68
|
const launchctl: Launchctl = async (args) => {
|
|
61
69
|
const p = Bun.spawn(['launchctl', ...args], { stdout: 'pipe', stderr: 'pipe' })
|
package/src/home/home-routes.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { tailLines } from './supervise.ts'
|
|
|
16
16
|
import { Hubs, type WorkerView } from './hubs.ts'
|
|
17
17
|
import type { Planner, PlanRow } from '../plan/planner.ts'
|
|
18
18
|
import type { Widgets, WidgetView } from './widgets.ts'
|
|
19
|
+
import { ActivityCache, windowMinutes } from './activity.ts'
|
|
20
|
+
import { HouseRuns } from '../serve/house-runs.ts'
|
|
21
|
+
import type { HouseView } from '../ledger/house-runs.ts'
|
|
19
22
|
|
|
20
23
|
/** how often the daemon looks at every repo */
|
|
21
24
|
export const TICK_MS = 2_000
|
|
@@ -76,10 +79,13 @@ export const runModernize = async (repo: WiredRepo, root: string, log: string):
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
/** active = an agent works in it, VS Code has it open, or it is flagged by
|
|
80
|
-
|
|
82
|
+
/** active = an agent works in it, VS Code has it open, or it is flagged by
|
|
83
|
+
* hand. The house repo is active whenever any repo is: its checks serve
|
|
84
|
+
* every repo (a login, a shared key), so they follow the machine's work */
|
|
85
|
+
export function activeSet(rows: ProjectRow[], flagged: string[], house?: string): Set<string> {
|
|
81
86
|
const out = new Set(flagged)
|
|
82
87
|
for (const r of rows) if ((r.sessions != null && r.sessions > 0) || r.vscode === true) out.add(r.name)
|
|
88
|
+
if (house != null && out.size > 0) out.add(house)
|
|
83
89
|
return out
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -87,13 +93,24 @@ export function activeSet(rows: ProjectRow[], flagged: string[]): Set<string> {
|
|
|
87
93
|
export type WidgetsAnswer = { widgets: WidgetView[]; errors: string[] }
|
|
88
94
|
|
|
89
95
|
/** the planned page's data: every row of the machine's plan */
|
|
90
|
-
export type PlannedAnswer = {
|
|
96
|
+
export type PlannedAnswer = {
|
|
97
|
+
now: number
|
|
98
|
+
rows: PlanRow[]
|
|
99
|
+
errors: { repo: string; error: string }[]
|
|
100
|
+
running: boolean
|
|
101
|
+
/** what holds work back, one line each (Planner.held) */
|
|
102
|
+
held: string[]
|
|
103
|
+
}
|
|
91
104
|
|
|
92
105
|
export function homeRoutes(d: HomeDeps) {
|
|
93
106
|
let repos: WiredRepo[] = []
|
|
94
107
|
let rows: ProjectRow[] = []
|
|
95
108
|
const modernizing = new Map<string, ModernizeView & { log: string }>()
|
|
109
|
+
// read only while the activity page asks, never on the tick
|
|
110
|
+
const activity = new ActivityCache({ declared: (name) => d.plan?.declared(name) ?? null, titles: (name) => d.plan?.titles(name) ?? null })
|
|
96
111
|
const modernizeLog = (repo: string): string => join(d.dir, 'modernize', `${repo}.log`)
|
|
112
|
+
// the dock: every loaded repo's recent runs, read only while a page asks
|
|
113
|
+
const house = new HouseRuns(() => repos)
|
|
97
114
|
|
|
98
115
|
async function refresh(): Promise<void> {
|
|
99
116
|
const a = await d.gutter.answer()
|
|
@@ -107,7 +124,7 @@ export function homeRoutes(d: HomeDeps) {
|
|
|
107
124
|
|
|
108
125
|
async function tick(): Promise<void> {
|
|
109
126
|
await refresh()
|
|
110
|
-
active = activeSet(rows, d.state.data.active)
|
|
127
|
+
active = activeSet(rows, d.state.data.active, rows.find((r) => r.path === d.root)?.name ?? d.name)
|
|
111
128
|
// the plan's clock: one look per tick, and it starts at most one batch
|
|
112
129
|
d.plan?.tick()
|
|
113
130
|
// a widget render never holds the tick back
|
|
@@ -148,11 +165,22 @@ export function homeRoutes(d: HomeDeps) {
|
|
|
148
165
|
}
|
|
149
166
|
if (path === '/api/home/planned.json') {
|
|
150
167
|
if (repos.length === 0) await refresh()
|
|
151
|
-
const out: PlannedAnswer = { now: Date.now(), rows: d.plan?.rows() ?? [], errors: d.plan?.errors() ?? [], running: d.plan?.busy() === true }
|
|
168
|
+
const out: PlannedAnswer = { now: Date.now(), rows: d.plan?.rows() ?? [], errors: d.plan?.errors() ?? [], running: d.plan?.busy() === true, held: d.plan?.held() ?? [] }
|
|
152
169
|
return Response.json(out)
|
|
153
170
|
}
|
|
171
|
+
if (path === '/api/home/activity.json') {
|
|
172
|
+
if (repos.length === 0) await refresh()
|
|
173
|
+
const q = new URL(req.url).searchParams
|
|
174
|
+
return Response.json(await activity.get(repos, windowMinutes(q.get('m')), { fresh: q.get('fresh') === '1' }))
|
|
175
|
+
}
|
|
176
|
+
if (path === '/api/house-runs.json') {
|
|
177
|
+
if (repos.length === 0) await refresh()
|
|
178
|
+
const view: HouseView = { ...house.view(), held: d.plan?.held() ?? [] }
|
|
179
|
+
return Response.json(view, { headers: { 'cache-control': 'no-store' } })
|
|
180
|
+
}
|
|
154
181
|
if (path === '/api/home/widgets.json') {
|
|
155
|
-
|
|
182
|
+
if (d.widgets != null && repos.length === 0) await refresh()
|
|
183
|
+
const out: WidgetsAnswer = { widgets: (await d.widgets?.look(repos.map((r) => ({ name: r.name, path: r.path })))) ?? [], errors: d.widgets?.errors ?? [] }
|
|
156
184
|
return Response.json(out)
|
|
157
185
|
}
|
|
158
186
|
if (path === '/api/home.json') {
|
package/src/home/home-start.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { homedir } from 'os'
|
|
|
6
6
|
import { dirname, join, resolve } from 'path'
|
|
7
7
|
import { gitRoot } from '../core/git-root.ts'
|
|
8
8
|
import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
|
|
9
|
-
import { BUNDLE_ID, canBuildGui, ensureGuiApp } from './gui-app.ts'
|
|
10
|
-
import { LABEL, plistPath } from './home-launchd.ts'
|
|
9
|
+
import { BUNDLE_ID, canBuildGui, ensureGuiApp, ensureWrapperApp, quitCmd, type WrapperOf } from './gui-app.ts'
|
|
10
|
+
import { installedHouseRoot, LABEL, plistPath } from './home-launchd.ts'
|
|
11
11
|
import { childLog, daemonPort, daemonUp, homeDir, homePort, homeUp, pmLog } from './home-paths.ts'
|
|
12
12
|
|
|
13
13
|
/** the pm LaunchAgent is installed: on this machine no hub runs outside the home */
|
|
@@ -65,7 +65,11 @@ export function houseRootOf(cwd: string): string | null {
|
|
|
65
65
|
return gitRoot(dirname(index))?.top ?? null
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/** the house the shipkit app wears the mark of: the one cwd sits in, else the
|
|
69
|
+
* one the installed pm runs from. Never cwd itself, which carries no mark */
|
|
70
|
+
export const guiRoot = (cwd: string, plist = plistPath()): string | null => houseRootOf(cwd) ?? installedHouseRoot(plist)
|
|
71
|
+
|
|
72
|
+
export const CHROME =process.env.CHROME ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
|
69
73
|
/** the window's own Chrome profile: its own dock icon and cookies, apart from the everyday browser */
|
|
70
74
|
export const guiProfile = (): string => join(homeDir(), 'gui-profile')
|
|
71
75
|
const GUI_DISK_CACHE = 32 * 1024 * 1024
|
|
@@ -87,9 +91,9 @@ export function windowCmd(url: string, o: { chrome?: string; platform?: string }
|
|
|
87
91
|
export function openWindow(url: string, o: { root?: string | null; version?: string; warn?: (l: string) => void } = {}): string[] {
|
|
88
92
|
if (canBuildGui()) {
|
|
89
93
|
try {
|
|
90
|
-
const { app, rebuilt } = ensureGuiApp({ root: o.root ??
|
|
94
|
+
const { app, rebuilt } = ensureGuiApp({ root: o.root ?? guiRoot(process.cwd()),version: o.version ?? '0' })
|
|
91
95
|
// a running copy is the old build: it quits, and the new one opens the window
|
|
92
|
-
if (rebuilt) Bun.spawnSync(
|
|
96
|
+
if (rebuilt) Bun.spawnSync(quitCmd(BUNDLE_ID), { stdout: 'ignore', stderr: 'ignore' })
|
|
93
97
|
const cmd = ['open', '-a', app, url]
|
|
94
98
|
Bun.spawn(cmd, { stdio: ['ignore', 'ignore', 'ignore'] })
|
|
95
99
|
return cmd
|
|
@@ -97,6 +101,30 @@ export function openWindow(url: string, o: { root?: string | null; version?: str
|
|
|
97
101
|
o.warn?.(`WARN the shipkit app could not be built, so this window is a Chrome one: ${(e as Error).message}`)
|
|
98
102
|
}
|
|
99
103
|
}
|
|
104
|
+
return chromeWindow(url)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** a global process's own app: built on first use in its repo's
|
|
108
|
+
* `.shipkit/wrapper/`, then opened with no url, so a running copy comes to
|
|
109
|
+
* the front instead of stacking windows. Where no app can be built, a
|
|
110
|
+
* Chrome app window on its url */
|
|
111
|
+
export function openProcessApp(d: WrapperOf, o: { version: string; warn?: (l: string) => void }): string[] {
|
|
112
|
+
if (d.url == null) throw new Error(`${d.id} declares no url: there is no page to open as an app`)
|
|
113
|
+
if (canBuildGui()) {
|
|
114
|
+
try {
|
|
115
|
+
const { app, rebuilt, bundleId } = ensureWrapperApp(d, { version: o.version })
|
|
116
|
+
if (rebuilt) Bun.spawnSync(quitCmd(bundleId), { stdout: 'ignore', stderr: 'ignore' })
|
|
117
|
+
const cmd = ['open', '-a', app]
|
|
118
|
+
Bun.spawnSync(cmd, { stdout: 'ignore', stderr: 'ignore' })
|
|
119
|
+
return cmd
|
|
120
|
+
} catch (e) {
|
|
121
|
+
o.warn?.(`WARN the app for ${d.id} could not be built, so this window is a Chrome one: ${(e as Error).message}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return chromeWindow(d.url)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function chromeWindow(url: string): string[] {
|
|
100
128
|
mkdirSync(guiProfile(), { recursive: true })
|
|
101
129
|
const cmd = windowCmd(url)
|
|
102
130
|
// detached: the window outlives the command that opened it
|