@archastro/movie-harness 0.1.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Window inventory + Screen Recording TCC helpers for desktop.window.
3
+ *
4
+ * Usage:
5
+ * swift WindowTools.swift list
6
+ * swift WindowTools.swift screen-access # preflight only (JSON)
7
+ * swift WindowTools.swift screen-access --request # may show system prompt
8
+ */
9
+ import AppKit
10
+ import CoreGraphics
11
+ import Foundation
12
+
13
+ struct WindowRow: Encodable {
14
+ let id: Int
15
+ let pid: Int
16
+ let owner: String
17
+ let title: String
18
+ let bundleId: String?
19
+ let width: Int
20
+ let height: Int
21
+ let x: Int
22
+ let y: Int
23
+ let onScreen: Bool
24
+ }
25
+
26
+ struct ScreenAccessReport: Encodable {
27
+ let granted: Bool
28
+ /// True when we invoked CGRequestScreenCaptureAccess (may have shown a prompt).
29
+ let requested: Bool
30
+ let hostApp: String
31
+ let hostBundleId: String?
32
+ let settingsHint: String
33
+ }
34
+
35
+ func listWindows() -> [WindowRow] {
36
+ // Include off-screen windows so agents can still target them.
37
+ let opts = CGWindowListOption(arrayLiteral: .excludeDesktopElements)
38
+ guard let info = CGWindowListCopyWindowInfo(opts, kCGNullWindowID) as? [[String: Any]]
39
+ else {
40
+ return []
41
+ }
42
+
43
+ var bundleByPid: [Int32: String] = [:]
44
+ for app in NSWorkspace.shared.runningApplications {
45
+ if let bid = app.bundleIdentifier {
46
+ bundleByPid[app.processIdentifier] = bid
47
+ }
48
+ }
49
+
50
+ var rows: [WindowRow] = []
51
+ for w in info {
52
+ let layer = w[kCGWindowLayer as String] as? Int ?? -1
53
+ guard layer == 0 else { continue }
54
+
55
+ let id = w[kCGWindowNumber as String] as? Int ?? 0
56
+ let pid = w[kCGWindowOwnerPID as String] as? Int ?? 0
57
+ let owner = w[kCGWindowOwnerName as String] as? String ?? ""
58
+ let title = w[kCGWindowName as String] as? String ?? ""
59
+ let onScreen = w[kCGWindowIsOnscreen as String] as? Bool ?? false
60
+ let bounds = w[kCGWindowBounds as String] as? [String: Any]
61
+ let width = (bounds?["Width"] as? NSNumber)?.intValue ?? 0
62
+ let height = (bounds?["Height"] as? NSNumber)?.intValue ?? 0
63
+ let x = (bounds?["X"] as? NSNumber)?.intValue ?? 0
64
+ let y = (bounds?["Y"] as? NSNumber)?.intValue ?? 0
65
+ if width < 2 || height < 2 { continue }
66
+
67
+ rows.append(
68
+ WindowRow(
69
+ id: id,
70
+ pid: pid,
71
+ owner: owner,
72
+ title: title,
73
+ bundleId: bundleByPid[Int32(pid)],
74
+ width: width,
75
+ height: height,
76
+ x: x,
77
+ y: y,
78
+ onScreen: onScreen
79
+ )
80
+ )
81
+ }
82
+
83
+ // Largest first — better default when multiple windows share a bundle id.
84
+ return rows.sorted { ($0.width * $0.height) > ($1.width * $1.height) }
85
+ }
86
+
87
+ func hostIdentity() -> (name: String, bundleId: String?) {
88
+ let app = NSRunningApplication.current
89
+ let name =
90
+ app.localizedName
91
+ ?? ProcessInfo.processInfo.processName
92
+ return (name, app.bundleIdentifier)
93
+ }
94
+
95
+ func screenAccess(request: Bool) -> ScreenAccessReport {
96
+ let host = hostIdentity()
97
+ var granted = CGPreflightScreenCaptureAccess()
98
+ var didRequest = false
99
+ if request && !granted {
100
+ // May show the system consent dialog (not always; Settings may still be required).
101
+ granted = CGRequestScreenCaptureAccess()
102
+ didRequest = true
103
+ }
104
+ return ScreenAccessReport(
105
+ granted: granted,
106
+ requested: didRequest,
107
+ hostApp: host.name,
108
+ hostBundleId: host.bundleId,
109
+ settingsHint:
110
+ "System Settings → Privacy & Security → Screen Recording → enable \(host.name), then quit & reopen it"
111
+ )
112
+ }
113
+
114
+ func emitJSON<T: Encodable>(_ value: T) {
115
+ let encoder = JSONEncoder()
116
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
117
+ let data = try! encoder.encode(value)
118
+ FileHandle.standardOutput.write(data)
119
+ fputs("\n", stdout)
120
+ }
121
+
122
+ let args = Array(CommandLine.arguments.dropFirst())
123
+ let command = args.first ?? "list"
124
+
125
+ if command == "list" || command == "--json" {
126
+ emitJSON(listWindows())
127
+ exit(0)
128
+ }
129
+
130
+ if command == "screen-access" {
131
+ let request = args.contains("--request") || args.contains("-r")
132
+ let report = screenAccess(request: request)
133
+ emitJSON(report)
134
+ // Exit 0 when granted, 2 when denied (easy for shells).
135
+ exit(report.granted ? 0 : 2)
136
+ }
137
+
138
+ fputs(
139
+ """
140
+ usage:
141
+ WindowTools.swift list
142
+ WindowTools.swift screen-access [--request]
143
+
144
+ list JSON windows for desktop.window matching
145
+ screen-access Screen Recording TCC preflight (optional --request prompt)
146
+
147
+ """,
148
+ stderr
149
+ )
150
+ exit(2)
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@archastro/movie-harness",
3
+ "version": "0.1.0",
4
+ "description": "Universal movie capture harness for browser, PTY, desktop, and raw frames into .astroshot/",
5
+ "keywords": [
6
+ "astroshots",
7
+ "movie",
8
+ "screencast",
9
+ "playwright",
10
+ "pty",
11
+ "harness"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ArchAstro/astroshots.git",
17
+ "directory": "packages/movie-harness"
18
+ },
19
+ "homepage": "https://github.com/ArchAstro/astroshots#readme",
20
+ "bugs": "https://github.com/ArchAstro/astroshots/issues",
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "bin": {
24
+ "astroshot-movie": "bin/astroshot-movie.mjs"
25
+ },
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "bin",
35
+ "dist",
36
+ "native",
37
+ "README.md"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "clean": "rm -rf dist",
42
+ "test": "npm run build && vitest run",
43
+ "typecheck": "tsc --noEmit",
44
+ "prepack": "npm run clean && npm run build"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.14.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "provenance": true,
52
+ "registry": "https://registry.npmjs.org/"
53
+ },
54
+ "dependencies": {
55
+ "@xterm/headless": "^5.5.0",
56
+ "playwright": "1.61.1",
57
+ "yaml": "^2.8.0"
58
+ },
59
+ "optionalDependencies": {
60
+ "node-pty": "1.2.0-beta.14"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^22.0.0",
64
+ "typescript": "^5.9.0",
65
+ "vitest": "^4.1.0"
66
+ }
67
+ }