@creativeintelligence/abbie 0.1.6 → 0.1.8

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 (141) hide show
  1. package/bin/dev.js +1 -49
  2. package/bin/run.js +42 -49
  3. package/dist/cli/commands/project/add.d.ts +0 -1
  4. package/dist/cli/commands/project/add.js +16 -52
  5. package/dist/cli/commands/project/list.js +13 -93
  6. package/dist/cli/commands/project/remove.d.ts +0 -2
  7. package/dist/cli/commands/project/remove.js +11 -28
  8. package/dist/cli/commands/session/list.js +3 -12
  9. package/dist/cli/commands/session/mark-done.js +1 -7
  10. package/dist/cli/commands/session/start.d.ts +0 -1
  11. package/dist/cli/commands/session/start.js +5 -7
  12. package/dist/lib/active-sessions.d.ts +0 -12
  13. package/dist/lib/active-sessions.js +6 -175
  14. package/dist/lib/project-path.d.ts +6 -0
  15. package/dist/lib/project-path.js +21 -0
  16. package/dist/lib.d.ts +1 -2
  17. package/dist/lib.js +2 -4
  18. package/oclif.manifest.json +2569 -6368
  19. package/package.json +21 -10
  20. package/dist/cli/commands/backlog/add.d.ts +0 -22
  21. package/dist/cli/commands/backlog/add.js +0 -65
  22. package/dist/cli/commands/backlog/claim.d.ts +0 -19
  23. package/dist/cli/commands/backlog/claim.js +0 -45
  24. package/dist/cli/commands/backlog/complete.d.ts +0 -18
  25. package/dist/cli/commands/backlog/complete.js +0 -42
  26. package/dist/cli/commands/backlog/list.d.ts +0 -20
  27. package/dist/cli/commands/backlog/list.js +0 -91
  28. package/dist/cli/commands/backlog/pick.d.ts +0 -18
  29. package/dist/cli/commands/backlog/pick.js +0 -42
  30. package/dist/cli/commands/backlog/sync.d.ts +0 -24
  31. package/dist/cli/commands/backlog/sync.js +0 -109
  32. package/dist/cli/commands/daemon.d.ts +0 -56
  33. package/dist/cli/commands/daemon.js +0 -1465
  34. package/dist/cli/commands/docs/lint.d.ts +0 -18
  35. package/dist/cli/commands/docs/lint.js +0 -82
  36. package/dist/cli/commands/docs/sync.d.ts +0 -19
  37. package/dist/cli/commands/docs/sync.js +0 -76
  38. package/dist/cli/commands/gc.d.ts +0 -29
  39. package/dist/cli/commands/gc.js +0 -211
  40. package/dist/cli/commands/index.d.ts +0 -36
  41. package/dist/cli/commands/index.js +0 -228
  42. package/dist/cli/commands/panes/broker.d.ts +0 -17
  43. package/dist/cli/commands/panes/broker.js +0 -57
  44. package/dist/cli/commands/panes/pipe-sink.d.ts +0 -17
  45. package/dist/cli/commands/panes/pipe-sink.js +0 -90
  46. package/dist/cli/commands/panes/snapshot.d.ts +0 -20
  47. package/dist/cli/commands/panes/snapshot.js +0 -125
  48. package/dist/cli/commands/preview/init.d.ts +0 -25
  49. package/dist/cli/commands/preview/init.js +0 -159
  50. package/dist/cli/commands/preview/sync.d.ts +0 -23
  51. package/dist/cli/commands/preview/sync.js +0 -144
  52. package/dist/cli/commands/preview/watch.d.ts +0 -24
  53. package/dist/cli/commands/preview/watch.js +0 -153
  54. package/dist/cli/commands/resource/acquire.d.ts +0 -21
  55. package/dist/cli/commands/resource/acquire.js +0 -90
  56. package/dist/cli/commands/resource/list.d.ts +0 -15
  57. package/dist/cli/commands/resource/list.js +0 -61
  58. package/dist/cli/commands/resource/release.d.ts +0 -18
  59. package/dist/cli/commands/resource/release.js +0 -50
  60. package/dist/cli/commands/resource/wait.d.ts +0 -21
  61. package/dist/cli/commands/resource/wait.js +0 -73
  62. package/dist/cli/commands/session/view.d.ts +0 -24
  63. package/dist/cli/commands/session/view.js +0 -145
  64. package/dist/cli/commands/start.d.ts +0 -37
  65. package/dist/cli/commands/start.js +0 -234
  66. package/dist/cli/commands/triage/claim.d.ts +0 -23
  67. package/dist/cli/commands/triage/claim.js +0 -186
  68. package/dist/cli/commands/triage/list.d.ts +0 -22
  69. package/dist/cli/commands/triage/list.js +0 -112
  70. package/dist/cli/commands/triage/next.d.ts +0 -18
  71. package/dist/cli/commands/triage/next.js +0 -63
  72. package/dist/cli/commands/triage/pull.d.ts +0 -19
  73. package/dist/cli/commands/triage/pull.js +0 -82
  74. package/dist/cli/commands/triage/stats.d.ts +0 -16
  75. package/dist/cli/commands/triage/stats.js +0 -69
  76. package/dist/cli/commands/tunnel/list.d.ts +0 -16
  77. package/dist/cli/commands/tunnel/list.js +0 -98
  78. package/dist/cli/commands/tunnel/start.d.ts +0 -24
  79. package/dist/cli/commands/tunnel/start.js +0 -107
  80. package/dist/cli/commands/tunnel/stop.d.ts +0 -20
  81. package/dist/cli/commands/tunnel/stop.js +0 -90
  82. package/dist/cli/commands/tunnel/url.d.ts +0 -21
  83. package/dist/cli/commands/tunnel/url.js +0 -70
  84. package/dist/cli/commands/windows/context.d.ts +0 -18
  85. package/dist/cli/commands/windows/context.js +0 -326
  86. package/dist/cli/commands/windows/focus.d.ts +0 -17
  87. package/dist/cli/commands/windows/focus.js +0 -103
  88. package/dist/cli/commands/windows/list.d.ts +0 -21
  89. package/dist/cli/commands/windows/list.js +0 -172
  90. package/dist/cli/commands/windows/map.d.ts +0 -17
  91. package/dist/cli/commands/windows/map.js +0 -168
  92. package/dist/cli/commands/windows/read.d.ts +0 -21
  93. package/dist/cli/commands/windows/read.js +0 -241
  94. package/dist/cli/commands/windows/search.d.ts +0 -24
  95. package/dist/cli/commands/windows/search.js +0 -171
  96. package/dist/cli/commands/windows/show.d.ts +0 -19
  97. package/dist/cli/commands/windows/show.js +0 -165
  98. package/dist/cli/commands/windows/watch.d.ts +0 -19
  99. package/dist/cli/commands/windows/watch.js +0 -241
  100. package/dist/lib/managed-session.d.ts +0 -27
  101. package/dist/lib/managed-session.js +0 -105
  102. package/dist/lib/panes/broker.d.ts +0 -130
  103. package/dist/lib/panes/broker.js +0 -97
  104. package/dist/lib/panes/index.d.ts +0 -2
  105. package/dist/lib/panes/index.js +0 -1
  106. package/dist/lib/panes/server.d.ts +0 -17
  107. package/dist/lib/panes/server.js +0 -308
  108. package/dist/lib/preview/manager.d.ts +0 -77
  109. package/dist/lib/preview/manager.js +0 -369
  110. package/dist/lib/preview/schema.d.ts +0 -2
  111. package/dist/lib/preview/schema.js +0 -32
  112. package/dist/lib/preview/sprite.d.ts +0 -85
  113. package/dist/lib/preview/sprite.js +0 -321
  114. package/dist/lib/preview/watcher.d.ts +0 -63
  115. package/dist/lib/preview/watcher.js +0 -185
  116. package/dist/lib/project-identity.d.ts +0 -16
  117. package/dist/lib/project-identity.js +0 -75
  118. package/dist/lib/tmux/bridge.d.ts +0 -133
  119. package/dist/lib/tmux/bridge.js +0 -315
  120. package/dist/lib/tmux/context.d.ts +0 -82
  121. package/dist/lib/tmux/context.js +0 -239
  122. package/dist/lib/tmux/index.d.ts +0 -8
  123. package/dist/lib/tmux/index.js +0 -11
  124. package/dist/lib/tmux/map.d.ts +0 -57
  125. package/dist/lib/tmux/map.js +0 -198
  126. package/dist/lib/tmux/panes.d.ts +0 -27
  127. package/dist/lib/tmux/panes.js +0 -151
  128. package/dist/lib/tmux/redaction.d.ts +0 -57
  129. package/dist/lib/tmux/redaction.js +0 -152
  130. package/dist/lib/web/analytics.d.ts +0 -63
  131. package/dist/lib/web/analytics.js +0 -168
  132. package/dist/lib/web/server.d.ts +0 -26
  133. package/dist/lib/web/server.js +0 -697
  134. package/dist/lib/web/tmux-bridge.d.ts +0 -7
  135. package/dist/lib/web/tmux-bridge.js +0 -7
  136. package/dist/lib/windows/index.d.ts +0 -3
  137. package/dist/lib/windows/index.js +0 -2
  138. package/dist/lib/windows/inventory.d.ts +0 -21
  139. package/dist/lib/windows/inventory.js +0 -263
  140. package/dist/lib/windows/types.d.ts +0 -46
  141. package/dist/lib/windows/types.js +0 -1
@@ -1,321 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createWriteStream, existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { basename, join } from "node:path";
5
- import { createGzip } from "node:zlib";
6
- import { pack } from "tar-fs";
7
- /**
8
- * Run a sprite CLI command and return the result
9
- */
10
- export function runSprite(args) {
11
- return new Promise((resolve) => {
12
- const child = spawn("sprite", args, {
13
- stdio: ["ignore", "pipe", "pipe"],
14
- });
15
- let stdout = "";
16
- let stderr = "";
17
- child.stdout.on("data", (data) => {
18
- stdout += data.toString();
19
- });
20
- child.stderr.on("data", (data) => {
21
- stderr += data.toString();
22
- });
23
- child.on("close", (code) => {
24
- if (code === 0) {
25
- resolve({ success: true, stdout, stderr });
26
- }
27
- else {
28
- resolve({ success: false, stdout, stderr, error: stderr || `Exit code ${code}` });
29
- }
30
- });
31
- child.on("error", (err) => {
32
- resolve({ success: false, stdout: "", stderr: "", error: err.message });
33
- });
34
- });
35
- }
36
- /**
37
- * Create a new sprite
38
- */
39
- export async function createSprite(name) {
40
- return runSprite(["create", name, "-skip-console"]);
41
- }
42
- /**
43
- * Destroy a sprite
44
- */
45
- export async function destroySprite(name) {
46
- return runSprite(["destroy", "-s", name]);
47
- }
48
- /**
49
- * Set sprite URL to public access
50
- */
51
- export async function makeUrlPublic(spriteName) {
52
- return runSprite(["url", "update", "-s", spriteName, "--auth", "public"]);
53
- }
54
- /**
55
- * Get sprite URL
56
- */
57
- export async function getSpriteUrl(spriteName) {
58
- const result = await runSprite(["url", "-s", spriteName]);
59
- if (!result.success)
60
- return null;
61
- // Parse URL from output (format: "URL: https://...")
62
- const lines = result.stdout.split("\n");
63
- for (const line of lines) {
64
- if (line.includes("http")) {
65
- // Extract URL from line
66
- const match = line.match(/https?:\/\/[^\s]+/);
67
- if (match)
68
- return match[0];
69
- }
70
- }
71
- return null;
72
- }
73
- /**
74
- * Check if a sprite exists by listing all sprites
75
- */
76
- export async function spriteExists(name) {
77
- const result = await runSprite(["list"]);
78
- if (!result.success)
79
- return false;
80
- // Check if sprite name appears in the output
81
- return result.stdout.includes(name);
82
- }
83
- /**
84
- * Execute a command on a sprite
85
- */
86
- export async function execOnSprite(spriteName, command, options) {
87
- const args = ["exec", "-s", spriteName];
88
- if (options?.dir) {
89
- args.push("-dir", options.dir);
90
- }
91
- if (options?.env && Object.keys(options.env).length > 0) {
92
- const envStr = Object.entries(options.env)
93
- .map(([k, v]) => `${k}=${v}`)
94
- .join(",");
95
- args.push("-env", envStr);
96
- }
97
- if (options?.files) {
98
- for (const file of options.files) {
99
- args.push("-file", `${file.local}:${file.remote}`);
100
- }
101
- }
102
- args.push("bash", "-c", command);
103
- return runSprite(args);
104
- }
105
- /**
106
- * Create a tarball of a project excluding common build artifacts
107
- */
108
- /** Directories/files to exclude from tarball and watch (checked against full path and basename) */
109
- export const EXCLUDE_PATTERNS = [
110
- "node_modules",
111
- ".git",
112
- ".next",
113
- ".turbo",
114
- "dist",
115
- ".cache",
116
- ".vercel",
117
- ".convex",
118
- ".pnpm",
119
- "coverage",
120
- ".DS_Store",
121
- "playwright-report",
122
- "test-results",
123
- ".playwright-mcp", // Large playwright screenshots
124
- ];
125
- /** File extensions to exclude from tarball and watch (reduce size for large assets) */
126
- export const EXCLUDE_EXTENSIONS = [
127
- ".png",
128
- ".svg",
129
- ".jpg",
130
- ".jpeg",
131
- ".gif",
132
- ".webp",
133
- ".mp4",
134
- ".mov",
135
- ];
136
- export async function createProjectTarball(projectPath) {
137
- const tarPath = join(tmpdir(), `sprite-sync-${Date.now()}.tar.gz`);
138
- const ignore = (fullPath) => {
139
- // Get the basename of the path
140
- const name = basename(fullPath);
141
- // Check file extension
142
- const ext = name.toLowerCase().slice(name.lastIndexOf("."));
143
- if (EXCLUDE_EXTENSIONS.includes(ext)) {
144
- return true;
145
- }
146
- // Check if basename matches any exclude pattern
147
- if (EXCLUDE_PATTERNS.includes(name)) {
148
- return true;
149
- }
150
- // Check if any segment of the path matches
151
- const segments = fullPath.split("/");
152
- return segments.some((seg) => EXCLUDE_PATTERNS.includes(seg));
153
- };
154
- return new Promise((resolve, reject) => {
155
- const writeStream = createWriteStream(tarPath);
156
- const gzip = createGzip();
157
- pack(projectPath, { ignore })
158
- .pipe(gzip)
159
- .pipe(writeStream)
160
- .on("finish", () => resolve(tarPath))
161
- .on("error", reject);
162
- });
163
- }
164
- /**
165
- * Sync project files to sprite:
166
- * 1. Create tarball locally
167
- * 2. Upload via sprite exec -file
168
- * 3. Extract on remote
169
- */
170
- export async function syncToSprite(spriteName, projectPath, remotePath = "/app") {
171
- const startTime = Date.now();
172
- // Create tarball
173
- let tarPath;
174
- try {
175
- tarPath = await createProjectTarball(projectPath);
176
- }
177
- catch (err) {
178
- return {
179
- success: false,
180
- error: `Failed to create tarball: ${err instanceof Error ? err.message : String(err)}`,
181
- };
182
- }
183
- const tarName = basename(tarPath);
184
- const remoteTar = `/tmp/${tarName}`;
185
- // Upload and extract
186
- const result = await execOnSprite(spriteName, `mkdir -p ${remotePath} && tar --overwrite -xzf ${remoteTar} -C ${remotePath} && rm ${remoteTar}`, {
187
- files: [{ local: tarPath, remote: remoteTar }],
188
- });
189
- // Clean up local tarball
190
- try {
191
- unlinkSync(tarPath);
192
- }
193
- catch {
194
- // Ignore cleanup errors
195
- }
196
- const syncDurationMs = Date.now() - startTime;
197
- if (!result.success) {
198
- return {
199
- success: false,
200
- tarPath,
201
- error: result.error || result.stderr,
202
- syncDurationMs,
203
- };
204
- }
205
- return {
206
- success: true,
207
- tarPath,
208
- syncDurationMs,
209
- };
210
- }
211
- /**
212
- * Check if a process is running on the sprite
213
- */
214
- export async function isProcessRunning(spriteName, processPattern) {
215
- const result = await execOnSprite(spriteName, `pgrep -f "${processPattern}" || true`);
216
- return result.success && result.stdout.trim().length > 0;
217
- }
218
- /**
219
- * Kill processes matching a pattern on the sprite
220
- * Note: Uses lsof to find PIDs since pkill is not allowed
221
- */
222
- export async function killProcess(spriteName, processPattern) {
223
- // Find PIDs matching the pattern, then kill them individually
224
- // Using lsof and grep instead of pkill
225
- const findAndKill = `
226
- pids=$(ps aux | grep -E "${processPattern}" | grep -v grep | awk '{print $2}')
227
- if [ -n "$pids" ]; then
228
- for pid in $pids; do
229
- kill -9 $pid 2>/dev/null || true
230
- done
231
- echo "Killed PIDs: $pids"
232
- else
233
- echo "No matching processes found"
234
- fi
235
- `;
236
- return execOnSprite(spriteName, findAndKill);
237
- }
238
- /**
239
- * Start a dev server on the sprite with health checking.
240
- *
241
- * Uses TTY session for proper PATH/environment on the sprite.
242
- * Pipes output to a local log file so the detached process persists
243
- * after the CLI exits. Polls the sprite URL until healthy (up to 90s).
244
- */
245
- export async function startDevServer(spriteName, devCommand, options) {
246
- const dir = options?.dir || "/app";
247
- const port = options?.port || 8080;
248
- // Ensure the command binds to 0.0.0.0 and uses the correct port
249
- let cmd = devCommand;
250
- if (!cmd.includes("-H") && !cmd.includes("--hostname")) {
251
- if (cmd.includes("next dev") || cmd.includes("bun run dev")) {
252
- cmd = cmd.replace(/-p\s*\d+/g, ""); // Remove existing port
253
- cmd = `${cmd} -p ${port} -H 0.0.0.0`;
254
- }
255
- }
256
- // Build environment string for -env flag
257
- const envStr = options?.env && Object.keys(options.env).length > 0
258
- ? Object.entries(options.env)
259
- .map(([k, v]) => `${k}=${v}`)
260
- .join(",")
261
- : "";
262
- // Kill any existing dev server first
263
- await killProcess(spriteName, "next dev|bun run dev|node.*dev");
264
- const fullCmd = `cd ${dir} && ${cmd}`;
265
- // Use TTY session for proper PATH/environment on the sprite
266
- const args = ["exec", "-s", spriteName, "-tty"];
267
- if (envStr) {
268
- args.push("-env", envStr);
269
- }
270
- args.push("bash", "-c", fullCmd);
271
- // Pipe output to a local log file so the detached process persists
272
- const logDir = join(tmpdir(), "agents-preview");
273
- if (!existsSync(logDir))
274
- mkdirSync(logDir, { recursive: true });
275
- const logPath = join(logDir, `${spriteName}.log`);
276
- const logStream = createWriteStream(logPath, { flags: "a" });
277
- const child = spawn("sprite", args, {
278
- detached: true,
279
- stdio: ["ignore", "pipe", "pipe"],
280
- });
281
- // Pipe stdout/stderr to log file to keep the process alive
282
- if (child.stdout)
283
- child.stdout.pipe(logStream);
284
- if (child.stderr)
285
- child.stderr.pipe(logStream);
286
- child.unref();
287
- // Write PID for later cleanup
288
- const pidPath = join(logDir, `${spriteName}.pid`);
289
- writeFileSync(pidPath, String(child.pid));
290
- // Health check: poll until server responds or timeout
291
- const healthUrl = options?.healthCheckUrl || (await getSpriteUrl(spriteName));
292
- if (healthUrl) {
293
- const timeoutMs = options?.healthCheckTimeoutMs ?? 90_000;
294
- const pollIntervalMs = 3000;
295
- const deadline = Date.now() + timeoutMs;
296
- while (Date.now() < deadline) {
297
- try {
298
- const resp = await fetch(healthUrl, {
299
- method: "HEAD",
300
- signal: AbortSignal.timeout(5000),
301
- });
302
- if (resp.ok || resp.status === 308 || resp.status === 307 || resp.status === 302) {
303
- return { success: true, stdout: `Server healthy (${resp.status})`, stderr: "" };
304
- }
305
- }
306
- catch {
307
- // Server not ready yet
308
- }
309
- await new Promise((r) => setTimeout(r, pollIntervalMs));
310
- }
311
- return {
312
- success: false,
313
- stdout: "",
314
- stderr: "",
315
- error: `Server did not become healthy within ${timeoutMs / 1000}s`,
316
- };
317
- }
318
- // No URL to check — fall back to a brief wait
319
- await new Promise((r) => setTimeout(r, 3000));
320
- return { success: true, stdout: "Server starting...", stderr: "" };
321
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * File watcher for continuous sprite sync.
3
- *
4
- * Uses node:fs watch() with recursive mode (macOS FSEvents).
5
- * Debounces changes and serializes syncs to prevent overlap.
6
- */
7
- export interface WatcherConfig {
8
- /** Absolute path to the project directory */
9
- projectPath: string;
10
- /** Sprite name for sync target */
11
- spriteName: string;
12
- /** Remote path on the sprite (default: "/app") */
13
- remotePath?: string;
14
- /** Debounce delay in ms (default: 1500) */
15
- debounceMs?: number;
16
- /** Whether to run install when lockfiles change (default: true) */
17
- autoInstall?: boolean;
18
- /** Package manager for install command */
19
- packageManager?: "pnpm" | "bun" | "npm";
20
- /** Doppler project for secrets */
21
- dopplerProject?: string;
22
- /** NPM token for private packages */
23
- npmToken?: string;
24
- /** AbortSignal for clean shutdown */
25
- signal: AbortSignal;
26
- }
27
- export type WatcherEvent = {
28
- type: "started";
29
- projectPath: string;
30
- } | {
31
- type: "sync_start";
32
- fileCount: number;
33
- needsInstall: boolean;
34
- } | {
35
- type: "sync_complete";
36
- syncMs: number;
37
- installMs: number;
38
- totalMs: number;
39
- } | {
40
- type: "sync_error";
41
- error: string;
42
- retryable: boolean;
43
- } | {
44
- type: "stopped";
45
- };
46
- export interface WatcherStats {
47
- totalSyncs: number;
48
- totalErrors: number;
49
- lastSyncMs: number | null;
50
- lastSyncAt: Date | null;
51
- queuedFiles: number;
52
- syncing: boolean;
53
- }
54
- export type WatcherEventHandler = (event: WatcherEvent) => void;
55
- export interface WatcherHandle {
56
- /** Resolves when the watcher has fully stopped */
57
- done: Promise<void>;
58
- /** Get current stats */
59
- stats: () => WatcherStats;
60
- }
61
- export declare function shouldIgnoreFile(filename: string): boolean;
62
- export declare function startWatcher(config: WatcherConfig, onEvent: WatcherEventHandler): WatcherHandle;
63
- //# sourceMappingURL=watcher.d.ts.map
@@ -1,185 +0,0 @@
1
- /**
2
- * File watcher for continuous sprite sync.
3
- *
4
- * Uses node:fs watch() with recursive mode (macOS FSEvents).
5
- * Debounces changes and serializes syncs to prevent overlap.
6
- */
7
- import { watch } from "node:fs";
8
- import { basename } from "node:path";
9
- import { getInstallCommand } from "./detect.js";
10
- import { EXCLUDE_EXTENSIONS, EXCLUDE_PATTERNS, execOnSprite, syncToSprite } from "./sprite.js";
11
- // ── Lockfile detection ─────────────────────────────────────────────────────
12
- const LOCKFILE_NAMES = new Set([
13
- "package.json",
14
- "pnpm-lock.yaml",
15
- "bun.lock",
16
- "bun.lockb",
17
- "package-lock.json",
18
- "yarn.lock",
19
- ]);
20
- function isLockfileChange(filename) {
21
- return LOCKFILE_NAMES.has(basename(filename));
22
- }
23
- // ── File filter ────────────────────────────────────────────────────────────
24
- const excludeSet = new Set(EXCLUDE_PATTERNS);
25
- const extSet = new Set(EXCLUDE_EXTENSIONS);
26
- export function shouldIgnoreFile(filename) {
27
- const name = basename(filename);
28
- // Check extension
29
- const dotIdx = name.lastIndexOf(".");
30
- if (dotIdx !== -1) {
31
- const ext = name.slice(dotIdx).toLowerCase();
32
- if (extSet.has(ext))
33
- return true;
34
- }
35
- // Check basename
36
- if (excludeSet.has(name))
37
- return true;
38
- // Check path segments
39
- const segments = filename.split("/");
40
- return segments.some((seg) => excludeSet.has(seg));
41
- }
42
- // ── Watch engine ───────────────────────────────────────────────────────────
43
- export function startWatcher(config, onEvent) {
44
- const { projectPath, spriteName, remotePath = "/app", debounceMs = 1500, autoInstall = true, packageManager = "npm", dopplerProject, npmToken, signal, } = config;
45
- // State
46
- let fsWatcher = null;
47
- let debounceTimer = null;
48
- let syncInFlight = false;
49
- let pendingSyncQueued = false;
50
- let needsInstall = false;
51
- const pendingChanges = new Set();
52
- const stats = {
53
- totalSyncs: 0,
54
- totalErrors: 0,
55
- lastSyncMs: null,
56
- lastSyncAt: null,
57
- queuedFiles: 0,
58
- syncing: false,
59
- };
60
- // Sync cycle
61
- async function runSync() {
62
- if (signal.aborted)
63
- return;
64
- syncInFlight = true;
65
- stats.syncing = true;
66
- const fileCount = pendingChanges.size;
67
- const shouldInstall = needsInstall && autoInstall;
68
- // Snapshot and clear
69
- pendingChanges.clear();
70
- needsInstall = false;
71
- stats.queuedFiles = 0;
72
- onEvent({ type: "sync_start", fileCount, needsInstall: shouldInstall });
73
- const totalStart = Date.now();
74
- let syncMs = 0;
75
- let installMs = 0;
76
- try {
77
- const syncResult = await syncToSprite(spriteName, projectPath, remotePath);
78
- syncMs = syncResult.syncDurationMs ?? 0;
79
- if (!syncResult.success) {
80
- throw new Error(syncResult.error ?? "Sync failed");
81
- }
82
- // Install if lockfiles changed
83
- if (shouldInstall) {
84
- const installStart = Date.now();
85
- let installCmd = getInstallCommand(packageManager);
86
- const env = {};
87
- if (npmToken)
88
- env.NPM_TOKEN = npmToken;
89
- if (dopplerProject) {
90
- installCmd = `doppler run -p ${dopplerProject} -c dev -- ${installCmd}`;
91
- }
92
- const installResult = await execOnSprite(spriteName, installCmd, {
93
- dir: remotePath,
94
- env,
95
- });
96
- installMs = Date.now() - installStart;
97
- if (!installResult.success) {
98
- throw new Error(`Install failed: ${installResult.error}`);
99
- }
100
- }
101
- const totalMs = Date.now() - totalStart;
102
- stats.totalSyncs++;
103
- stats.lastSyncMs = totalMs;
104
- stats.lastSyncAt = new Date();
105
- onEvent({ type: "sync_complete", syncMs, installMs, totalMs });
106
- }
107
- catch (err) {
108
- stats.totalErrors++;
109
- const message = err instanceof Error ? err.message : String(err);
110
- onEvent({ type: "sync_error", error: message, retryable: true });
111
- }
112
- finally {
113
- syncInFlight = false;
114
- stats.syncing = false;
115
- stats.queuedFiles = pendingChanges.size;
116
- // If changes accumulated during sync, trigger another cycle
117
- if (pendingSyncQueued && pendingChanges.size > 0 && !signal.aborted) {
118
- pendingSyncQueued = false;
119
- runSync();
120
- }
121
- else {
122
- pendingSyncQueued = false;
123
- }
124
- }
125
- }
126
- // Debounced trigger
127
- function scheduleSync() {
128
- if (debounceTimer)
129
- clearTimeout(debounceTimer);
130
- stats.queuedFiles = pendingChanges.size;
131
- debounceTimer = setTimeout(() => {
132
- debounceTimer = null;
133
- if (syncInFlight) {
134
- pendingSyncQueued = true;
135
- return;
136
- }
137
- runSync();
138
- }, debounceMs);
139
- }
140
- // Done promise — resolves when watcher fully stops
141
- const done = new Promise((resolve) => {
142
- const shutdown = async () => {
143
- if (debounceTimer)
144
- clearTimeout(debounceTimer);
145
- fsWatcher?.close();
146
- fsWatcher = null;
147
- // Wait for in-flight sync (max 30s)
148
- if (syncInFlight) {
149
- const deadline = Date.now() + 30_000;
150
- while (syncInFlight && Date.now() < deadline) {
151
- await new Promise((r) => setTimeout(r, 200));
152
- }
153
- }
154
- onEvent({ type: "stopped" });
155
- resolve();
156
- };
157
- signal.addEventListener("abort", () => shutdown(), { once: true });
158
- // If already aborted, shut down immediately
159
- if (signal.aborted) {
160
- shutdown();
161
- }
162
- });
163
- // Start watching
164
- fsWatcher = watch(projectPath, { recursive: true }, (_eventType, filename) => {
165
- if (!filename)
166
- return;
167
- if (signal.aborted)
168
- return;
169
- if (shouldIgnoreFile(filename))
170
- return;
171
- pendingChanges.add(filename);
172
- if (isLockfileChange(filename)) {
173
- needsInstall = true;
174
- }
175
- scheduleSync();
176
- });
177
- fsWatcher.on("error", (err) => {
178
- onEvent({ type: "sync_error", error: `Watcher error: ${err.message}`, retryable: false });
179
- });
180
- onEvent({ type: "started", projectPath });
181
- return {
182
- done,
183
- stats: () => ({ ...stats, queuedFiles: pendingChanges.size }),
184
- };
185
- }
@@ -1,16 +0,0 @@
1
- export declare const PROJECT_IDENTITY_FALLBACK_EMOJIS: readonly string[];
2
- export declare const PROJECT_IDENTITY_COLOR_HEXES: readonly string[];
3
- export declare function normalizeProjectName(name: string): string;
4
- export declare function projectIdentityHash(name: string): number;
5
- export declare function normalizeProjectEmoji(emoji?: string | null): string | undefined;
6
- export declare function normalizeProjectColorHex(color?: string | null): string | undefined;
7
- export declare function resolveProjectIdentity(name: string, identity?: {
8
- emoji?: string | null;
9
- color?: string | null;
10
- }): {
11
- emoji: string;
12
- color: string;
13
- };
14
- export declare function resolveProjectEmoji(name: string, emoji?: string | null): string;
15
- export declare function resolveProjectColor(name: string, color?: string | null): string;
16
- //# sourceMappingURL=project-identity.d.ts.map
@@ -1,75 +0,0 @@
1
- // Generated by @lnittman/icons (scripts/sync-clients.mjs).
2
- export const PROJECT_IDENTITY_FALLBACK_EMOJIS = [
3
- "🌑",
4
- "🌙",
5
- "🔮",
6
- "🎱",
7
- "👻",
8
- "🦇",
9
- "🕳️",
10
- "🪨",
11
- "🌿",
12
- "🌊",
13
- "☁️",
14
- "⚡",
15
- ];
16
- export const PROJECT_IDENTITY_COLOR_HEXES = [
17
- "#bec2c8",
18
- "#d67600",
19
- "#c74440",
20
- "#9f3f4f",
21
- "#c4a000",
22
- "#5f8c50",
23
- "#2aa889",
24
- "#00857c",
25
- "#1b8dbf",
26
- "#3067c6",
27
- "#5b3fc5",
28
- "#8338ec",
29
- "#a4457a",
30
- "#506480",
31
- "#8b6f47",
32
- "#2d3748",
33
- ];
34
- const DEFAULT_PROJECT_EMOJI = "📁";
35
- const DEFAULT_PROJECT_COLOR = PROJECT_IDENTITY_COLOR_HEXES[0] ?? "#bec2c8";
36
- export function normalizeProjectName(name) {
37
- return name.trim().toLowerCase();
38
- }
39
- export function projectIdentityHash(name) {
40
- const normalized = normalizeProjectName(name);
41
- if (!normalized)
42
- return 0;
43
- let hash = 0;
44
- for (let i = 0; i < normalized.length; i++) {
45
- hash = (Math.imul(hash, 31) + normalized.charCodeAt(i)) | 0;
46
- }
47
- return Math.abs(hash);
48
- }
49
- export function normalizeProjectEmoji(emoji) {
50
- const trimmed = emoji?.trim();
51
- return trimmed ? trimmed : undefined;
52
- }
53
- export function normalizeProjectColorHex(color) {
54
- const trimmed = color?.trim();
55
- if (!trimmed)
56
- return undefined;
57
- return /^#[0-9a-fA-F]{6}$/.test(trimmed) ? trimmed.toLowerCase() : undefined;
58
- }
59
- export function resolveProjectIdentity(name, identity) {
60
- const hash = projectIdentityHash(name);
61
- return {
62
- emoji: normalizeProjectEmoji(identity?.emoji) ??
63
- PROJECT_IDENTITY_FALLBACK_EMOJIS[hash % PROJECT_IDENTITY_FALLBACK_EMOJIS.length] ??
64
- DEFAULT_PROJECT_EMOJI,
65
- color: normalizeProjectColorHex(identity?.color) ??
66
- PROJECT_IDENTITY_COLOR_HEXES[hash % PROJECT_IDENTITY_COLOR_HEXES.length] ??
67
- DEFAULT_PROJECT_COLOR,
68
- };
69
- }
70
- export function resolveProjectEmoji(name, emoji) {
71
- return resolveProjectIdentity(name, { emoji }).emoji;
72
- }
73
- export function resolveProjectColor(name, color) {
74
- return resolveProjectIdentity(name, { color }).color;
75
- }