@myerscarpenter/quest-dev 1.4.1 → 2.0.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.
Files changed (142) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.github/workflows/docs.yml +45 -0
  3. package/.github/workflows/publish.yml +11 -1
  4. package/README.md +27 -0
  5. package/build/cast/decoder.d.ts +48 -0
  6. package/build/cast/decoder.d.ts.map +1 -0
  7. package/build/cast/decoder.js +152 -0
  8. package/build/cast/decoder.js.map +1 -0
  9. package/build/cast/session.d.ts +87 -0
  10. package/build/cast/session.d.ts.map +1 -0
  11. package/build/cast/session.js +565 -0
  12. package/build/cast/session.js.map +1 -0
  13. package/build/commands/logcat.d.ts.map +1 -1
  14. package/build/commands/logcat.js +7 -6
  15. package/build/commands/logcat.js.map +1 -1
  16. package/build/commands/screenshot.d.ts.map +1 -1
  17. package/build/commands/screenshot.js +17 -20
  18. package/build/commands/screenshot.js.map +1 -1
  19. package/build/commands/stay-awake.d.ts +2 -15
  20. package/build/commands/stay-awake.d.ts.map +1 -1
  21. package/build/commands/stay-awake.js +14 -77
  22. package/build/commands/stay-awake.js.map +1 -1
  23. package/build/daemon/cast-manager.d.ts +42 -0
  24. package/build/daemon/cast-manager.d.ts.map +1 -0
  25. package/build/daemon/cast-manager.js +243 -0
  26. package/build/daemon/cast-manager.js.map +1 -0
  27. package/build/daemon/client.d.ts +40 -0
  28. package/build/daemon/client.d.ts.map +1 -0
  29. package/build/daemon/client.js +133 -0
  30. package/build/daemon/client.js.map +1 -0
  31. package/build/daemon/daemon.d.ts +20 -0
  32. package/build/daemon/daemon.d.ts.map +1 -0
  33. package/build/daemon/daemon.js +130 -0
  34. package/build/daemon/daemon.js.map +1 -0
  35. package/build/daemon/deploy.d.ts +44 -0
  36. package/build/daemon/deploy.d.ts.map +1 -0
  37. package/build/daemon/deploy.js +230 -0
  38. package/build/daemon/deploy.js.map +1 -0
  39. package/build/daemon/logcat-manager.d.ts +39 -0
  40. package/build/daemon/logcat-manager.d.ts.map +1 -0
  41. package/build/daemon/logcat-manager.js +194 -0
  42. package/build/daemon/logcat-manager.js.map +1 -0
  43. package/build/daemon/server.d.ts +19 -0
  44. package/build/daemon/server.d.ts.map +1 -0
  45. package/build/daemon/server.js +482 -0
  46. package/build/daemon/server.js.map +1 -0
  47. package/build/daemon/stay-awake-manager.d.ts +22 -0
  48. package/build/daemon/stay-awake-manager.d.ts.map +1 -0
  49. package/build/daemon/stay-awake-manager.js +74 -0
  50. package/build/daemon/stay-awake-manager.js.map +1 -0
  51. package/build/index.js +272 -45
  52. package/build/index.js.map +1 -1
  53. package/build/public/dashboard.js +749 -0
  54. package/build/public/index.html +12 -0
  55. package/build/public/style.css +106 -0
  56. package/build/utils/adb.d.ts +6 -0
  57. package/build/utils/adb.d.ts.map +1 -1
  58. package/build/utils/adb.js +62 -66
  59. package/build/utils/adb.js.map +1 -1
  60. package/build/utils/casting-apk.d.ts +40 -0
  61. package/build/utils/casting-apk.d.ts.map +1 -0
  62. package/build/utils/casting-apk.js +252 -0
  63. package/build/utils/casting-apk.js.map +1 -0
  64. package/build/utils/config.d.ts +5 -3
  65. package/build/utils/config.d.ts.map +1 -1
  66. package/build/utils/config.js +18 -38
  67. package/build/utils/config.js.map +1 -1
  68. package/build/utils/exec.d.ts +5 -0
  69. package/build/utils/exec.d.ts.map +1 -1
  70. package/build/utils/exec.js +17 -0
  71. package/build/utils/exec.js.map +1 -1
  72. package/build/utils/filename.d.ts +7 -1
  73. package/build/utils/filename.d.ts.map +1 -1
  74. package/build/utils/filename.js +17 -2
  75. package/build/utils/filename.js.map +1 -1
  76. package/build/utils/filename.test.js +33 -1
  77. package/build/utils/filename.test.js.map +1 -1
  78. package/build/utils/jpeg-comment.d.ts +14 -0
  79. package/build/utils/jpeg-comment.d.ts.map +1 -0
  80. package/build/utils/jpeg-comment.js +28 -0
  81. package/build/utils/jpeg-comment.js.map +1 -0
  82. package/build/utils/test-properties.d.ts +34 -0
  83. package/build/utils/test-properties.d.ts.map +1 -0
  84. package/build/utils/test-properties.js +73 -0
  85. package/build/utils/test-properties.js.map +1 -0
  86. package/package.json +11 -5
  87. package/packages/cast2-protocol/README.md +86 -0
  88. package/packages/cast2-protocol/docs/_config.yml +4 -0
  89. package/packages/cast2-protocol/docs/feature-flags.md +102 -0
  90. package/packages/cast2-protocol/docs/index.md +24 -0
  91. package/packages/cast2-protocol/docs/open-investigations.md +149 -0
  92. package/packages/cast2-protocol/docs/protocol.md +602 -0
  93. package/packages/cast2-protocol/package.json +46 -0
  94. package/packages/cast2-protocol/src/constants.ts +65 -0
  95. package/packages/cast2-protocol/src/index.ts +7 -0
  96. package/packages/cast2-protocol/src/mgik.ts +69 -0
  97. package/packages/cast2-protocol/src/mud.ts +294 -0
  98. package/packages/cast2-protocol/src/pose.ts +99 -0
  99. package/packages/cast2-protocol/src/resolutions.ts +34 -0
  100. package/packages/cast2-protocol/src/types.ts +64 -0
  101. package/packages/cast2-protocol/src/xrsp.ts +73 -0
  102. package/packages/cast2-protocol/tests/mgik.test.ts +80 -0
  103. package/packages/cast2-protocol/tests/mud.test.ts +295 -0
  104. package/packages/cast2-protocol/tests/pose.test.ts +173 -0
  105. package/packages/cast2-protocol/tests/xrsp.test.ts +90 -0
  106. package/packages/cast2-protocol/tsconfig.json +20 -0
  107. package/pnpm-workspace.yaml +2 -0
  108. package/src/cast/decoder.ts +178 -0
  109. package/src/cast/session.ts +708 -0
  110. package/src/commands/logcat.ts +6 -5
  111. package/src/commands/screenshot.ts +19 -13
  112. package/src/commands/stay-awake.ts +22 -91
  113. package/src/daemon/adbkit-apkreader.d.ts +14 -0
  114. package/src/daemon/cast-manager.ts +282 -0
  115. package/src/daemon/client.ts +166 -0
  116. package/src/daemon/daemon.ts +169 -0
  117. package/src/daemon/deploy.ts +307 -0
  118. package/src/daemon/logcat-manager.ts +229 -0
  119. package/src/daemon/server.ts +595 -0
  120. package/src/daemon/stay-awake-manager.ts +83 -0
  121. package/src/index.ts +326 -56
  122. package/src/public/dashboard.js +288 -0
  123. package/src/public/index.html +12 -0
  124. package/src/public/style.css +106 -0
  125. package/src/utils/adb.ts +70 -57
  126. package/src/utils/casting-apk.ts +276 -0
  127. package/src/utils/config.ts +18 -36
  128. package/src/utils/exec.ts +20 -0
  129. package/src/utils/filename.test.ts +41 -1
  130. package/src/utils/filename.ts +18 -2
  131. package/src/utils/jpeg-comment.ts +30 -0
  132. package/src/utils/test-properties.ts +94 -0
  133. package/tests/cast/auto-layer.test.ts +87 -0
  134. package/tests/cast/decoder.test.ts +82 -0
  135. package/tests/cast/session-restart.test.ts +107 -0
  136. package/tests/config.test.ts +17 -22
  137. package/tests/daemon/api-status.test.ts +82 -0
  138. package/tests/daemon/cast-manager.test.ts +69 -0
  139. package/tests/daemon/mjpeg-stream.test.ts +144 -0
  140. package/tests/daemon/pose-endpoint.test.ts +63 -0
  141. package/tests/daemon/start-guard.test.ts +77 -0
  142. package/vitest.config.ts +10 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Casting APK extraction and installation utilities.
3
+ *
4
+ * The casting service APK ships inside Meta Quest Developer Hub (MQDH).
5
+ *
6
+ * Supported MQDH sources:
7
+ * - macOS: /Applications/Meta Quest Developer Hub.app (or any .app bundle)
8
+ * - macOS: .dmg file (mounted, APKs extracted from the .app inside)
9
+ * - Windows: .exe.zip or .exe (NSIS installer, extracted via 7z)
10
+ * - Any platform: pre-extracted directory containing the APK files
11
+ */
12
+ import { existsSync, mkdirSync, statSync, readdirSync, copyFileSync, rmSync } from "node:fs";
13
+ import { execFileSync } from "node:child_process";
14
+ import { join } from "node:path";
15
+ import { homedir, platform } from "node:os";
16
+ import { execCommand } from "./exec.js";
17
+ import { verbose } from "./verbose.js";
18
+ const CASTING_PKG = "com.oculus.magicislandcastingservice";
19
+ const RELEASE_APK = "com.oculus.magicislandcastingservice.release.apk";
20
+ const APK_DIR = join(homedir(), ".local", "share", "quest-dev");
21
+ const RELEASE_APK_PATH = join(APK_DIR, RELEASE_APK);
22
+ /** APK location inside the macOS .app bundle */
23
+ const MACOS_APP_REL = "Contents/Resources/bin/Casting/Resources/";
24
+ /** APK location inside the Windows NSIS-extracted MQDH */
25
+ const WINDOWS_RES_REL = "resources/bin/Casting/Resources/";
26
+ /** Default macOS install location */
27
+ const MACOS_DEFAULT_APP = "/Applications/Meta Quest Developer Hub.app";
28
+ /**
29
+ * Try to find MQDH on this machine without user input.
30
+ * Returns the path if found, null otherwise.
31
+ */
32
+ export function findInstalledMqdh() {
33
+ if (platform() === "darwin") {
34
+ if (existsSync(MACOS_DEFAULT_APP)) {
35
+ return MACOS_DEFAULT_APP;
36
+ }
37
+ const userApps = join(homedir(), "Applications", "Meta Quest Developer Hub.app");
38
+ if (existsSync(userApps)) {
39
+ return userApps;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Extract the casting APK from a MQDH source.
46
+ *
47
+ * Accepts:
48
+ * - macOS .app bundle (e.g. /Applications/Meta Quest Developer Hub.app)
49
+ * - macOS .dmg disk image
50
+ * - Windows .exe.zip or .exe (requires 7z)
51
+ * - Any directory containing the APK file
52
+ */
53
+ export async function extractCastingApk(source) {
54
+ mkdirSync(APK_DIR, { recursive: true });
55
+ const stat = statSync(source);
56
+ let searchDir;
57
+ if (stat.isDirectory() && source.endsWith(".app")) {
58
+ searchDir = source;
59
+ verbose(`Using macOS .app bundle: ${source}`);
60
+ }
61
+ else if (stat.isDirectory()) {
62
+ searchDir = source;
63
+ }
64
+ else if (source.endsWith(".dmg")) {
65
+ searchDir = await extractDmg(source);
66
+ }
67
+ else if (source.endsWith(".zip")) {
68
+ require7z();
69
+ const tmpZip = join(APK_DIR, "mqdh-zip-tmp");
70
+ rmSync(tmpZip, { recursive: true, force: true });
71
+ execFileSync("7z", ["x", `-o${tmpZip}`, source, "-y"], { stdio: "pipe" });
72
+ const exe = readdirSync(tmpZip).find((f) => f.endsWith(".exe"));
73
+ if (!exe) {
74
+ throw new Error("No .exe found inside zip");
75
+ }
76
+ searchDir = extractNsis(join(tmpZip, exe));
77
+ rmSync(tmpZip, { recursive: true, force: true });
78
+ }
79
+ else if (source.endsWith(".exe")) {
80
+ require7z();
81
+ searchDir = extractNsis(source);
82
+ }
83
+ else {
84
+ throw new Error(`Unsupported file type: ${source}\n` +
85
+ `Supported formats: .app (macOS), .dmg (macOS), .exe.zip (Windows), .exe (Windows), or a directory`);
86
+ }
87
+ // Try known paths first, then fall back to recursive search
88
+ if (!copyApk(searchDir, [MACOS_APP_REL, WINDOWS_RES_REL])) {
89
+ throw new Error("Could not find casting APK. Is this the right MQDH installation?");
90
+ }
91
+ // Cleanup temp extraction dirs
92
+ for (const tmp of ["mqdh-app-tmp", "mqdh-nsis-tmp", "mqdh-zip-tmp", "mqdh-dmg-tmp"]) {
93
+ rmSync(join(APK_DIR, tmp), { recursive: true, force: true });
94
+ }
95
+ return APK_DIR;
96
+ }
97
+ /**
98
+ * Copy the release APK from a search directory, trying known relative paths
99
+ * first, then falling back to recursive search.
100
+ */
101
+ function copyApk(searchDir, knownPaths) {
102
+ // Try known paths first
103
+ for (const relPath of knownPaths) {
104
+ const candidate = join(searchDir, relPath, RELEASE_APK);
105
+ if (existsSync(candidate)) {
106
+ copyFileSync(candidate, RELEASE_APK_PATH);
107
+ return true;
108
+ }
109
+ }
110
+ // Fall back to recursive search
111
+ const found = findFile(searchDir, RELEASE_APK);
112
+ if (found) {
113
+ copyFileSync(found, RELEASE_APK_PATH);
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ /** Mount a .dmg and extract APKs from the .app inside */
119
+ async function extractDmg(dmgPath) {
120
+ if (platform() !== "darwin") {
121
+ throw new Error(".dmg files can only be opened on macOS. On other platforms, mount the DMG and point to the .app inside.");
122
+ }
123
+ const mountPoint = join(APK_DIR, "mqdh-dmg-tmp");
124
+ rmSync(mountPoint, { recursive: true, force: true });
125
+ mkdirSync(mountPoint, { recursive: true });
126
+ try {
127
+ execFileSync("hdiutil", ["attach", dmgPath, "-mountpoint", mountPoint, "-nobrowse", "-quiet"], {
128
+ stdio: "pipe",
129
+ });
130
+ }
131
+ catch (e) {
132
+ throw new Error(`Failed to mount DMG: ${e.message}`);
133
+ }
134
+ try {
135
+ const entries = readdirSync(mountPoint);
136
+ const app = entries.find((e) => e.endsWith(".app"));
137
+ if (!app) {
138
+ throw new Error("No .app found inside DMG");
139
+ }
140
+ return join(mountPoint, app);
141
+ }
142
+ catch (e) {
143
+ try {
144
+ execFileSync("hdiutil", ["detach", mountPoint, "-quiet"], { stdio: "pipe" });
145
+ }
146
+ catch { }
147
+ throw e;
148
+ }
149
+ }
150
+ function require7z() {
151
+ try {
152
+ execFileSync("7z", ["--help"], { stdio: "pipe" });
153
+ }
154
+ catch {
155
+ throw new Error("7z is required to extract Windows MQDH installers.\n" +
156
+ "Install it with: sudo apt install p7zip-full (Linux) or brew install p7zip (macOS)");
157
+ }
158
+ }
159
+ function extractNsis(exePath) {
160
+ const nsisDir = join(APK_DIR, "mqdh-nsis-tmp");
161
+ rmSync(nsisDir, { recursive: true, force: true });
162
+ execFileSync("7z", ["x", `-o${nsisDir}`, exePath, "-y"], { stdio: "pipe" });
163
+ const inner = join(nsisDir, "$PLUGINSDIR", "app-64.7z");
164
+ if (!existsSync(inner)) {
165
+ throw new Error("Could not find app-64.7z inside NSIS installer");
166
+ }
167
+ const appDir = join(APK_DIR, "mqdh-app-tmp");
168
+ rmSync(appDir, { recursive: true, force: true });
169
+ execFileSync("7z", ["x", `-o${appDir}`, inner, "-y"], { stdio: "pipe" });
170
+ rmSync(nsisDir, { recursive: true, force: true });
171
+ return appDir;
172
+ }
173
+ /** Recursively find a file by name */
174
+ function findFile(dir, name) {
175
+ for (const entry of readdirSync(dir)) {
176
+ const full = join(dir, entry);
177
+ try {
178
+ if (statSync(full).isDirectory()) {
179
+ const found = findFile(full, name);
180
+ if (found)
181
+ return found;
182
+ }
183
+ else if (entry === name) {
184
+ return full;
185
+ }
186
+ }
187
+ catch {
188
+ // Skip permission errors
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ /** Check whether the casting APK has been extracted locally */
194
+ export function hasCastingApk() {
195
+ return existsSync(RELEASE_APK_PATH);
196
+ }
197
+ /** Check if casting service is installed on the connected Quest device */
198
+ export async function isCastingInstalled(device) {
199
+ try {
200
+ const output = await execCommand("adb", [
201
+ "-s", device, "shell", "pm", "list", "packages", CASTING_PKG,
202
+ ]);
203
+ return output.includes(CASTING_PKG);
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ /**
210
+ * Install the casting APK onto the Quest device.
211
+ *
212
+ * Uses the release APK which matches the system app's signing certificate.
213
+ * This installs as UPDATED_SYSTEM_APP, preserving privileged permissions.
214
+ */
215
+ export async function installCastingApk(device) {
216
+ if (!hasCastingApk()) {
217
+ throw new Error(`Casting APK not found. Run: quest-dev setup-cast`);
218
+ }
219
+ try {
220
+ await execCommand("adb", ["-s", device, "install", "-r", "-g", RELEASE_APK_PATH]);
221
+ }
222
+ catch (error) {
223
+ throw new Error(`Failed to install casting APK: ${error.message}\n` +
224
+ `Try installing manually: adb -s ${device} install -r -g ${RELEASE_APK_PATH}`);
225
+ }
226
+ }
227
+ /** Ensure casting service is installed, installing if needed. */
228
+ export async function ensureCastingInstalled(device) {
229
+ if (await isCastingInstalled(device)) {
230
+ verbose("Casting service already installed");
231
+ return false;
232
+ }
233
+ // If we don't have the APK locally, try to find MQDH on this machine
234
+ if (!hasCastingApk()) {
235
+ const mqdh = findInstalledMqdh();
236
+ if (mqdh) {
237
+ console.log(`Found MQDH at ${mqdh}, extracting casting APK...`);
238
+ await extractCastingApk(mqdh);
239
+ }
240
+ else {
241
+ throw new Error(`Casting service not installed on Quest and APK not found locally.\n\n` +
242
+ `To fix this, run:\n\n` +
243
+ ` quest-dev setup-cast\n\n` +
244
+ `This will guide you through downloading and extracting the casting APK.`);
245
+ }
246
+ }
247
+ console.log("Installing casting service on Quest...");
248
+ await installCastingApk(device);
249
+ console.log("Casting service installed");
250
+ return true;
251
+ }
252
+ //# sourceMappingURL=casting-apk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casting-apk.js","sourceRoot":"","sources":["../../src/utils/casting-apk.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Config file loading for quest-dev
3
- * Resolves settings from CLI flags → .quest-dev.json → ~/.config/quest-dev/config.json
3
+ * Resolves settings from CLI flags → ~/.config/quest-dev/config.json
4
4
  */
5
5
  export interface QuestDevConfig {
6
6
  pin?: string;
7
+ port?: number;
8
+ host?: string;
9
+ device?: string;
7
10
  idleTimeout?: number;
8
11
  lowBattery?: number;
9
12
  }
10
13
  /**
11
- * Load merged config from all config file locations.
12
- * First file found wins for each field.
14
+ * Load config from ~/.config/quest-dev/config.json
13
15
  */
14
16
  export declare function loadConfig(): QuestDevConfig;
15
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAgBD;;;GAGG;AACH,wBAAgB,UAAU,IAAI,cAAc,CAY3C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAiBzD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAc/C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;GAEG;AACH,wBAAgB,UAAU,IAAI,cAAc,CAO3C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAczD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAc/C"}
@@ -1,65 +1,45 @@
1
1
  /**
2
2
  * Config file loading for quest-dev
3
- * Resolves settings from CLI flags → .quest-dev.json → ~/.config/quest-dev/config.json
3
+ * Resolves settings from CLI flags → ~/.config/quest-dev/config.json
4
4
  */
5
5
  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
6
6
  import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
- const CONFIG_LOCATIONS = [
9
- join(process.cwd(), '.quest-dev.json'),
10
- join(homedir(), '.config', 'quest-dev', 'config.json'),
11
- ];
12
- function tryReadConfig(path) {
8
+ const CONFIG_PATH = join(homedir(), '.config', 'quest-dev', 'config.json');
9
+ /**
10
+ * Load config from ~/.config/quest-dev/config.json
11
+ */
12
+ export function loadConfig() {
13
13
  try {
14
- const content = readFileSync(path, 'utf-8');
14
+ const content = readFileSync(CONFIG_PATH, 'utf-8');
15
15
  return JSON.parse(content);
16
16
  }
17
17
  catch {
18
- return null;
19
- }
20
- }
21
- /**
22
- * Load merged config from all config file locations.
23
- * First file found wins for each field.
24
- */
25
- export function loadConfig() {
26
- const merged = {};
27
- for (const path of CONFIG_LOCATIONS) {
28
- const config = tryReadConfig(path);
29
- if (!config)
30
- continue;
31
- if (merged.pin === undefined && config.pin)
32
- merged.pin = config.pin;
33
- if (merged.idleTimeout === undefined && config.idleTimeout !== undefined)
34
- merged.idleTimeout = config.idleTimeout;
35
- if (merged.lowBattery === undefined && config.lowBattery !== undefined)
36
- merged.lowBattery = config.lowBattery;
18
+ return {};
37
19
  }
38
- return merged;
39
20
  }
40
21
  /**
41
22
  * Save config values to ~/.config/quest-dev/config.json
42
23
  * Merges with existing config (doesn't overwrite unrelated fields).
43
24
  */
44
25
  export function saveConfig(values) {
45
- const configPath = join(homedir(), '.config', 'quest-dev', 'config.json');
46
- let existing = {};
47
- try {
48
- existing = JSON.parse(readFileSync(configPath, 'utf-8'));
49
- }
50
- catch {
51
- // No existing config, start fresh
52
- }
26
+ const existing = loadConfig();
53
27
  const merged = { ...existing };
54
28
  if (values.pin !== undefined)
55
29
  merged.pin = values.pin;
30
+ if (values.port !== undefined)
31
+ merged.port = values.port;
32
+ if (values.host !== undefined)
33
+ merged.host = values.host;
34
+ if (values.device !== undefined)
35
+ merged.device = values.device;
56
36
  if (values.idleTimeout !== undefined)
57
37
  merged.idleTimeout = values.idleTimeout;
58
38
  if (values.lowBattery !== undefined)
59
39
  merged.lowBattery = values.lowBattery;
60
- mkdirSync(dirname(configPath), { recursive: true });
61
- writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n');
62
- return configPath;
40
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
41
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n');
42
+ return CONFIG_PATH;
63
43
  }
64
44
  /**
65
45
  * Resolve PIN from CLI flag, then config files
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAW7B,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;AAE3E;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAsB;IAC/C,MAAM,QAAQ,GAAG,UAAU,EAAE,CAAC;IAE9B,MAAM,MAAM,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAAE,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;IACtD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/D,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;IAC9E,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS;QAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IAE3E,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,MAAe;IACrC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,MAAM,CAAC,GAAG;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAElC,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC3C,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAClE,OAAO,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;IACzE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
@@ -10,6 +10,11 @@ export interface ExecResult {
10
10
  * Execute a shell command and return a promise with stdout
11
11
  */
12
12
  export declare function execCommand(command: string, args?: string[]): Promise<string>;
13
+ /**
14
+ * Execute a shell command with stdout/stderr streamed to the console in real time.
15
+ * Returns the exit code.
16
+ */
17
+ export declare function execCommandStreaming(command: string, args?: string[]): Promise<number>;
13
18
  /**
14
19
  * Execute a shell command and return full result (doesn't throw on non-zero exit)
15
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BjF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BzF"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BjF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BzF"}
@@ -33,6 +33,23 @@ export function execCommand(command, args = []) {
33
33
  proc.on('error', reject);
34
34
  });
35
35
  }
36
+ /**
37
+ * Execute a shell command with stdout/stderr streamed to the console in real time.
38
+ * Returns the exit code.
39
+ */
40
+ export function execCommandStreaming(command, args = []) {
41
+ return new Promise((resolve) => {
42
+ const proc = spawn(command, args, {
43
+ stdio: 'inherit',
44
+ });
45
+ proc.on('close', (code) => {
46
+ resolve(code ?? 1);
47
+ });
48
+ proc.on('error', () => {
49
+ resolve(1);
50
+ });
51
+ });
52
+ }
36
53
  /**
37
54
  * Execute a shell command and return full result (doesn't throw on non-zero exit)
38
55
  */
@@ -1 +1 @@
1
- {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAQtC;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAiB,EAAE;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YAChC,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;YACnE,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,OAAiB,EAAE;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YAChC,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Filename generation utilities for screenshots
3
3
  */
4
+ /**
5
+ * Slugify a caption for use in filenames.
6
+ * Lowercase, replace non-alphanumeric with dashes, collapse runs, trim, truncate to 25 chars.
7
+ */
8
+ export declare function slugifyCaption(caption: string): string;
4
9
  /**
5
10
  * Generate UTC timestamp filename
6
11
  * Format: screenshot-YYYY-MM-DD-HH-MM-SS-Z.jpg
12
+ * With caption: screenshot-YYYY-MM-DD-HH-MM-SS-Z-slug.jpg
7
13
  */
8
- export declare function generateScreenshotFilename(date?: Date): string;
14
+ export declare function generateScreenshotFilename(date?: Date, caption?: string): string;
9
15
  //# sourceMappingURL=filename.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../../src/utils/filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,IAAiB,GAAG,MAAM,CAS1E"}
1
+ {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../../src/utils/filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,IAAiB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAW5F"}
@@ -1,17 +1,32 @@
1
1
  /**
2
2
  * Filename generation utilities for screenshots
3
3
  */
4
+ /**
5
+ * Slugify a caption for use in filenames.
6
+ * Lowercase, replace non-alphanumeric with dashes, collapse runs, trim, truncate to 25 chars.
7
+ */
8
+ export function slugifyCaption(caption) {
9
+ return caption
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-|-$/g, "")
13
+ .slice(0, 25)
14
+ .replace(/-$/, "");
15
+ }
4
16
  /**
5
17
  * Generate UTC timestamp filename
6
18
  * Format: screenshot-YYYY-MM-DD-HH-MM-SS-Z.jpg
19
+ * With caption: screenshot-YYYY-MM-DD-HH-MM-SS-Z-slug.jpg
7
20
  */
8
- export function generateScreenshotFilename(date = new Date()) {
21
+ export function generateScreenshotFilename(date = new Date(), caption) {
9
22
  const year = date.getUTCFullYear();
10
23
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
11
24
  const day = String(date.getUTCDate()).padStart(2, '0');
12
25
  const hours = String(date.getUTCHours()).padStart(2, '0');
13
26
  const minutes = String(date.getUTCMinutes()).padStart(2, '0');
14
27
  const seconds = String(date.getUTCSeconds()).padStart(2, '0');
15
- return `screenshot-${year}-${month}-${day}-${hours}-${minutes}-${seconds}-Z.jpg`;
28
+ const base = `screenshot-${year}-${month}-${day}-${hours}-${minutes}-${seconds}-Z`;
29
+ const slug = caption ? slugifyCaption(caption) : "";
30
+ return slug ? `${base}-${slug}.jpg` : `${base}.jpg`;
16
31
  }
17
32
  //# sourceMappingURL=filename.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filename.js","sourceRoot":"","sources":["../../src/utils/filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAAa,IAAI,IAAI,EAAE;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAE9D,OAAO,cAAc,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,QAAQ,CAAC;AACnF,CAAC"}
1
+ {"version":3,"file":"filename.js","sourceRoot":"","sources":["../../src/utils/filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,OAAO,OAAO;SACX,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAAa,IAAI,IAAI,EAAE,EAAE,OAAgB;IAClF,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAE9D,MAAM,IAAI,GAAG,cAAc,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,IAAI,CAAC;IACnF,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC;AACtD,CAAC"}
@@ -2,7 +2,7 @@
2
2
  * Tests for filename generation utilities
3
3
  */
4
4
  import { describe, it, expect } from 'vitest';
5
- import { generateScreenshotFilename } from './filename.js';
5
+ import { generateScreenshotFilename, slugifyCaption } from './filename.js';
6
6
  describe('generateScreenshotFilename', () => {
7
7
  it('formats UTC timestamp correctly', () => {
8
8
  const date = new Date('2026-01-15T14:30:45Z');
@@ -19,6 +19,21 @@ describe('generateScreenshotFilename', () => {
19
19
  expect(generateScreenshotFilename(date))
20
20
  .toBe('screenshot-2026-12-31-00-00-00-Z.jpg');
21
21
  });
22
+ it('includes slugified caption in filename', () => {
23
+ const date = new Date('2026-01-15T14:30:45Z');
24
+ expect(generateScreenshotFilename(date, 'Main Menu Screenshot'))
25
+ .toBe('screenshot-2026-01-15-14-30-45-Z-main-menu-screenshot.jpg');
26
+ });
27
+ it('truncates long captions to 25 chars', () => {
28
+ const date = new Date('2026-01-15T14:30:45Z');
29
+ expect(generateScreenshotFilename(date, 'This is a very long caption that should be truncated'))
30
+ .toBe('screenshot-2026-01-15-14-30-45-Z-this-is-a-very-long-capti.jpg');
31
+ });
32
+ it('omits slug when no caption provided', () => {
33
+ const date = new Date('2026-01-15T14:30:45Z');
34
+ expect(generateScreenshotFilename(date))
35
+ .toBe('screenshot-2026-01-15-14-30-45-Z.jpg');
36
+ });
22
37
  it('generates filename with current time when no date provided', () => {
23
38
  const filename = generateScreenshotFilename();
24
39
  // Verify format is correct
@@ -37,4 +52,21 @@ describe('generateScreenshotFilename', () => {
37
52
  }
38
53
  });
39
54
  });
55
+ describe('slugifyCaption', () => {
56
+ it('lowercases and replaces spaces with dashes', () => {
57
+ expect(slugifyCaption('Main Menu')).toBe('main-menu');
58
+ });
59
+ it('replaces non-alphanumeric with dashes', () => {
60
+ expect(slugifyCaption('Hello, World! #1')).toBe('hello-world-1');
61
+ });
62
+ it('collapses consecutive dashes', () => {
63
+ expect(slugifyCaption('foo---bar')).toBe('foo-bar');
64
+ });
65
+ it('trims leading and trailing dashes', () => {
66
+ expect(slugifyCaption('--hello--')).toBe('hello');
67
+ });
68
+ it('truncates to 25 chars without trailing dash', () => {
69
+ expect(slugifyCaption('this is a very long caption that exceeds')).toBe('this-is-a-very-long-capti');
70
+ });
71
+ });
40
72
  //# sourceMappingURL=filename.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filename.test.js","sourceRoot":"","sources":["../../src/utils/filename.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAE3D,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,QAAQ,GAAG,0BAA0B,EAAE,CAAC;QAE9C,2BAA2B;QAC3B,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,yDAAyD,CAAC,CAAC;QAEpF,kCAAkC;QAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACpG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAE7B,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC;YAC5D,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CACpC,QAAQ,CAAC,IAAI,CAAC,EACd,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EACnB,QAAQ,CAAC,GAAG,CAAC,EACb,QAAQ,CAAC,KAAK,CAAC,EACf,QAAQ,CAAC,OAAO,CAAC,EACjB,QAAQ,CAAC,OAAO,CAAC,CAClB,CAAC,CAAC;YAEH,0DAA0D;YAC1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC;YACpD,MAAM,CAAC,IAAI,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,yBAAyB;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"filename.test.js","sourceRoot":"","sources":["../../src/utils/filename.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,0BAA0B,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE3E,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC;aAC7D,IAAI,CAAC,2DAA2D,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,EAAE,sDAAsD,CAAC,CAAC;aAC7F,IAAI,CAAC,gEAAgE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;aACrC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,QAAQ,GAAG,0BAA0B,EAAE,CAAC;QAE9C,2BAA2B;QAC3B,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,yDAAyD,CAAC,CAAC;QAEpF,kCAAkC;QAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACpG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAE7B,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC;YAC5D,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CACpC,QAAQ,CAAC,IAAI,CAAC,EACd,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EACnB,QAAQ,CAAC,GAAG,CAAC,EACb,QAAQ,CAAC,KAAK,CAAC,EACf,QAAQ,CAAC,OAAO,CAAC,EACjB,QAAQ,CAAC,OAAO,CAAC,CAClB,CAAC,CAAC;YAEH,0DAA0D;YAC1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC;YACpD,MAAM,CAAC,IAAI,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,yBAAyB;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,cAAc,CAAC,0CAA0C,CAAC,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACvG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * JPEG COM marker insertion.
3
+ * Inserts a COM (0xFFFE) marker after the SOI marker without re-encoding.
4
+ */
5
+ /**
6
+ * Insert a COM comment marker into a JPEG buffer.
7
+ * Splices after the SOI (FF D8) marker — no re-encoding, no quality loss.
8
+ */
9
+ export declare function insertJpegComment(jpeg: Buffer, comment: string): Buffer;
10
+ /**
11
+ * Add a COM comment to a JPEG file in-place.
12
+ */
13
+ export declare function addJpegFileComment(filePath: string, comment: string): void;
14
+ //# sourceMappingURL=jpeg-comment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jpeg-comment.d.ts","sourceRoot":"","sources":["../../src/utils/jpeg-comment.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CASvE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI1E"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * JPEG COM marker insertion.
3
+ * Inserts a COM (0xFFFE) marker after the SOI marker without re-encoding.
4
+ */
5
+ import { readFileSync, writeFileSync } from "fs";
6
+ /**
7
+ * Insert a COM comment marker into a JPEG buffer.
8
+ * Splices after the SOI (FF D8) marker — no re-encoding, no quality loss.
9
+ */
10
+ export function insertJpegComment(jpeg, comment) {
11
+ if (jpeg[0] !== 0xff || jpeg[1] !== 0xd8) {
12
+ throw new Error("Not a valid JPEG (missing SOI marker)");
13
+ }
14
+ const payload = Buffer.from(comment, "utf-8");
15
+ const marker = Buffer.alloc(4);
16
+ marker.writeUInt16BE(0xfffe, 0);
17
+ marker.writeUInt16BE(payload.length + 2, 2);
18
+ return Buffer.concat([jpeg.subarray(0, 2), marker, payload, jpeg.subarray(2)]);
19
+ }
20
+ /**
21
+ * Add a COM comment to a JPEG file in-place.
22
+ */
23
+ export function addJpegFileComment(filePath, comment) {
24
+ const jpeg = readFileSync(filePath);
25
+ const result = insertJpegComment(jpeg, comment);
26
+ writeFileSync(filePath, result);
27
+ }
28
+ //# sourceMappingURL=jpeg-comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jpeg-comment.js","sourceRoot":"","sources":["../../src/utils/jpeg-comment.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,OAAe;IAC7D,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC"}