cedarstalking with keyboard shortcuts
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: init

+1661
+3
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + .DS_Store
+87
assets/auth-browser.swift
··· 1 + import Cocoa 2 + import WebKit 3 + 4 + let TARGET_HOST = "selfservice.cedarville.edu" 5 + let AUTH_COOKIES: Set<String> = [".ASPXAUTH", "studentselfservice_live"] 6 + 7 + class AuthBrowser: NSObject, NSApplicationDelegate, WKNavigationDelegate, NSWindowDelegate { 8 + var window: NSWindow! 9 + var webView: WKWebView! 10 + let signInUrl: URL 11 + let cookieOutputFile: String 12 + var didComplete = false 13 + 14 + init(url: URL, cookieOutputFile: String) { 15 + self.signInUrl = url 16 + self.cookieOutputFile = cookieOutputFile 17 + } 18 + 19 + func applicationDidFinishLaunching(_: Notification) { 20 + let config = WKWebViewConfiguration() 21 + config.websiteDataStore = .nonPersistent() // fully isolated, no shared cookies 22 + 23 + let rect = NSRect(x: 0, y: 0, width: 520, height: 700) 24 + webView = WKWebView(frame: rect, configuration: config) 25 + webView.navigationDelegate = self 26 + webView.autoresizingMask = [.width, .height] 27 + 28 + window = NSWindow( 29 + contentRect: rect, 30 + styleMask: [.titled, .closable, .resizable, .miniaturizable], 31 + backing: .buffered, 32 + defer: false 33 + ) 34 + window.title = "CedarStalk — Sign In" 35 + window.contentView = webView 36 + window.delegate = self 37 + window.center() 38 + window.makeKeyAndOrderFront(nil) 39 + NSApp.activate(ignoringOtherApps: true) 40 + 41 + webView.load(URLRequest(url: signInUrl)) 42 + } 43 + 44 + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { 45 + guard !didComplete, 46 + let host = webView.url?.host, 47 + host.contains(TARGET_HOST) 48 + else { return } 49 + 50 + webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { [weak self] all in 51 + guard let self, !self.didComplete else { return } 52 + let site = all.filter { $0.domain.contains(TARGET_HOST) } 53 + guard site.contains(where: { AUTH_COOKIES.contains($0.name) }) else { return } 54 + 55 + self.didComplete = true 56 + let cookieStr = site.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") 57 + try? cookieStr.write(toFile: self.cookieOutputFile, atomically: true, encoding: .utf8) 58 + NSApp.terminate(nil) 59 + } 60 + } 61 + 62 + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { true } 63 + 64 + func windowWillClose(_: Notification) { 65 + if !didComplete { exit(1) } 66 + } 67 + } 68 + 69 + // Filter out ALL system-injected flags (-psn_XXX, -AppleLanguages, etc.) 70 + // Our args are positional: URL (starts with https) and file path (starts with /) 71 + let args = CommandLine.arguments.dropFirst().filter { !$0.hasPrefix("-") } 72 + 73 + guard args.count >= 2, 74 + let url = URL(string: String(args[0])), 75 + url.scheme != nil 76 + else { 77 + fputs("Usage: auth-browser <url> <cookie-output-file>\n", stderr) 78 + exit(1) 79 + } 80 + 81 + let cookieOutputFile = String(args[1]) 82 + 83 + let app = NSApplication.shared 84 + let delegate = AuthBrowser(url: url, cookieOutputFile: cookieOutputFile) 85 + app.setActivationPolicy(.regular) 86 + app.delegate = delegate 87 + app.run()
+20
assets/cedarstalk.svg
··· 1 + <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <g clip-path="url(#clip0_113_8)"> 3 + <rect width="1024" height="1024" rx="199" fill="#232327"/> 4 + <path d="M625.781 622.752L534.179 662.48C534.179 662.48 557.373 709.887 569.44 728.694C581.508 747.5 597.571 760.664 599.765 783.859C601.96 807.053 590.754 878.36 580.803 897.088C572.261 913.23 562.78 926.708 570.537 937.756C578.295 948.805 622.02 949.51 628.602 938.305C633.617 929.842 635.027 903.043 636.124 893.092C637.221 883.14 642.706 844.039 647.173 813.087C649.994 793.418 656.576 780.567 657.673 767.325C658.77 754.082 637.691 736.53 630.091 723.208C612.538 692.256 625.781 622.752 625.781 622.752Z" fill="#8A5B51"/> 5 + <path d="M720.674 535.068L749.902 554.344C749.902 554.344 791.276 555.441 800.679 516.261C807.966 485.78 800.679 454.122 786.888 463.29C783.596 465.484 770.902 506.858 770.902 506.858L720.674 535.068Z" fill="#FFEAC3"/> 6 + <path d="M313.597 329.296L290.403 331.49C290.403 331.49 279.589 336.27 271.283 338.464C262.742 340.736 249.578 342.225 247.383 348.886C245.189 355.468 245.66 391.278 253.417 402.092C258.902 409.849 262.272 422.465 292.048 421.917C321.825 421.368 363.199 420.271 363.199 420.271L350.504 340.893L313.597 329.296Z" fill="#FFEAC3"/> 7 + <path d="M443.204 271.388C443.204 271.388 425.024 246.548 390.468 250.231C366.333 252.817 343.374 270.84 343.374 270.84C343.374 270.84 319.631 227.272 298.709 210.738C277.787 194.204 232.495 175.476 224.816 185.898C217.059 196.398 249.656 239.418 265.641 260.34C281.627 281.262 309.209 311.116 309.209 311.116C309.209 311.116 300.903 325.456 288.836 331.803C301.53 333.449 317.672 337.68 329.818 355.076C342.669 373.49 351.523 401.778 347.449 420.506C343.844 437.04 323 462.82 325.743 516.261C328.486 569.781 340.083 602.3 350.034 618.834C359.986 635.367 384.199 665.693 384.199 665.693C384.199 665.693 386.393 726.343 385.845 758.392C385.296 790.362 379.262 848.27 373.699 876.401C368.214 904.532 361.553 917.775 354.971 926.081C348.389 934.387 336.4 952.174 356.93 956.954C372.367 960.559 395.091 958.208 400.419 956.563C411.547 953.115 408.49 934.857 407.942 924.905C407.393 914.954 420.636 835.341 423.927 801.724C427.14 769.44 433.487 729.242 435.681 710.514C438.267 688.809 441.245 678.7 445.241 679.249C450.726 679.954 451.667 687.555 451.118 705.734C450.569 724.07 445.946 778.844 443.752 810.266C441.558 841.688 432.939 918.167 431.215 924.279C429.569 930.312 413.349 946.768 415.073 959.775C416.562 970.981 429.961 975.761 447.043 975.761C464.126 975.761 476.036 971.137 477.212 961.813C478.7 949.667 476.271 941.047 475.723 927.805C475.174 914.562 486.772 823.587 488.26 805.564C489.514 790.049 502.208 721.563 504.402 712.708C506.596 703.854 507.615 693.667 509.574 688.965C511.063 685.282 551.34 691.159 585.504 674.626C619.669 658.092 641.218 635.994 641.218 635.994C641.218 635.994 665.509 668.514 677.655 683.402C689.801 698.29 732.82 755.649 734.465 776.101C736.111 796.474 738.305 869.897 738.305 890.271C738.305 910.644 737.757 926.159 736.111 930.547C734.465 934.935 715.189 960.872 720.674 970.275C726.159 979.679 741.439 982.029 758.914 980.227C777.015 978.347 785.086 970.667 787.985 959.305C793.157 939.088 785.007 855.792 786.183 813.322C787.28 770.851 793.548 756.354 789.16 745.306C784.772 734.257 763.223 708.32 756.641 675.252C750.059 642.185 756.093 617.345 756.093 588.117C756.093 558.889 754.447 541.258 754.447 541.258C754.447 541.258 776.544 532.873 785.634 516.418C794.724 499.884 798.015 462.899 789.787 461.801C781.481 460.704 769.727 483.037 764.947 488.287C756.876 497.142 740.656 500.981 740.656 500.981C740.656 500.981 722.477 451.928 668.957 441.977C615.438 432.025 550.634 459.137 516.705 465.719C482.54 472.302 461.54 476.768 456.055 466.816C450.57 456.865 457.7 437.04 469.297 417.764C480.895 398.487 486.772 385.793 488.417 372.55C490.063 359.307 490.219 345.516 490.219 345.516C490.219 345.516 529.948 338.307 554.239 311.352C578.53 284.318 600.001 227.507 607.21 213.167C614.419 198.827 620.453 196.085 617.162 188.327C613.87 180.57 569.206 178.376 541.623 186.133C516.548 193.186 480.973 215.91 466.633 234.089C452.058 252.112 443.204 271.388 443.204 271.388Z" fill="#D17856"/> 8 + <path d="M475.017 307.982C463.89 297.325 465.458 278.911 483.48 256.892C501.503 234.873 523.443 221.238 543.425 213.481C559.567 207.212 581.9 201.1 586.836 205.723C589.109 207.917 575.317 235.813 558.784 262.534C543.582 287.138 524.697 305.71 509.966 311.273C496.958 316.131 483.48 316.053 475.017 307.982Z" fill="#FFEAC3"/> 9 + <path d="M248.794 207.447C246.208 212.227 261.253 230.014 282.175 257.127C298.082 277.735 308.739 292.937 316.732 294.269C321.668 295.131 326.762 290.351 325.038 282.672C321.982 269.429 310.541 253.444 295.261 237.772C277.552 219.749 251.38 202.667 248.794 207.447Z" fill="#FFEAC3"/> 10 + <path d="M411.311 328.747C410.449 341.677 405.434 349.904 393.367 350.845C383.651 351.628 375.423 339.953 375.423 328.199C375.423 316.445 383.651 308.139 393.367 308.139C403.084 308.139 412.173 316.993 411.311 328.747Z" fill="#0A070B"/> 11 + <path d="M256.473 394.412C256.473 394.412 278.257 380.621 278.257 369.337C278.257 360.483 269.794 353.274 261.175 348.102C256.395 345.281 249.343 343.4 245.973 347.789C242.682 352.177 237.981 363.774 241.037 382.815C244.093 401.856 253.966 405.461 253.966 405.461C253.966 405.461 248.481 415.413 282.645 414.551C316.81 413.689 317.359 405.148 316.575 400.211C315.713 395.274 295.026 395.509 284.056 397.155C267.835 399.662 256.473 394.412 256.473 394.412Z" fill="#2C2A32"/> 12 + <path d="M420.558 263.709C420.558 263.709 438.189 221.238 442.107 204.939C445.084 192.324 445.398 169.678 445.711 156.122C445.789 151.028 442.42 146.718 442.655 138.491C442.968 129.166 448.062 119.841 460.286 119.449C471.648 119.136 478.23 127.207 478.779 136.297C479.327 145.7 474.939 150.636 474.626 156.122C473.764 170.775 472.118 199.689 472.118 199.689C472.118 199.689 494.451 178.767 496.958 172.42C499.466 166.073 499.701 134.103 499.701 127.755C499.701 121.408 499.701 112.554 499.701 112.554C499.701 112.554 493.197 103.307 493.354 95.1579C493.432 88.6541 498.29 78.8592 509.104 78.6241C520.779 78.3107 525.716 85.9899 526.5 94.0609C527.205 102.132 523.208 109.498 523.208 109.498L522.111 143.427C522.111 143.427 534.492 127.129 539.507 120.233C544.444 113.337 550.007 102.289 550.007 102.289C550.007 102.289 547.186 93.1206 548.362 84.1093C549.459 75.9599 554.944 67.0269 566.306 66.7135C577.355 66.4784 583.388 75.0196 583.388 84.1093C583.388 97.9005 575.082 107.304 575.082 107.304C575.082 107.304 554.709 147.267 538.959 167.954C527.518 182.999 519.682 194.126 514.667 197.182C509.731 200.238 478.23 217.869 463.89 239.104C454.722 252.66 443.204 271.388 443.204 271.388C443.204 271.388 437.797 279.302 427.767 275.541C419.774 272.485 420.558 263.709 420.558 263.709Z" fill="#BD9178"/> 13 + <path d="M230.301 95.4714C230.301 95.4714 246.052 135.435 257.022 157.219C267.992 179.003 284.291 201.335 284.291 201.335C284.291 201.335 306.78 218.182 314.068 226.958C327.31 242.944 342.512 272.485 342.512 272.485L360.613 259.791L344.471 231.973C344.471 231.973 344.158 203.529 344.158 192.559C344.158 183.469 344.471 167.484 344.471 167.484C344.471 167.484 349.878 159.099 350.27 150.636C350.74 141.077 345.646 131.36 334.284 130.811C323.549 130.263 317.437 139.588 316.653 147.894C315.792 156.2 320.258 163.644 320.258 163.644L319.161 208.622C319.161 208.622 313.127 197.339 310.855 194.283C308.582 191.227 303.254 178.611 302 176.338C299.493 171.637 304.508 132.222 304.508 125.875C304.508 119.528 306.153 101.035 306.153 101.035C306.153 101.035 311.482 92.9639 311.403 83.9526C311.325 75.8816 306.545 66.9486 296.75 66.2434C285.702 65.3814 279.903 73.2957 278.571 80.5831C276.925 89.5161 280.765 97.117 280.765 97.117C280.765 97.117 279.041 137.315 276.377 136.532C273.321 135.67 254.828 87.4788 254.828 87.4788C254.828 87.4788 259.059 77.4488 255.925 64.8329C253.026 53.1574 243.544 45.7916 229.988 49.6312C216.353 53.5492 213.689 67.8106 218.156 78.8592C222.152 88.8892 230.301 95.4714 230.301 95.4714Z" fill="#BD9178"/> 14 + </g> 15 + <defs> 16 + <clipPath id="clip0_113_8"> 17 + <rect width="1024" height="1024" rx="199" fill="white"/> 18 + </clipPath> 19 + </defs> 20 + </svg>
assets/icon.png

This is a binary file and will not be displayed.

+255
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "cedarstalk", 7 + "dependencies": { 8 + "@raycast/api": "^1.93.2", 9 + }, 10 + "devDependencies": { 11 + "@raycast/utils": "^1.17.0", 12 + "@types/node": "^22.13.10", 13 + "@types/react": "^19.0.10", 14 + "typescript": "^5.8.2", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 20 + 21 + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 22 + 23 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 24 + 25 + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 26 + 27 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 28 + 29 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 30 + 31 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 32 + 33 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 34 + 35 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 36 + 37 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 38 + 39 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 40 + 41 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 42 + 43 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 44 + 45 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 46 + 47 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 48 + 49 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 50 + 51 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 52 + 53 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 54 + 55 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 56 + 57 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 58 + 59 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 60 + 61 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 62 + 63 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 64 + 65 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 66 + 67 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 68 + 69 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 70 + 71 + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], 72 + 73 + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], 74 + 75 + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], 76 + 77 + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], 78 + 79 + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], 80 + 81 + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], 82 + 83 + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], 84 + 85 + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], 86 + 87 + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], 88 + 89 + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], 90 + 91 + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], 92 + 93 + "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="], 94 + 95 + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], 96 + 97 + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], 98 + 99 + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], 100 + 101 + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], 102 + 103 + "@oclif/core": ["@oclif/core@4.8.4", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^10.2.4", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-UTAqwXJJyRvLBvosL+1uPZYSpr8lEHgUb/EVGbPXo5WZqUIBHfJ0sR2bkBEsrj00/ar4IegKxx4YK0wn2c8SQg=="], 104 + 105 + "@oclif/plugin-autocomplete": ["@oclif/plugin-autocomplete@3.2.40", "", { "dependencies": { "@oclif/core": "^4", "ansis": "^3.16.0", "debug": "^4.4.1", "ejs": "^3.1.10" } }, "sha512-HCfDuUV3l5F5Wz7SKkaoFb+OMQ5vKul8zvsPNgI0QbZcQuGHmn3svk+392wSfXboyA1gq8kzEmKPAoQK6r6UNw=="], 106 + 107 + "@oclif/plugin-help": ["@oclif/plugin-help@6.2.37", "", { "dependencies": { "@oclif/core": "^4" } }, "sha512-5N/X/FzlJaYfpaHwDC0YHzOzKDWa41s9t+4FpCDu4f9OMReds4JeNBaaWk9rlIzdKjh2M6AC5Q18ORfECRkHGA=="], 108 + 109 + "@oclif/plugin-not-found": ["@oclif/plugin-not-found@3.2.74", "", { "dependencies": { "@inquirer/prompts": "^7.10.1", "@oclif/core": "^4.8.0", "ansis": "^3.17.0", "fast-levenshtein": "^3.0.0" } }, "sha512-6RD/EuIUGxAYR45nMQg+nw+PqwCXUxkR6Eyn+1fvbVjtb9d+60OPwB77LCRUI4zKNI+n0LOFaMniEdSpb+A7kQ=="], 110 + 111 + "@raycast/api": ["@raycast/api@1.104.9", "", { "dependencies": { "@oclif/core": "^4.5.4", "@oclif/plugin-autocomplete": "^3.2.35", "@oclif/plugin-help": "^6.2.33", "@oclif/plugin-not-found": "^3.2.68", "@types/node": "22.13.10", "@types/react": "19.0.10", "esbuild": "^0.25.10", "react": "19.0.0" }, "peerDependencies": { "react-devtools": "6.1.1" }, "optionalPeers": ["react-devtools"], "bin": { "ray": "bin/run.js" } }, "sha512-Y75OUUhCHCag/ZWd/CB4avuNHNUQWwlbuO4pgadtBuvHOlpKVE4JZCN7nWbO4Wd/zkyfmToo3ZT53AFkugsBcQ=="], 112 + 113 + "@raycast/utils": ["@raycast/utils@1.19.1", "", { "dependencies": { "cross-fetch": "^3.1.6", "dequal": "^2.0.3", "object-hash": "^3.0.0", "signal-exit": "^4.0.2", "stream-chain": "^2.2.5", "stream-json": "^1.8.0" }, "peerDependencies": { "@raycast/api": ">=1.69.0" } }, "sha512-/udUGcTZCgZZwzesmjBkqG5naQZTD/ZLHbqRwkWcF+W97vf9tr9raxKyQjKsdZ17OVllw2T3sHBQsVUdEmCm2g=="], 114 + 115 + "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], 116 + 117 + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], 118 + 119 + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], 120 + 121 + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 122 + 123 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 124 + 125 + "ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], 126 + 127 + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], 128 + 129 + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], 130 + 131 + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], 132 + 133 + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], 134 + 135 + "clean-stack": ["clean-stack@3.0.1", "", { "dependencies": { "escape-string-regexp": "4.0.0" } }, "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg=="], 136 + 137 + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], 138 + 139 + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], 140 + 141 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 142 + 143 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 144 + 145 + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], 146 + 147 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 148 + 149 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 150 + 151 + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 152 + 153 + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], 154 + 155 + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 156 + 157 + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 158 + 159 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 160 + 161 + "fast-levenshtein": ["fast-levenshtein@3.0.0", "", { "dependencies": { "fastest-levenshtein": "^1.0.7" } }, "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ=="], 162 + 163 + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], 164 + 165 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 166 + 167 + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], 168 + 169 + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], 170 + 171 + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 172 + 173 + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], 174 + 175 + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], 176 + 177 + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], 178 + 179 + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 180 + 181 + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], 182 + 183 + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], 184 + 185 + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], 186 + 187 + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], 188 + 189 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 190 + 191 + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], 192 + 193 + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 194 + 195 + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], 196 + 197 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 198 + 199 + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 200 + 201 + "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], 202 + 203 + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 204 + 205 + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 206 + 207 + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 208 + 209 + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], 210 + 211 + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], 212 + 213 + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 214 + 215 + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 216 + 217 + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], 218 + 219 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 220 + 221 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 222 + 223 + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], 224 + 225 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 226 + 227 + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 228 + 229 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 230 + 231 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 232 + 233 + "widest-line": ["widest-line@3.1.0", "", { "dependencies": { "string-width": "^4.0.0" } }, "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg=="], 234 + 235 + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], 236 + 237 + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 238 + 239 + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], 240 + 241 + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], 242 + 243 + "@raycast/api/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], 244 + 245 + "@raycast/api/@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], 246 + 247 + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], 248 + 249 + "@raycast/api/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 250 + 251 + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 252 + 253 + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 254 + } 255 + }
+33
package.json
··· 1 + { 2 + "$schema": "https://www.raycast.com/schemas/extension.json", 3 + "name": "cedarstalk", 4 + "title": "CedarStalk", 5 + "description": "Search the Cedarville University student/staff directory", 6 + "icon": "icon.png", 7 + "author": "kierank", 8 + "license": "MIT", 9 + "commands": [ 10 + { 11 + "name": "search-directory", 12 + "title": "Search Directory", 13 + "description": "Search the Cedarville directory by name", 14 + "mode": "view" 15 + } 16 + ], 17 + "dependencies": { 18 + "@raycast/api": "^1.93.2" 19 + }, 20 + "devDependencies": { 21 + "@raycast/utils": "^1.17.0", 22 + "@types/node": "^22.13.10", 23 + "@types/react": "^19.0.10", 24 + "typescript": "^5.8.2" 25 + }, 26 + "scripts": { 27 + "build": "ray build", 28 + "dev": "ray develop", 29 + "fix-lint": "ray lint --fix", 30 + "lint": "ray lint", 31 + "publish": "npx @raycast/api@latest publish" 32 + } 33 + }
+24
raycast-env.d.ts
··· 1 + /// <reference types="@raycast/api"> 2 + 3 + /* 🚧 🚧 🚧 4 + * This file is auto-generated from the extension's manifest. 5 + * Do not modify manually. Instead, update the `package.json` file. 6 + * 🚧 🚧 🚧 */ 7 + 8 + /* eslint-disable @typescript-eslint/ban-types */ 9 + 10 + type ExtensionPreferences = {} 11 + 12 + /** Preferences accessible in all the extension's commands */ 13 + declare type Preferences = ExtensionPreferences 14 + 15 + declare namespace Preferences { 16 + /** Preferences accessible in the `search-directory` command */ 17 + export type SearchDirectory = ExtensionPreferences & {} 18 + } 19 + 20 + declare namespace Arguments { 21 + /** Arguments passed to the `search-directory` command */ 22 + export type SearchDirectory = {} 23 + } 24 +
+185
src/api.ts
··· 1 + const BASE_URL = "https://selfservice.cedarville.edu"; 2 + 3 + export interface DirectoryPerson { 4 + Id: string; 5 + Username: string; 6 + FirstName: string; 7 + LastName: string; 8 + MiddleName: string | null; 9 + Nickname: string | null; 10 + AddressCity: string | null; 11 + AddressState: string | null; 12 + AddressCountry: string | null; 13 + DepartmentDescription: string | null; 14 + Title: string | null; 15 + OfficeBuildingCode: string | null; 16 + OfficeBuildingName: string | null; 17 + OfficeRoom: string | null; 18 + OfficePhone: string | null; 19 + DormCode: string | null; 20 + DormName: string | null; 21 + DormRoom: string | null; 22 + StudentType: string | null; 23 + StudentClass: string | null; 24 + studentWorker: boolean | null; 25 + empInactive: boolean | null; 26 + PhotoUrl: string | null; 27 + } 28 + 29 + export class AuthRequiredError extends Error { 30 + constructor(public readonly signInUrl: string) { 31 + super("Authentication required"); 32 + this.name = "AuthRequiredError"; 33 + } 34 + } 35 + 36 + function makeHeaders(cookie?: string): Record<string, string> { 37 + const headers: Record<string, string> = { 38 + accept: "*/*", 39 + "accept-language": "en-US,en;q=0.9", 40 + referer: `${BASE_URL}/cedarinfo/directory`, 41 + "user-agent": 42 + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", 43 + }; 44 + if (cookie) headers["cookie"] = cookie; 45 + return headers; 46 + } 47 + 48 + export interface ScheduleItem { 49 + title: string; 50 + description: string; 51 + startTime: string; 52 + endTime: string; 53 + day: string; 54 + type: string; 55 + } 56 + 57 + export interface Term { 58 + code: string; 59 + desc: string; 60 + start: string; 61 + end: string; 62 + } 63 + 64 + export interface PersonInfo { 65 + faculty: { 66 + isFaculty: boolean; 67 + facultyDepts: { code: string; description: string; division: string }[]; 68 + scheduleItems: ScheduleItem[]; 69 + term: { key: string; description: string }; 70 + }; 71 + student: { 72 + isStudent: boolean; 73 + scheduleItems: ScheduleItem[]; 74 + programs: string[]; 75 + majors: { code: string; description: string }[]; 76 + minors: { code: string; description: string }[]; 77 + concentrations: { code: string; description: string }[]; 78 + advisors: { id: string; name: string }[]; 79 + term: { key: string; description: string }; 80 + }; 81 + } 82 + 83 + export interface Department { 84 + code: string; 85 + description: string; 86 + } 87 + 88 + export interface Population { 89 + code: number; 90 + desc: string; 91 + } 92 + 93 + export async function getDepartments(cookie?: string): Promise<Department[]> { 94 + const url = `${BASE_URL}/CedarInfo/Directory/DepartmentJson`; 95 + const res = await fetch(url, { headers: makeHeaders(cookie) }); 96 + if (!res.ok || !res.headers.get("content-type")?.includes("json")) return []; 97 + try { 98 + const data = await res.json(); 99 + return Array.isArray(data) ? data : []; 100 + } catch { 101 + return []; 102 + } 103 + } 104 + 105 + export async function getPopulations(cookie?: string): Promise<Population[]> { 106 + const url = `${BASE_URL}/CedarInfo/Directory/PopulationsJson`; 107 + const res = await fetch(url, { headers: makeHeaders(cookie) }); 108 + if (!res.ok || !res.headers.get("content-type")?.includes("json")) return []; 109 + try { 110 + const data = await res.json(); 111 + return Array.isArray(data) ? data : []; 112 + } catch { 113 + return []; 114 + } 115 + } 116 + 117 + export async function getPersonTerms( 118 + id: string, 119 + cookie: string, 120 + ): Promise<Term[]> { 121 + const url = `${BASE_URL}/CedarInfo/Json/GetTerms?id=${id}&past=5&future=2&summer=true`; 122 + const res = await fetch(url, { headers: makeHeaders(cookie) }); 123 + if (!res.ok) return []; 124 + const data = await res.json(); 125 + return Array.isArray(data) ? data : []; 126 + } 127 + 128 + export async function getPersonInfo( 129 + id: string, 130 + term: string, 131 + cookie: string, 132 + ): Promise<PersonInfo | null> { 133 + const url = `${BASE_URL}/CedarInfo/Info/Json?id=${id}&term=${term}`; 134 + const res = await fetch(url, { headers: makeHeaders(cookie) }); 135 + if (!res.ok) return null; 136 + const data = await res.json(); 137 + return data as PersonInfo; 138 + } 139 + 140 + export async function searchDirectory( 141 + firstName: string, 142 + lastName: string, 143 + cookie?: string, 144 + options?: { department?: string; population?: number }, 145 + ): Promise<DirectoryPerson[]> { 146 + const params = new URLSearchParams(); 147 + if (firstName) params.set("FirstNameSearch", firstName); 148 + if (lastName) params.set("LastNameSearch", lastName); 149 + if (options?.department) params.set("Department", options.department); 150 + if (options?.population != null) 151 + params.set("PopulationSearch", String(options.population)); 152 + 153 + const apiUrl = `${BASE_URL}/CedarInfo/Directory/SearchResultsJson?${params.toString()}`; 154 + const response = await fetch(apiUrl, { headers: makeHeaders(cookie) }); 155 + 156 + // Unauthenticated: server redirects us to SSO. fetch follows by default, 157 + // so we end up at a non-selfservice URL. 158 + const landedOutside = !response.url.includes("selfservice.cedarville.edu"); 159 + if (landedOutside || response.status === 401 || response.status === 403) { 160 + const signInUrl = landedOutside 161 + ? response.url 162 + : `${BASE_URL}/cedarinfo/directory`; 163 + throw new AuthRequiredError(signInUrl); 164 + } 165 + 166 + if (!response.ok) { 167 + throw new Error( 168 + `Request failed: ${response.status} ${response.statusText}`, 169 + ); 170 + } 171 + 172 + const data = await response.json(); 173 + 174 + // Server can also return JSON with a redirect URL instead of an array 175 + if (!Array.isArray(data)) { 176 + const signInUrl = 177 + (data as Record<string, unknown>)?.signInUrl ?? 178 + (data as Record<string, unknown>)?.SignInUrl ?? 179 + (data as Record<string, unknown>)?.redirectUrl; 180 + if (typeof signInUrl === "string") throw new AuthRequiredError(signInUrl); 181 + throw new AuthRequiredError(`${BASE_URL}/cedarinfo/directory`); 182 + } 183 + 184 + return data as DirectoryPerson[]; 185 + }
+116
src/auth.ts
··· 1 + import { environment, LocalStorage } from "@raycast/api"; 2 + import { exec, spawn } from "child_process"; 3 + import { 4 + access, 5 + mkdir, 6 + readFile, 7 + symlink, 8 + unlink, 9 + writeFile, 10 + } from "fs/promises"; 11 + import * as os from "os"; 12 + import * as path from "path"; 13 + import { promisify } from "util"; 14 + 15 + const execAsync = promisify(exec); 16 + const COOKIE_KEY = "session_cookie"; 17 + 18 + // ─── Cookie storage ──────────────────────────────────────────────────────── 19 + 20 + export async function getStoredCookie(): Promise<string | undefined> { 21 + return LocalStorage.getItem<string>(COOKIE_KEY); 22 + } 23 + 24 + export async function storeCookie(cookie: string): Promise<void> { 25 + await LocalStorage.setItem(COOKIE_KEY, cookie); 26 + } 27 + 28 + export async function clearCookie(): Promise<void> { 29 + await LocalStorage.removeItem(COOKIE_KEY); 30 + } 31 + 32 + // ─── Auth browser ────────────────────────────────────────────────────────── 33 + 34 + // Opens an isolated WKWebView window via a temporary .app bundle so macOS 35 + // grants it proper window-server access. Cookie is returned through a temp file. 36 + export async function launchAuthBrowser(signInUrl: string): Promise<string> { 37 + const binaryPath = await ensureBinary(); 38 + const appBundle = await ensureAppBundle(binaryPath); 39 + const cookieFile = path.join(os.tmpdir(), "cedarstalk-cookie.txt"); 40 + 41 + await unlink(cookieFile).catch(() => {}); 42 + 43 + // Use spawn (not execAsync) so the SAML URL isn't shell-interpreted 44 + await new Promise<void>((resolve, reject) => { 45 + const proc = spawn( 46 + "open", 47 + ["-n", "-W", appBundle, "--args", signInUrl, cookieFile], 48 + { 49 + stdio: "ignore", 50 + }, 51 + ); 52 + proc.on("close", (code) => { 53 + // open -W exits 0 whether the app succeeded or cancelled; 54 + // we detect success by whether the cookie file was written 55 + resolve(); 56 + }); 57 + proc.on("error", reject); 58 + }); 59 + 60 + const cookie = await readFile(cookieFile, "utf-8") 61 + .then((s) => s.trim()) 62 + .catch(() => ""); 63 + await unlink(cookieFile).catch(() => {}); 64 + 65 + if (!cookie) throw new Error("Sign-in cancelled"); 66 + return cookie; 67 + } 68 + 69 + async function ensureBinary(): Promise<string> { 70 + const swiftSrc = path.join(environment.assetsPath, "auth-browser.swift"); 71 + const binaryPath = path.join(environment.supportPath, "auth-browser"); 72 + 73 + try { 74 + await access(binaryPath); 75 + return binaryPath; 76 + } catch { 77 + await mkdir(environment.supportPath, { recursive: true }); 78 + // Compile with optimisations to avoid debug-mode assertion traps 79 + await execAsync(`swiftc -O "${swiftSrc}" -o "${binaryPath}"`); 80 + // Ad-hoc sign so macOS treats it as a trusted binary 81 + await execAsync(`codesign --sign - --force "${binaryPath}"`); 82 + return binaryPath; 83 + } 84 + } 85 + 86 + async function ensureAppBundle(binaryPath: string): Promise<string> { 87 + const appDir = path.join(os.tmpdir(), "CedarStalkAuth.app"); 88 + const macosDir = path.join(appDir, "Contents", "MacOS"); 89 + const plistPath = path.join(appDir, "Contents", "Info.plist"); 90 + const bundledBinary = path.join(macosDir, "CedarStalkAuth"); 91 + 92 + await mkdir(macosDir, { recursive: true }); 93 + 94 + // Always recreate the symlink so it tracks the current binary path 95 + await unlink(bundledBinary).catch(() => {}); 96 + await symlink(binaryPath, bundledBinary); 97 + 98 + await writeFile( 99 + plistPath, 100 + `<?xml version="1.0" encoding="UTF-8"?> 101 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 102 + <plist version="1.0"> 103 + <dict> 104 + <key>CFBundlePackageType</key><string>APPL</string> 105 + <key>CFBundleExecutable</key><string>CedarStalkAuth</string> 106 + <key>CFBundleIdentifier</key><string>sh.dunkirk.cedarstalk.auth</string> 107 + <key>CFBundleName</key><string>CedarStalk Auth</string> 108 + <key>NSPrincipalClass</key><string>NSApplication</string> 109 + <key>NSHighResolutionCapable</key><true/> 110 + <key>LSMinimumSystemVersion</key><string>13.0</string> 111 + </dict> 112 + </plist>`, 113 + ); 114 + 115 + return appDir; 116 + }
+73
src/cache.ts
··· 1 + import { LocalStorage } from "@raycast/api"; 2 + import { DirectoryPerson } from "./api"; 3 + 4 + const CACHE_KEY = "directory_cache"; 5 + 6 + interface CacheStore { 7 + [id: string]: DirectoryPerson; 8 + } 9 + 10 + let memoryCache: CacheStore | null = null; 11 + 12 + export async function loadCache(): Promise<CacheStore> { 13 + if (memoryCache) return memoryCache; 14 + const raw = await LocalStorage.getItem<string>(CACHE_KEY); 15 + memoryCache = raw ? (JSON.parse(raw) as CacheStore) : {}; 16 + return memoryCache; 17 + } 18 + 19 + export async function mergePeopleIntoCache( 20 + people: DirectoryPerson[], 21 + ): Promise<void> { 22 + const cache = await loadCache(); 23 + for (const p of people) { 24 + cache[p.Id] = p; 25 + } 26 + memoryCache = cache; 27 + await LocalStorage.setItem(CACHE_KEY, JSON.stringify(cache)); 28 + } 29 + 30 + export async function searchCache(query: string): Promise<DirectoryPerson[]> { 31 + const cache = await loadCache(); 32 + const all = Object.values(cache); 33 + if (!query.trim()) return all; 34 + 35 + const terms = query.toLowerCase().split(/\s+/); 36 + const scored = all.map((p) => ({ p, score: scoreMatch(p, terms) })); 37 + return scored 38 + .filter((x) => x.score > 0) 39 + .sort((a, b) => b.score - a.score) 40 + .map((x) => x.p); 41 + } 42 + 43 + function scoreMatch(p: DirectoryPerson, terms: string[]): number { 44 + const fields = [ 45 + p.FirstName?.toLowerCase(), 46 + p.LastName?.toLowerCase(), 47 + p.Nickname?.toLowerCase(), 48 + p.Username?.toLowerCase(), 49 + p.DormName?.toLowerCase(), 50 + p.DepartmentDescription?.toLowerCase(), 51 + ].filter(Boolean) as string[]; 52 + 53 + let score = 0; 54 + for (const term of terms) { 55 + let hit = false; 56 + for (const field of fields) { 57 + if (field.startsWith(term)) { 58 + score += 2; 59 + hit = true; 60 + } else if (field.includes(term)) { 61 + score += 1; 62 + hit = true; 63 + } 64 + } 65 + if (!hit) return 0; // all terms must match something 66 + } 67 + return score; 68 + } 69 + 70 + export async function getCacheSize(): Promise<number> { 71 + const cache = await loadCache(); 72 + return Object.keys(cache).length; 73 + }
+48
src/images.ts
··· 1 + import { environment } from "@raycast/api"; 2 + import { access, mkdir, writeFile } from "fs/promises"; 3 + import * as path from "path"; 4 + 5 + const BASE_URL = "https://selfservice.cedarville.edu"; 6 + const PHOTO_DIR = path.join(environment.supportPath, "photos"); 7 + 8 + // In-memory set to avoid duplicate in-flight fetches 9 + const inflight = new Set<string>(); 10 + 11 + export async function getCachedPhotoPath( 12 + personId: string, 13 + photoUrl: string, 14 + cookie: string, 15 + ): Promise<string | null> { 16 + await mkdir(PHOTO_DIR, { recursive: true }); 17 + const filePath = path.join(PHOTO_DIR, `${personId}.jpg`); 18 + 19 + try { 20 + await access(filePath); 21 + return filePath; 22 + } catch { 23 + if (inflight.has(personId)) return null; 24 + inflight.add(personId); 25 + try { 26 + const fullUrl = photoUrl.startsWith("http") 27 + ? photoUrl 28 + : `${BASE_URL}${photoUrl}`; 29 + const headers: Record<string, string> = { 30 + "user-agent": 31 + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", 32 + }; 33 + // Only send the auth cookie for selfservice requests 34 + if (!photoUrl.startsWith("http")) { 35 + headers["cookie"] = cookie; 36 + headers["referer"] = `${BASE_URL}/cedarinfo/directory`; 37 + } 38 + const res = await fetch(fullUrl, { headers }); 39 + if (!res.ok) return null; 40 + await writeFile(filePath, Buffer.from(await res.arrayBuffer())); 41 + return filePath; 42 + } catch { 43 + return null; 44 + } finally { 45 + inflight.delete(personId); 46 + } 47 + } 48 + }
+804
src/search-directory.tsx
··· 1 + import { 2 + Action, 3 + ActionPanel, 4 + Color, 5 + Detail, 6 + Icon, 7 + Image, 8 + List, 9 + showToast, 10 + Toast, 11 + } from "@raycast/api"; 12 + import { readFile } from "fs/promises"; 13 + import { useEffect, useRef, useState } from "react"; 14 + import { 15 + AuthRequiredError, 16 + type Department, 17 + type DirectoryPerson, 18 + type PersonInfo, 19 + type Population, 20 + getDepartments, 21 + getPersonInfo, 22 + getPersonTerms, 23 + getPopulations, 24 + searchDirectory, 25 + } from "./api"; 26 + import { 27 + clearCookie, 28 + getStoredCookie, 29 + launchAuthBrowser, 30 + storeCookie, 31 + } from "./auth"; 32 + import { getCacheSize, mergePeopleIntoCache, searchCache } from "./cache"; 33 + import { getCachedPhotoPath } from "./images"; 34 + 35 + type AuthState = 36 + | { kind: "loading" } 37 + | { kind: "ready"; cookie: string } 38 + | { kind: "sign-in"; signInUrl: string } 39 + | { kind: "signing-in" }; 40 + 41 + const CLASS_LABELS: Record<string, string> = { 42 + FR: "Freshman", 43 + SO: "Sophomore", 44 + JR: "Junior", 45 + SR: "Senior", 46 + GR: "Graduate", 47 + GS: "Graduate Student", 48 + HS: "High School", 49 + P1: "Pharmacy Year 1", 50 + P2: "Pharmacy Year 2", 51 + P3: "Pharmacy Year 3", 52 + P4: "Pharmacy Year 4", 53 + }; 54 + 55 + const TYPE_LABELS: Record<string, string> = { 56 + UG: "Undergraduate", 57 + GR: "Graduate", 58 + GS: "Graduate Student", 59 + DE: "Dual Enrollment", 60 + P1: "Pharmacy Year 1", 61 + P2: "Pharmacy Year 2", 62 + P3: "Pharmacy Year 3", 63 + P4: "Pharmacy Year 4", 64 + }; 65 + 66 + function displayName(person: DirectoryPerson, showLegal = false): string { 67 + const nickname = 68 + person.Nickname && person.Nickname !== person.FirstName 69 + ? person.Nickname 70 + : null; 71 + const first = 72 + showLegal && nickname 73 + ? `${nickname} (${person.FirstName})` 74 + : (nickname ?? person.FirstName); 75 + const middle = person.MiddleName ? ` ${person.MiddleName}` : ""; 76 + return `${first}${middle} ${person.LastName}`; 77 + } 78 + 79 + function email(username: string): string { 80 + return `${username}@cedarville.edu`; 81 + } 82 + 83 + function formatPhone(phone: string): string { 84 + // 4-digit campus extensions → "ext. XXXX" 85 + return /^\d{4}$/.test(phone.trim()) ? `ext. ${phone.trim()}` : phone; 86 + } 87 + 88 + function parseSearchQuery(query: string): { 89 + firstName: string; 90 + lastName: string; 91 + } { 92 + const parts = query.trim().split(/\s+/); 93 + if (parts.length === 1) return { firstName: parts[0], lastName: "" }; 94 + const lastName = parts.pop() ?? ""; 95 + return { firstName: parts.join(" "), lastName }; 96 + } 97 + 98 + // ─── Person detail view ──────────────────────────────────────────────────── 99 + 100 + function formatTime(t: string): string { 101 + const [h, m] = t.split(":"); 102 + const hour = parseInt(h, 10); 103 + return `${hour > 12 ? hour - 12 : hour || 12}:${m} ${hour >= 12 ? "PM" : "AM"}`; 104 + } 105 + 106 + function PersonDetail({ 107 + person, 108 + photoPath, 109 + cookie, 110 + onSignOut, 111 + }: { 112 + person: DirectoryPerson; 113 + photoPath: string | null; 114 + cookie: string; 115 + onSignOut: () => void; 116 + }) { 117 + const name = displayName(person, true); 118 + const [photoDataUrl, setPhotoDataUrl] = useState<string | null>(null); 119 + const [info, setInfo] = useState<PersonInfo | null>(null); 120 + 121 + useEffect(() => { 122 + if (!photoPath) return; 123 + readFile(photoPath) 124 + .then((buf) => 125 + setPhotoDataUrl(`data:image/jpeg;base64,${buf.toString("base64")}`), 126 + ) 127 + .catch(() => {}); 128 + }, [photoPath]); 129 + 130 + useEffect(() => { 131 + (async () => { 132 + const terms = await getPersonTerms(person.Id, cookie); 133 + if (!terms.length) return; 134 + const now = Date.now(); 135 + const current = 136 + terms.find((t) => { 137 + const start = t.start ? new Date(t.start).getTime() : 0; 138 + const end = t.end ? new Date(t.end).getTime() : Infinity; 139 + return now >= start && now <= end; 140 + }) ?? terms[0]; 141 + const result = await getPersonInfo(person.Id, current.code, cookie); 142 + if (result) setInfo(result); 143 + })(); 144 + }, [person.Id, cookie]); 145 + 146 + // Photo full-width at top, name + italic tags below 147 + const md: string[] = []; 148 + if (photoDataUrl) md.push(`![Photo](${photoDataUrl})`); 149 + md.push(`# ${name}`); 150 + const isStaff = !person.StudentType || !!(person.Title?.trim() && person.OfficeBuildingCode); 151 + const tags: string[] = []; 152 + if (isStaff) { 153 + tags.push("Staff"); 154 + } else if (person.StudentType === "DE") { 155 + tags.push("Dual Enrollment"); 156 + } else { 157 + if ( 158 + person.StudentClass && 159 + CLASS_LABELS[person.StudentClass] && 160 + person.StudentClass !== "GS" && 161 + person.StudentClass !== "HS" && 162 + person.StudentType !== null 163 + ) 164 + tags.push(CLASS_LABELS[person.StudentClass]); 165 + if (person.StudentType) 166 + tags.push(TYPE_LABELS[person.StudentType] ?? person.StudentType); 167 + } 168 + if (person.studentWorker) tags.push("Student Worker"); 169 + if (person.Title?.trim()) tags.push(person.Title.trim()); 170 + if (tags.length) md.push(`*${tags.join(" · ")}*`); 171 + 172 + const scheduleItems = info?.faculty?.isFaculty 173 + ? info.faculty.scheduleItems 174 + : info?.student?.isStudent 175 + ? info.student.scheduleItems 176 + : []; 177 + const termDesc = info?.faculty?.isFaculty 178 + ? info.faculty.term?.description 179 + : info?.student?.term?.description; 180 + 181 + if (scheduleItems.length) { 182 + md.push(`## Schedule${termDesc ? ` — ${termDesc}` : ""}`); 183 + for (const item of scheduleItems) { 184 + md.push( 185 + `**${item.title}** — ${item.description} \n${item.day} ${formatTime(item.startTime)}–${formatTime(item.endTime)}`, 186 + ); 187 + } 188 + } 189 + 190 + return ( 191 + <Detail 192 + isLoading={!info} 193 + markdown={md.join("\n\n")} 194 + navigationTitle={name} 195 + metadata={ 196 + <Detail.Metadata> 197 + {person.Username && ( 198 + <Detail.Metadata.Label 199 + title="Email" 200 + text={email(person.Username)} 201 + /> 202 + )} 203 + {person.DepartmentDescription && ( 204 + <Detail.Metadata.Label 205 + title="Department" 206 + text={person.DepartmentDescription} 207 + /> 208 + )} 209 + {(isStaff || 210 + person.StudentClass || 211 + person.studentWorker) && <Detail.Metadata.Separator />} 212 + {isStaff ? ( 213 + <Detail.Metadata.TagList title="Role"> 214 + <Detail.Metadata.TagList.Item text="Staff" color={Color.Green} /> 215 + </Detail.Metadata.TagList> 216 + ) : person.StudentType === "DE" ? ( 217 + <Detail.Metadata.TagList title="Program"> 218 + <Detail.Metadata.TagList.Item 219 + text="Dual Enrollment" 220 + color={Color.Orange} 221 + /> 222 + </Detail.Metadata.TagList> 223 + ) : ( 224 + <> 225 + {person.StudentClass && 226 + CLASS_LABELS[person.StudentClass] && 227 + person.StudentClass !== "GS" && 228 + person.StudentClass !== "HS" && 229 + person.StudentType !== null && ( 230 + <Detail.Metadata.TagList title="Year"> 231 + <Detail.Metadata.TagList.Item 232 + text={CLASS_LABELS[person.StudentClass]} 233 + color={Color.Blue} 234 + /> 235 + </Detail.Metadata.TagList> 236 + )} 237 + {person.StudentType && ( 238 + <Detail.Metadata.Label 239 + title="Program" 240 + text={TYPE_LABELS[person.StudentType] ?? person.StudentType} 241 + /> 242 + )} 243 + </> 244 + )} 245 + {person.studentWorker && ( 246 + <Detail.Metadata.TagList title="Role"> 247 + <Detail.Metadata.TagList.Item 248 + text="Student Worker" 249 + color={Color.Yellow} 250 + /> 251 + </Detail.Metadata.TagList> 252 + )} 253 + {(person.DormName || 254 + person.OfficeBuildingName || 255 + person.OfficePhone) && <Detail.Metadata.Separator />} 256 + {person.DormName && ( 257 + <Detail.Metadata.Label 258 + title="Dorm" 259 + text={ 260 + person.DormRoom 261 + ? `${person.DormName}, Room ${person.DormRoom}` 262 + : person.DormName 263 + } 264 + /> 265 + )} 266 + {person.OfficeBuildingName && ( 267 + <Detail.Metadata.Label 268 + title="Office" 269 + text={ 270 + person.OfficeRoom 271 + ? `${person.OfficeBuildingName}, Room ${person.OfficeRoom}` 272 + : person.OfficeBuildingName 273 + } 274 + /> 275 + )} 276 + {person.OfficePhone && ( 277 + <Detail.Metadata.Label 278 + title="Phone" 279 + text={formatPhone(person.OfficePhone)} 280 + /> 281 + )} 282 + {(person.AddressCity || person.AddressState) && ( 283 + <Detail.Metadata.Separator /> 284 + )} 285 + {(person.AddressCity || person.AddressState) && ( 286 + <Detail.Metadata.Label 287 + title="Hometown" 288 + text={[person.AddressCity, person.AddressState] 289 + .filter(Boolean) 290 + .join(", ")} 291 + /> 292 + )} 293 + {info?.student?.isStudent && info.student.majors.length > 0 && ( 294 + <> 295 + <Detail.Metadata.Separator /> 296 + {info.student.majors.map((m) => ( 297 + <Detail.Metadata.Label 298 + key={m.code} 299 + title="Major" 300 + text={m.description} 301 + /> 302 + ))} 303 + {info.student.minors.map((m) => ( 304 + <Detail.Metadata.Label 305 + key={m.code} 306 + title="Minor" 307 + text={m.description} 308 + /> 309 + ))} 310 + {info.student.concentrations.map((c) => ( 311 + <Detail.Metadata.Label 312 + key={c.code} 313 + title="Concentration" 314 + text={c.description} 315 + /> 316 + ))} 317 + {info.student.advisors.map((a) => ( 318 + <Detail.Metadata.Label 319 + key={a.id} 320 + title="Advisor" 321 + text={a.name} 322 + /> 323 + ))} 324 + </> 325 + )} 326 + {info?.faculty?.isFaculty && info.faculty.facultyDepts.length > 0 && ( 327 + <> 328 + <Detail.Metadata.Separator /> 329 + {info.faculty.facultyDepts.map((d) => ( 330 + <Detail.Metadata.Label 331 + key={d.code} 332 + title="Faculty Dept" 333 + text={d.description} 334 + /> 335 + ))} 336 + </> 337 + )} 338 + <Detail.Metadata.Separator /> 339 + <Detail.Metadata.Label title="ID" text={person.Id} /> 340 + </Detail.Metadata> 341 + } 342 + actions={ 343 + <ActionPanel> 344 + {person.Username && ( 345 + <Action.CopyToClipboard 346 + title="Copy Email" 347 + content={email(person.Username)} 348 + /> 349 + )} 350 + {person.OfficePhone && ( 351 + <Action.CopyToClipboard 352 + title="Copy Phone" 353 + content={formatPhone(person.OfficePhone)} 354 + /> 355 + )} 356 + <Action.CopyToClipboard 357 + title="Copy ID" 358 + content={person.Id} 359 + icon={Icon.Person} 360 + /> 361 + <Action.OpenInBrowser 362 + title="Open Info Page" 363 + url={`https://selfservice.cedarville.edu/Cedarinfo/Info?id=${person.Id}`} 364 + icon={Icon.Globe} 365 + /> 366 + <Action.CopyToClipboard 367 + title="Export as JSON" 368 + icon={Icon.Code} 369 + content={JSON.stringify(person, null, 2)} 370 + shortcut={{ modifiers: ["cmd", "shift"], key: "j" }} 371 + /> 372 + <Action 373 + title="Sign Out" 374 + icon={Icon.ArrowLeft} 375 + onAction={onSignOut} 376 + shortcut={{ modifiers: ["cmd", "shift"], key: "s" }} 377 + /> 378 + </ActionPanel> 379 + } 380 + /> 381 + ); 382 + } 383 + 384 + // ─── Person list item ────────────────────────────────────────────────────── 385 + 386 + function PersonListItem({ 387 + person, 388 + photoPath, 389 + cookie, 390 + onSignOut, 391 + }: { 392 + person: DirectoryPerson; 393 + photoPath: string | null; 394 + cookie: string; 395 + onSignOut: () => void; 396 + }) { 397 + const name = displayName(person); 398 + 399 + // Subtitle: title for staff, dorm for students 400 + const isStudent = 401 + !!person.StudentClass && 402 + !!person.StudentType && 403 + !(person.Title?.trim() && person.OfficeBuildingCode); 404 + const hasOffice = !isStudent && !!person.OfficeBuildingCode; 405 + const rawTitle = isStudent 406 + ? person.DormName 407 + ? `${person.DormName}${person.DormRoom ? ` ${person.DormRoom}` : ""}` 408 + : person.Username 409 + ? email(person.Username) 410 + : undefined 411 + : ((person.Title?.trim() || 412 + (person.Username ? email(person.Username) : undefined)) ?? 413 + undefined); 414 + const subtitle = 415 + hasOffice && rawTitle && rawTitle.length > 30 416 + ? `${rawTitle.slice(0, 29)}…` 417 + : rawTitle; 418 + 419 + // Badge 420 + let badge: List.Item.Accessory | null = null; 421 + if (!isStudent) { 422 + badge = { tag: { value: "Staff", color: Color.Green } }; 423 + } else if (person.StudentType === "DE") { 424 + badge = { tag: { value: "DE", color: Color.Orange } }; 425 + } else if (person.StudentType === "GS" || person.StudentClass === "GS") { 426 + badge = { tag: { value: "Graduate", color: Color.Purple } }; 427 + } else if ( 428 + person.StudentClass && 429 + CLASS_LABELS[person.StudentClass] && 430 + person.StudentClass !== "HS" && 431 + person.StudentType !== null 432 + ) { 433 + badge = { 434 + tag: { value: CLASS_LABELS[person.StudentClass], color: Color.Blue }, 435 + }; 436 + } 437 + 438 + // Accessories: office then badge 439 + const accessories: List.Item.Accessory[] = []; 440 + if (hasOffice) { 441 + const officeLabel = person.OfficeRoom 442 + ? `${person.OfficeBuildingCode} ${person.OfficeRoom}` 443 + : person.OfficeBuildingCode!; 444 + accessories.push({ text: officeLabel, icon: Icon.Building }); 445 + } 446 + if (badge) accessories.push(badge); 447 + 448 + return ( 449 + <List.Item 450 + title={name} 451 + subtitle={subtitle} 452 + icon={ 453 + photoPath 454 + ? { 455 + source: photoPath, 456 + mask: Image.Mask.Circle, 457 + fallback: Icon.Person, 458 + } 459 + : Icon.Person 460 + } 461 + accessories={accessories} 462 + actions={ 463 + <ActionPanel> 464 + <Action.Push 465 + title="View Details" 466 + icon={Icon.Eye} 467 + target={ 468 + <PersonDetail 469 + person={person} 470 + photoPath={photoPath} 471 + cookie={cookie} 472 + onSignOut={onSignOut} 473 + /> 474 + } 475 + /> 476 + {person.Username && ( 477 + <Action.CopyToClipboard 478 + title="Copy Email" 479 + content={email(person.Username)} 480 + /> 481 + )} 482 + {person.OfficePhone && ( 483 + <Action.CopyToClipboard 484 + title="Copy Phone" 485 + content={formatPhone(person.OfficePhone)} 486 + /> 487 + )} 488 + <Action.CopyToClipboard 489 + title="Copy ID" 490 + content={person.Id} 491 + icon={Icon.Person} 492 + /> 493 + <Action.OpenInBrowser 494 + title="Open Info Page" 495 + url={`https://selfservice.cedarville.edu/Cedarinfo/Info?id=${person.Id}`} 496 + icon={Icon.Globe} 497 + /> 498 + <Action.CopyToClipboard 499 + title="Export as JSON" 500 + icon={Icon.Code} 501 + content={JSON.stringify(person, null, 2)} 502 + shortcut={{ modifiers: ["cmd", "shift"], key: "j" }} 503 + /> 504 + <Action 505 + title="Sign Out" 506 + icon={Icon.ArrowLeft} 507 + onAction={onSignOut} 508 + shortcut={{ modifiers: ["cmd", "shift"], key: "s" }} 509 + /> 510 + </ActionPanel> 511 + } 512 + /> 513 + ); 514 + } 515 + 516 + // ─── Main command ────────────────────────────────────────────────────────── 517 + 518 + export default function SearchDirectory() { 519 + const [authState, setAuthState] = useState<AuthState>({ kind: "loading" }); 520 + const [query, setQuery] = useState(""); 521 + const [filter, setFilter] = useState(""); 522 + const [results, setResults] = useState<DirectoryPerson[]>([]); 523 + const [photoPaths, setPhotoPaths] = useState<Record<string, string>>({}); 524 + const [cacheSize, setCacheSize] = useState(0); 525 + const [isSearching, setIsSearching] = useState(false); 526 + const [departments, setDepartments] = useState<Department[]>([]); 527 + const [populations, setPopulations] = useState<Population[]>([]); 528 + const searchRef = useRef<AbortController | null>(null); 529 + 530 + useEffect(() => { 531 + (async () => { 532 + const cookie = await getStoredCookie(); 533 + if (cookie) { 534 + setAuthState({ kind: "ready", cookie }); 535 + setCacheSize(await getCacheSize()); 536 + getDepartments(cookie).then(setDepartments); 537 + getPopulations(cookie).then(setPopulations); 538 + return; 539 + } 540 + try { 541 + await searchDirectory("probe", "probe"); 542 + setAuthState({ 543 + kind: "sign-in", 544 + signInUrl: "https://selfservice.cedarville.edu/cedarinfo/directory", 545 + }); 546 + } catch (err) { 547 + setAuthState({ 548 + kind: "sign-in", 549 + signInUrl: 550 + err instanceof AuthRequiredError 551 + ? err.signInUrl 552 + : "https://selfservice.cedarville.edu/cedarinfo/directory", 553 + }); 554 + } 555 + })(); 556 + }, []); 557 + 558 + useEffect(() => { 559 + if (authState.kind !== "ready") return; 560 + 561 + searchRef.current?.abort(); 562 + const controller = new AbortController(); 563 + searchRef.current = controller; 564 + 565 + // Parse filter value into API options 566 + const apiOptions = filter.startsWith("dept:") 567 + ? { department: filter.slice(5) } 568 + : filter.startsWith("pop:") 569 + ? { population: Number(filter.slice(4)) } 570 + : {}; 571 + const hasFilter = !!filter; 572 + 573 + // Cache search runs immediately (no debounce) 574 + if (!hasFilter) { 575 + searchCache(query.trim()).then((local) => { 576 + if (!controller.signal.aborted) setResults(local); 577 + }); 578 + } 579 + 580 + const run = async () => { 581 + const trimmed = query.trim(); 582 + 583 + if (!trimmed) return; 584 + 585 + setIsSearching(true); 586 + try { 587 + const { firstName, lastName } = parseSearchQuery(trimmed); 588 + 589 + // For single-word queries, search as both first and last name in parallel 590 + let fresh: DirectoryPerson[]; 591 + if (trimmed && !lastName) { 592 + const [byFirst, byLast] = await Promise.all([ 593 + searchDirectory(firstName, "", authState.cookie, apiOptions), 594 + searchDirectory("", firstName, authState.cookie, apiOptions), 595 + ]); 596 + const seen = new Set<string>(); 597 + fresh = []; 598 + for (const p of [...byFirst, ...byLast]) { 599 + if (!seen.has(p.Id)) { 600 + seen.add(p.Id); 601 + fresh.push(p); 602 + } 603 + } 604 + } else { 605 + fresh = await searchDirectory( 606 + firstName, 607 + lastName, 608 + authState.cookie, 609 + apiOptions, 610 + ); 611 + } 612 + 613 + if (!controller.signal.aborted) { 614 + await mergePeopleIntoCache(fresh); 615 + setCacheSize(await getCacheSize()); 616 + 617 + if (hasFilter) { 618 + setResults(fresh); 619 + } else { 620 + // Re-run cache search after merge so order stays stable (fuzzy score) 621 + setResults(await searchCache(trimmed)); 622 + } 623 + } 624 + } catch (err) { 625 + if (controller.signal.aborted) return; 626 + if (err instanceof AuthRequiredError) { 627 + await clearCookie(); 628 + setAuthState({ kind: "sign-in", signInUrl: err.signInUrl }); 629 + } else { 630 + await showToast({ 631 + style: Toast.Style.Failure, 632 + title: "Search failed", 633 + message: String(err), 634 + }); 635 + } 636 + } finally { 637 + if (!controller.signal.aborted) setIsSearching(false); 638 + } 639 + }; 640 + 641 + const timer = setTimeout(run, 300); 642 + return () => { 643 + clearTimeout(timer); 644 + controller.abort(); 645 + }; 646 + }, [query, filter, authState]); 647 + 648 + useEffect(() => { 649 + if (authState.kind !== "ready") return; 650 + const { cookie } = authState; 651 + for (const person of results) { 652 + if (!person.PhotoUrl || photoPaths[person.Id]) continue; 653 + getCachedPhotoPath(person.Id, person.PhotoUrl, cookie).then((p) => { 654 + if (p) setPhotoPaths((prev) => ({ ...prev, [person.Id]: p })); 655 + }); 656 + } 657 + }, [results, authState]); 658 + 659 + async function handleSignOut() { 660 + await clearCookie(); 661 + setResults([]); 662 + try { 663 + await searchDirectory("probe", "probe"); 664 + setAuthState({ 665 + kind: "sign-in", 666 + signInUrl: "https://selfservice.cedarville.edu/cedarinfo/directory", 667 + }); 668 + } catch (err) { 669 + setAuthState({ 670 + kind: "sign-in", 671 + signInUrl: 672 + err instanceof AuthRequiredError 673 + ? err.signInUrl 674 + : "https://selfservice.cedarville.edu/cedarinfo/directory", 675 + }); 676 + } 677 + } 678 + 679 + async function handleSignIn(signInUrl: string) { 680 + setAuthState({ kind: "signing-in" }); 681 + const toast = await showToast({ 682 + style: Toast.Style.Animated, 683 + title: "Opening sign-in window…", 684 + message: "Complete login in the window that opens", 685 + }); 686 + try { 687 + const cookie = await launchAuthBrowser(signInUrl); 688 + await storeCookie(cookie); 689 + toast.style = Toast.Style.Success; 690 + toast.title = "Signed in!"; 691 + setCacheSize(await getCacheSize()); 692 + getDepartments(cookie).then(setDepartments); 693 + getPopulations(cookie).then(setPopulations); 694 + setAuthState({ kind: "ready", cookie }); 695 + } catch (err) { 696 + toast.style = Toast.Style.Failure; 697 + toast.title = String(err); 698 + setAuthState({ kind: "sign-in", signInUrl }); 699 + } 700 + } 701 + 702 + // ── Auth screens ──────────────────────────────────────────────────────── 703 + 704 + if (authState.kind === "loading") return <List isLoading />; 705 + 706 + if (authState.kind === "sign-in") { 707 + return ( 708 + <List> 709 + <List.EmptyView 710 + title="Sign in to Cedarville" 711 + description="A small sign-in window will open. No browser or cookies are touched." 712 + icon={Icon.Lock} 713 + actions={ 714 + <ActionPanel> 715 + <Action 716 + title="Sign In" 717 + icon={Icon.Person} 718 + onAction={() => handleSignIn(authState.signInUrl)} 719 + /> 720 + </ActionPanel> 721 + } 722 + /> 723 + </List> 724 + ); 725 + } 726 + 727 + if (authState.kind === "signing-in") { 728 + return ( 729 + <List isLoading> 730 + <List.EmptyView 731 + title="Waiting for sign-in…" 732 + description="Complete login in the window that just opened." 733 + icon={Icon.Clock} 734 + /> 735 + </List> 736 + ); 737 + } 738 + 739 + // ── Ready: search ─────────────────────────────────────────────────────── 740 + 741 + return ( 742 + <List 743 + isLoading={isSearching} 744 + searchBarPlaceholder="Search by name…" 745 + onSearchTextChange={setQuery} 746 + throttle={false} 747 + searchBarAccessory={ 748 + <List.Dropdown tooltip="Filter" value={filter} onChange={setFilter}> 749 + <List.Dropdown.Item title="All People" value="" /> 750 + {populations.length > 0 && ( 751 + <List.Dropdown.Section title="By Type"> 752 + {populations.map((p) => ( 753 + <List.Dropdown.Item 754 + key={p.code} 755 + title={p.desc} 756 + value={`pop:${p.code}`} 757 + /> 758 + ))} 759 + </List.Dropdown.Section> 760 + )} 761 + {departments.length > 0 && ( 762 + <List.Dropdown.Section title="By Department"> 763 + {departments.map((d) => ( 764 + <List.Dropdown.Item 765 + key={d.code} 766 + title={d.description} 767 + value={`dept:${d.code}`} 768 + /> 769 + ))} 770 + </List.Dropdown.Section> 771 + )} 772 + </List.Dropdown> 773 + } 774 + > 775 + {results.length === 0 ? ( 776 + <List.EmptyView 777 + title={ 778 + query.trim() 779 + ? "No results found" 780 + : "Search the Cedarville Directory" 781 + } 782 + description={ 783 + query.trim() 784 + ? `No one matched "${query}"` 785 + : cacheSize > 0 786 + ? `${cacheSize} people cached` 787 + : "Start typing to search" 788 + } 789 + icon={query.trim() ? Icon.MagnifyingGlass : Icon.Person} 790 + /> 791 + ) : ( 792 + results.map((person) => ( 793 + <PersonListItem 794 + key={person.Id} 795 + person={person} 796 + photoPath={photoPaths[person.Id] ?? null} 797 + cookie={authState.cookie} 798 + onSignOut={handleSignOut} 799 + /> 800 + )) 801 + )} 802 + </List> 803 + ); 804 + }
+13
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "lib": ["ES2022"], 4 + "module": "CommonJS", 5 + "target": "ES2022", 6 + "strict": true, 7 + "jsx": "react-jsx", 8 + "jsxImportSource": "react", 9 + "moduleResolution": "node", 10 + "allowSyntheticDefaultImports": true, 11 + "esModuleInterop": true 12 + } 13 + }