@creativeintelligence/abbie 0.1.5 → 0.1.7

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/bin/dev.js +1 -49
  2. package/bin/run.js +42 -49
  3. package/dist/cli/commands/login.js +26 -0
  4. package/dist/cli/commands/project/add.d.ts +0 -1
  5. package/dist/cli/commands/project/add.js +16 -52
  6. package/dist/cli/commands/project/list.js +13 -93
  7. package/dist/cli/commands/project/remove.d.ts +0 -2
  8. package/dist/cli/commands/project/remove.js +11 -28
  9. package/dist/cli/commands/session/list.js +3 -12
  10. package/dist/cli/commands/session/mark-done.js +1 -7
  11. package/dist/cli/commands/session/start.d.ts +0 -1
  12. package/dist/cli/commands/session/start.js +5 -7
  13. package/dist/lib/active-sessions.d.ts +0 -12
  14. package/dist/lib/active-sessions.js +6 -175
  15. package/dist/lib/project-path.d.ts +6 -0
  16. package/dist/lib/project-path.js +21 -0
  17. package/dist/lib.d.ts +1 -2
  18. package/dist/lib.js +2 -4
  19. package/oclif.manifest.json +2569 -6368
  20. package/package.json +1 -1
  21. package/dist/cli/commands/backlog/add.d.ts +0 -22
  22. package/dist/cli/commands/backlog/add.js +0 -65
  23. package/dist/cli/commands/backlog/claim.d.ts +0 -19
  24. package/dist/cli/commands/backlog/claim.js +0 -45
  25. package/dist/cli/commands/backlog/complete.d.ts +0 -18
  26. package/dist/cli/commands/backlog/complete.js +0 -42
  27. package/dist/cli/commands/backlog/list.d.ts +0 -20
  28. package/dist/cli/commands/backlog/list.js +0 -91
  29. package/dist/cli/commands/backlog/pick.d.ts +0 -18
  30. package/dist/cli/commands/backlog/pick.js +0 -42
  31. package/dist/cli/commands/backlog/sync.d.ts +0 -24
  32. package/dist/cli/commands/backlog/sync.js +0 -109
  33. package/dist/cli/commands/daemon.d.ts +0 -56
  34. package/dist/cli/commands/daemon.js +0 -1465
  35. package/dist/cli/commands/docs/lint.d.ts +0 -18
  36. package/dist/cli/commands/docs/lint.js +0 -82
  37. package/dist/cli/commands/docs/sync.d.ts +0 -19
  38. package/dist/cli/commands/docs/sync.js +0 -76
  39. package/dist/cli/commands/gc.d.ts +0 -29
  40. package/dist/cli/commands/gc.js +0 -211
  41. package/dist/cli/commands/index.d.ts +0 -36
  42. package/dist/cli/commands/index.js +0 -228
  43. package/dist/cli/commands/panes/broker.d.ts +0 -17
  44. package/dist/cli/commands/panes/broker.js +0 -57
  45. package/dist/cli/commands/panes/pipe-sink.d.ts +0 -17
  46. package/dist/cli/commands/panes/pipe-sink.js +0 -90
  47. package/dist/cli/commands/panes/snapshot.d.ts +0 -20
  48. package/dist/cli/commands/panes/snapshot.js +0 -125
  49. package/dist/cli/commands/preview/init.d.ts +0 -25
  50. package/dist/cli/commands/preview/init.js +0 -159
  51. package/dist/cli/commands/preview/sync.d.ts +0 -23
  52. package/dist/cli/commands/preview/sync.js +0 -144
  53. package/dist/cli/commands/preview/watch.d.ts +0 -24
  54. package/dist/cli/commands/preview/watch.js +0 -153
  55. package/dist/cli/commands/resource/acquire.d.ts +0 -21
  56. package/dist/cli/commands/resource/acquire.js +0 -90
  57. package/dist/cli/commands/resource/list.d.ts +0 -15
  58. package/dist/cli/commands/resource/list.js +0 -61
  59. package/dist/cli/commands/resource/release.d.ts +0 -18
  60. package/dist/cli/commands/resource/release.js +0 -50
  61. package/dist/cli/commands/resource/wait.d.ts +0 -21
  62. package/dist/cli/commands/resource/wait.js +0 -73
  63. package/dist/cli/commands/session/view.d.ts +0 -24
  64. package/dist/cli/commands/session/view.js +0 -145
  65. package/dist/cli/commands/start.d.ts +0 -37
  66. package/dist/cli/commands/start.js +0 -234
  67. package/dist/cli/commands/triage/claim.d.ts +0 -23
  68. package/dist/cli/commands/triage/claim.js +0 -186
  69. package/dist/cli/commands/triage/list.d.ts +0 -22
  70. package/dist/cli/commands/triage/list.js +0 -112
  71. package/dist/cli/commands/triage/next.d.ts +0 -18
  72. package/dist/cli/commands/triage/next.js +0 -63
  73. package/dist/cli/commands/triage/pull.d.ts +0 -19
  74. package/dist/cli/commands/triage/pull.js +0 -82
  75. package/dist/cli/commands/triage/stats.d.ts +0 -16
  76. package/dist/cli/commands/triage/stats.js +0 -69
  77. package/dist/cli/commands/tunnel/list.d.ts +0 -16
  78. package/dist/cli/commands/tunnel/list.js +0 -98
  79. package/dist/cli/commands/tunnel/start.d.ts +0 -24
  80. package/dist/cli/commands/tunnel/start.js +0 -107
  81. package/dist/cli/commands/tunnel/stop.d.ts +0 -20
  82. package/dist/cli/commands/tunnel/stop.js +0 -90
  83. package/dist/cli/commands/tunnel/url.d.ts +0 -21
  84. package/dist/cli/commands/tunnel/url.js +0 -70
  85. package/dist/cli/commands/windows/context.d.ts +0 -18
  86. package/dist/cli/commands/windows/context.js +0 -326
  87. package/dist/cli/commands/windows/focus.d.ts +0 -17
  88. package/dist/cli/commands/windows/focus.js +0 -103
  89. package/dist/cli/commands/windows/list.d.ts +0 -21
  90. package/dist/cli/commands/windows/list.js +0 -172
  91. package/dist/cli/commands/windows/map.d.ts +0 -17
  92. package/dist/cli/commands/windows/map.js +0 -168
  93. package/dist/cli/commands/windows/read.d.ts +0 -21
  94. package/dist/cli/commands/windows/read.js +0 -241
  95. package/dist/cli/commands/windows/search.d.ts +0 -24
  96. package/dist/cli/commands/windows/search.js +0 -171
  97. package/dist/cli/commands/windows/show.d.ts +0 -19
  98. package/dist/cli/commands/windows/show.js +0 -165
  99. package/dist/cli/commands/windows/watch.d.ts +0 -19
  100. package/dist/cli/commands/windows/watch.js +0 -241
  101. package/dist/lib/managed-session.d.ts +0 -27
  102. package/dist/lib/managed-session.js +0 -105
  103. package/dist/lib/panes/broker.d.ts +0 -130
  104. package/dist/lib/panes/broker.js +0 -97
  105. package/dist/lib/panes/index.d.ts +0 -2
  106. package/dist/lib/panes/index.js +0 -1
  107. package/dist/lib/panes/server.d.ts +0 -17
  108. package/dist/lib/panes/server.js +0 -308
  109. package/dist/lib/preview/manager.d.ts +0 -77
  110. package/dist/lib/preview/manager.js +0 -369
  111. package/dist/lib/preview/schema.d.ts +0 -2
  112. package/dist/lib/preview/schema.js +0 -32
  113. package/dist/lib/preview/sprite.d.ts +0 -85
  114. package/dist/lib/preview/sprite.js +0 -321
  115. package/dist/lib/preview/watcher.d.ts +0 -63
  116. package/dist/lib/preview/watcher.js +0 -185
  117. package/dist/lib/project-identity.d.ts +0 -16
  118. package/dist/lib/project-identity.js +0 -75
  119. package/dist/lib/tmux/bridge.d.ts +0 -133
  120. package/dist/lib/tmux/bridge.js +0 -315
  121. package/dist/lib/tmux/context.d.ts +0 -82
  122. package/dist/lib/tmux/context.js +0 -239
  123. package/dist/lib/tmux/index.d.ts +0 -8
  124. package/dist/lib/tmux/index.js +0 -11
  125. package/dist/lib/tmux/map.d.ts +0 -57
  126. package/dist/lib/tmux/map.js +0 -198
  127. package/dist/lib/tmux/panes.d.ts +0 -27
  128. package/dist/lib/tmux/panes.js +0 -151
  129. package/dist/lib/tmux/redaction.d.ts +0 -57
  130. package/dist/lib/tmux/redaction.js +0 -152
  131. package/dist/lib/web/analytics.d.ts +0 -63
  132. package/dist/lib/web/analytics.js +0 -168
  133. package/dist/lib/web/server.d.ts +0 -26
  134. package/dist/lib/web/server.js +0 -697
  135. package/dist/lib/web/tmux-bridge.d.ts +0 -7
  136. package/dist/lib/web/tmux-bridge.js +0 -7
  137. package/dist/lib/windows/index.d.ts +0 -3
  138. package/dist/lib/windows/index.js +0 -2
  139. package/dist/lib/windows/inventory.d.ts +0 -21
  140. package/dist/lib/windows/inventory.js +0 -263
  141. package/dist/lib/windows/types.d.ts +0 -46
  142. 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
- }