keep-computer-on-in-your-backpack 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.
package/Info.plist ADDED
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
+ <plist version="1.0">
5
+ <dict>
6
+ <key>CFBundleDevelopmentRegion</key>
7
+ <string>en</string>
8
+ <key>CFBundleExecutable</key>
9
+ <string>BackpackAwakeMenu</string>
10
+ <key>CFBundleIdentifier</key>
11
+ <string>com.codex.backpack-awake-menu</string>
12
+ <key>CFBundleInfoDictionaryVersion</key>
13
+ <string>6.0</string>
14
+ <key>CFBundleName</key>
15
+ <string>Backpack Awake</string>
16
+ <key>CFBundlePackageType</key>
17
+ <string>APPL</string>
18
+ <key>CFBundleShortVersionString</key>
19
+ <string>1.0</string>
20
+ <key>CFBundleVersion</key>
21
+ <string>1</string>
22
+ <key>LSMinimumSystemVersion</key>
23
+ <string>14.0</string>
24
+ <key>LSUIElement</key>
25
+ <true/>
26
+ <key>NSHighResolutionCapable</key>
27
+ <true/>
28
+ </dict>
29
+ </plist>
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Declan Kramper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Keep Computer On In Your Backpack
2
+
3
+ Problem: closing your laptop to put it into your backpack turns it off. This is an issue if you want to keep using local coding tools like Claude code or Codex.
4
+
5
+ Solution:
6
+ 1. run the script below to download this app
7
+ 2. click the backpack icon in your menu bar to keep your laptop in your backpack. This means you can use codex on the ChatGPT iPhone app to code while you’re commuting.
8
+
9
+ ```sh
10
+ npx keep-computer-on-in-your-backpack
11
+ ```
12
+
13
+ Note: the screen turns off, but computer is still on. Be wary of hot laptop running in your backpack.
14
+
15
+ @dkbuilds.co
@@ -0,0 +1,155 @@
1
+ import AppKit
2
+
3
+ private let stateDirectory = URL(fileURLWithPath: NSHomeDirectory())
4
+ .appendingPathComponent("Library/Application Support/BackpackAwake")
5
+ private let stateFile = stateDirectory.appendingPathComponent("state")
6
+ private let caffeinatePIDFile = stateDirectory.appendingPathComponent("caffeinate.pid")
7
+
8
+ final class AppDelegate: NSObject, NSApplicationDelegate {
9
+ private var statusItem: NSStatusItem!
10
+ private let menu = NSMenu()
11
+ private let stateItem = NSMenuItem(title: "Status: Checking", action: nil, keyEquivalent: "")
12
+ private let toggleItem = NSMenuItem(title: "Turn On", action: #selector(toggle), keyEquivalent: "")
13
+ private var enabled = false
14
+ private var caffeinateProcess: Process?
15
+ private var timer: Timer?
16
+
17
+ func applicationDidFinishLaunching(_ notification: Notification) {
18
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
19
+ statusItem.autosaveName = "BackpackAwake"
20
+ statusItem.button?.title = ""
21
+ statusItem.button?.imagePosition = .imageOnly
22
+ setStatusIcon(symbolName: "backpack", fallbackSymbolName: "power.circle", tooltip: "Backpack Awake: Checking")
23
+
24
+ toggleItem.target = self
25
+ menu.addItem(stateItem)
26
+ menu.addItem(NSMenuItem.separator())
27
+ menu.addItem(toggleItem)
28
+ menu.addItem(NSMenuItem(title: "Refresh", action: #selector(refresh), keyEquivalent: "r"))
29
+ menu.items.last?.target = self
30
+ menu.addItem(NSMenuItem.separator())
31
+ menu.addItem(NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q"))
32
+ menu.items.last?.target = self
33
+
34
+ statusItem.menu = menu
35
+ refresh()
36
+ timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
37
+ self?.refresh()
38
+ }
39
+ }
40
+
41
+ @objc private func refresh() {
42
+ DispatchQueue.global(qos: .utility).async {
43
+ let status = self.readDesiredState()
44
+ DispatchQueue.main.async {
45
+ self.enabled = status
46
+ self.syncCaffeinateProcess()
47
+ self.render()
48
+ }
49
+ }
50
+ }
51
+
52
+ @objc private func toggle() {
53
+ let targetEnabled = !enabled
54
+ toggleItem.isEnabled = false
55
+ stateItem.title = "Status: Updating"
56
+ setStatusIcon(symbolName: "backpack", fallbackSymbolName: "power.circle", tooltip: "Backpack Awake: Updating")
57
+
58
+ DispatchQueue.global(qos: .userInitiated).async {
59
+ self.writeDesiredState(targetEnabled)
60
+ DispatchQueue.main.async {
61
+ self.enabled = targetEnabled
62
+ self.toggleItem.isEnabled = true
63
+ self.syncCaffeinateProcess()
64
+ self.render()
65
+ }
66
+ }
67
+ }
68
+
69
+ private func render() {
70
+ if enabled {
71
+ setStatusIcon(symbolName: "backpack.fill", fallbackSymbolName: "power.circle.fill", tooltip: "Backpack Awake: On")
72
+ stateItem.title = "Status: On"
73
+ toggleItem.title = "Turn Off"
74
+ } else {
75
+ setStatusIcon(symbolName: "backpack", fallbackSymbolName: "power.circle", tooltip: "Backpack Awake: Off")
76
+ stateItem.title = "Status: Off"
77
+ toggleItem.title = "Turn On"
78
+ }
79
+ }
80
+
81
+ private func syncCaffeinateProcess() {
82
+ if enabled {
83
+ if caffeinateProcess?.isRunning == true {
84
+ return
85
+ }
86
+
87
+ let process = Process()
88
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")
89
+ process.arguments = ["-im"]
90
+
91
+ do {
92
+ try process.run()
93
+ caffeinateProcess = process
94
+ writeCaffeinatePID(process.processIdentifier)
95
+ } catch {
96
+ caffeinateProcess = nil
97
+ }
98
+ } else {
99
+ if caffeinateProcess?.isRunning == true {
100
+ caffeinateProcess?.terminate()
101
+ }
102
+ caffeinateProcess = nil
103
+ removeCaffeinatePID()
104
+ }
105
+ }
106
+
107
+ private func readDesiredState() -> Bool {
108
+ if let value = try? String(contentsOf: stateFile, encoding: .utf8)
109
+ .trimmingCharacters(in: .whitespacesAndNewlines) {
110
+ return value == "on"
111
+ }
112
+
113
+ return false
114
+ }
115
+
116
+ private func writeDesiredState(_ enabled: Bool) {
117
+ do {
118
+ try FileManager.default.createDirectory(at: stateDirectory, withIntermediateDirectories: true)
119
+ try (enabled ? "on\n" : "off\n").write(to: stateFile, atomically: true, encoding: .utf8)
120
+ } catch {
121
+ return
122
+ }
123
+ }
124
+
125
+ private func writeCaffeinatePID(_ pid: Int32) {
126
+ do {
127
+ try FileManager.default.createDirectory(at: stateDirectory, withIntermediateDirectories: true)
128
+ try "\(pid)\n".write(to: caffeinatePIDFile, atomically: true, encoding: .utf8)
129
+ } catch {
130
+ return
131
+ }
132
+ }
133
+
134
+ private func removeCaffeinatePID() {
135
+ try? FileManager.default.removeItem(at: caffeinatePIDFile)
136
+ }
137
+
138
+ private func setStatusIcon(symbolName: String, fallbackSymbolName: String, tooltip: String) {
139
+ let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: tooltip)
140
+ ?? NSImage(systemSymbolName: fallbackSymbolName, accessibilityDescription: tooltip)
141
+ image?.isTemplate = true
142
+ statusItem.button?.image = image
143
+ statusItem.button?.toolTip = tooltip
144
+ }
145
+
146
+ @objc private func quit() {
147
+ NSApp.terminate(nil)
148
+ }
149
+ }
150
+
151
+ let app = NSApplication.shared
152
+ let delegate = AppDelegate()
153
+ app.delegate = delegate
154
+ app.setActivationPolicy(.accessory)
155
+ app.run()
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ if (process.platform !== "darwin") {
7
+ console.error("Backpack Awake can only be installed on macOS.");
8
+ process.exit(1);
9
+ }
10
+
11
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const installer = resolve(packageRoot, "install.sh");
13
+
14
+ const result = spawnSync("/bin/zsh", [installer], {
15
+ cwd: packageRoot,
16
+ env: {
17
+ ...process.env,
18
+ BACKPACK_AWAKE_SOURCE_DIR: packageRoot,
19
+ },
20
+ stdio: "inherit",
21
+ });
22
+
23
+ if (result.error) {
24
+ console.error(result.error.message);
25
+ process.exit(1);
26
+ }
27
+
28
+ process.exit(result.status ?? 1);
package/install.sh ADDED
@@ -0,0 +1,84 @@
1
+ #!/bin/zsh
2
+ set -eu
3
+
4
+ REPO_URL="${REPO_URL:-https://github.com/declankra/keep-computer-on-in-your-backpack.git}"
5
+ WORK_DIR="${TMPDIR:-/tmp}/backpack-awake-install"
6
+ SOURCE_DIR="${BACKPACK_AWAKE_SOURCE_DIR:-}"
7
+ APP_NAME="Backpack Awake.app"
8
+ APP_DIR="$HOME/Applications/$APP_NAME"
9
+ STATE_DIR="$HOME/Library/Application Support/BackpackAwake"
10
+ LOGIN_AGENT="$HOME/Library/LaunchAgents/com.declankramper.backpack-awake-menu.plist"
11
+ CONTROLLER_DIR="/Library/Application Support/BackpackAwake"
12
+ CONTROLLER_PATH="$CONTROLLER_DIR/backpack-awake-controller"
13
+ CONTROLLER_PLIST="/Library/LaunchDaemons/com.declankramper.backpack-awake-controller.plist"
14
+ LOG_DIR="/Library/Logs/BackpackAwake"
15
+
16
+ need_command() {
17
+ if ! command -v "$1" >/dev/null 2>&1; then
18
+ echo "Missing required command: $1" >&2
19
+ exit 1
20
+ fi
21
+ }
22
+
23
+ need_command swiftc
24
+ need_command codesign
25
+
26
+ if [[ -n "$SOURCE_DIR" ]]; then
27
+ WORK_DIR="$SOURCE_DIR"
28
+ else
29
+ need_command git
30
+ rm -rf "$WORK_DIR"
31
+ git clone --depth 1 "$REPO_URL" "$WORK_DIR"
32
+ fi
33
+
34
+ mkdir -p "$APP_DIR/Contents/MacOS" "$HOME/Applications" "$STATE_DIR" "$HOME/Library/LaunchAgents"
35
+
36
+ swiftc "$WORK_DIR/Sources/main.swift" -framework AppKit -o "$APP_DIR/Contents/MacOS/BackpackAwakeMenu"
37
+ cp "$WORK_DIR/Info.plist" "$APP_DIR/Contents/Info.plist"
38
+ codesign --force --deep --sign - "$APP_DIR" >/dev/null
39
+
40
+ if [[ ! -f "$STATE_DIR/state" ]]; then
41
+ printf 'off\n' > "$STATE_DIR/state"
42
+ fi
43
+
44
+ cat > "$LOGIN_AGENT" <<PLIST
45
+ <?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
47
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
48
+ <plist version="1.0">
49
+ <dict>
50
+ <key>Label</key>
51
+ <string>com.declankramper.backpack-awake-menu</string>
52
+
53
+ <key>ProgramArguments</key>
54
+ <array>
55
+ <string>/usr/bin/open</string>
56
+ <string>$APP_DIR</string>
57
+ </array>
58
+
59
+ <key>RunAtLoad</key>
60
+ <true/>
61
+ </dict>
62
+ </plist>
63
+ PLIST
64
+
65
+ if [[ "${BACKPACK_AWAKE_SKIP_PRIVILEGED:-0}" != "1" ]]; then
66
+ echo "Installing the privileged controller. macOS may ask for your password once."
67
+ sudo mkdir -p "$CONTROLLER_DIR" "$LOG_DIR"
68
+ sudo install -o root -g wheel -m 755 "$WORK_DIR/scripts/backpack-awake-controller" "$CONTROLLER_PATH"
69
+ sudo install -o root -g wheel -m 644 "$WORK_DIR/scripts/com.declankramper.backpack-awake-controller.plist" "$CONTROLLER_PLIST"
70
+ sudo launchctl bootout system "$CONTROLLER_PLIST" 2>/dev/null || true
71
+ sudo launchctl bootstrap system "$CONTROLLER_PLIST"
72
+ sudo launchctl kickstart -k system/com.declankramper.backpack-awake-controller
73
+ else
74
+ echo "Skipping privileged controller install."
75
+ fi
76
+
77
+ if [[ "${BACKPACK_AWAKE_SKIP_LAUNCH:-0}" != "1" ]]; then
78
+ launchctl bootout "gui/$(id -u)" "$LOGIN_AGENT" 2>/dev/null || true
79
+ launchctl bootstrap "gui/$(id -u)" "$LOGIN_AGENT"
80
+ launchctl kickstart -k "gui/$(id -u)/com.declankramper.backpack-awake-menu"
81
+ open "$APP_DIR"
82
+ fi
83
+
84
+ echo "Installed. Look for the backpack icon in your menu bar."
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "keep-computer-on-in-your-backpack",
3
+ "version": "0.1.0",
4
+ "description": "A macOS menu bar app that keeps your laptop awake with the lid closed while letting the screen turn off.",
5
+ "bin": {
6
+ "keep-computer-on-in-your-backpack": "bin/install.mjs"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "install.sh",
11
+ "Info.plist",
12
+ "Sources",
13
+ "scripts"
14
+ ],
15
+ "os": [
16
+ "darwin"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "test": "node --check bin/install.mjs && zsh -n install.sh && zsh -n scripts/backpack-awake-controller && plutil -lint Info.plist scripts/com.declankramper.backpack-awake-controller.plist",
23
+ "pack:check": "npm pack --dry-run"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/declankra/keep-computer-on-in-your-backpack.git"
28
+ },
29
+ "homepage": "https://github.com/declankra/keep-computer-on-in-your-backpack#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/declankra/keep-computer-on-in-your-backpack/issues"
32
+ },
33
+ "license": "MIT"
34
+ }
@@ -0,0 +1,88 @@
1
+ #!/bin/zsh
2
+ set -eu
3
+
4
+ PATH=/usr/bin:/bin:/usr/sbin:/sbin
5
+
6
+ LOG_FILE=/Library/Logs/BackpackAwake/controller.log
7
+
8
+ log() {
9
+ printf '%s %s\n' "$(/bin/date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
10
+ }
11
+
12
+ console_user() {
13
+ /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" \
14
+ | /usr/bin/awk '/Name :/ { print $3; exit }'
15
+ }
16
+
17
+ console_home() {
18
+ local user="$1"
19
+ /usr/bin/dscl . -read "/Users/$user" NFSHomeDirectory 2>/dev/null \
20
+ | /usr/bin/awk '{ print $2; exit }'
21
+ }
22
+
23
+ state_paths() {
24
+ local user home
25
+ user="$(console_user)"
26
+ if [[ -z "$user" || "$user" == "loginwindow" ]]; then
27
+ return 1
28
+ fi
29
+
30
+ home="$(console_home "$user")"
31
+ if [[ -z "$home" || ! -d "$home" ]]; then
32
+ return 1
33
+ fi
34
+
35
+ STATE_DIR="$home/Library/Application Support/BackpackAwake"
36
+ STATE_FILE="$STATE_DIR/state"
37
+ PID_FILE="$STATE_DIR/caffeinate.pid"
38
+ }
39
+
40
+ sleep_disabled() {
41
+ /usr/bin/pmset -g | /usr/bin/grep -q 'SleepDisabled[[:space:]]*1'
42
+ }
43
+
44
+ desired_state() {
45
+ if [[ -f "$STATE_FILE" ]]; then
46
+ /bin/cat "$STATE_FILE" 2>/dev/null | /usr/bin/tr -d '[:space:]'
47
+ else
48
+ echo "off"
49
+ fi
50
+ }
51
+
52
+ kill_recorded_caffeinate() {
53
+ [[ -f "$PID_FILE" ]] || return 0
54
+ local pid
55
+ pid="$(/bin/cat "$PID_FILE" 2>/dev/null || true)"
56
+ [[ -n "$pid" ]] || return 0
57
+
58
+ if /bin/ps -p "$pid" -o comm= 2>/dev/null | /usr/bin/grep -q '^caffeinate$'; then
59
+ /bin/kill "$pid" 2>/dev/null || true
60
+ log "killed recorded caffeinate pid $pid"
61
+ fi
62
+ /bin/rm -f "$PID_FILE"
63
+ }
64
+
65
+ apply_once() {
66
+ state_paths || return 0
67
+
68
+ case "$(desired_state)" in
69
+ on)
70
+ if ! sleep_disabled; then
71
+ /usr/bin/pmset -a disablesleep 1
72
+ log "enabled SleepDisabled"
73
+ fi
74
+ ;;
75
+ *)
76
+ kill_recorded_caffeinate
77
+ if sleep_disabled; then
78
+ /usr/bin/pmset -a disablesleep 0
79
+ log "disabled SleepDisabled"
80
+ fi
81
+ ;;
82
+ esac
83
+ }
84
+
85
+ while true; do
86
+ apply_once
87
+ /bin/sleep 2
88
+ done
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
+ <plist version="1.0">
5
+ <dict>
6
+ <key>Label</key>
7
+ <string>com.declankramper.backpack-awake-controller</string>
8
+
9
+ <key>ProgramArguments</key>
10
+ <array>
11
+ <string>/Library/Application Support/BackpackAwake/backpack-awake-controller</string>
12
+ </array>
13
+
14
+ <key>RunAtLoad</key>
15
+ <true/>
16
+
17
+ <key>KeepAlive</key>
18
+ <true/>
19
+
20
+ <key>StandardOutPath</key>
21
+ <string>/Library/Logs/BackpackAwake/controller.out</string>
22
+
23
+ <key>StandardErrorPath</key>
24
+ <string>/Library/Logs/BackpackAwake/controller.err</string>
25
+ </dict>
26
+ </plist>