@creativeintelligence/abbie 0.1.6 → 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.
- package/bin/dev.js +1 -49
- package/bin/run.js +42 -49
- package/dist/cli/commands/project/add.d.ts +0 -1
- package/dist/cli/commands/project/add.js +16 -52
- package/dist/cli/commands/project/list.js +13 -93
- package/dist/cli/commands/project/remove.d.ts +0 -2
- package/dist/cli/commands/project/remove.js +11 -28
- package/dist/cli/commands/session/list.js +3 -12
- package/dist/cli/commands/session/mark-done.js +1 -7
- package/dist/cli/commands/session/start.d.ts +0 -1
- package/dist/cli/commands/session/start.js +5 -7
- package/dist/lib/active-sessions.d.ts +0 -12
- package/dist/lib/active-sessions.js +6 -175
- package/dist/lib/project-path.d.ts +6 -0
- package/dist/lib/project-path.js +21 -0
- package/dist/lib.d.ts +1 -2
- package/dist/lib.js +2 -4
- package/oclif.manifest.json +2569 -6368
- package/package.json +1 -1
- package/dist/cli/commands/backlog/add.d.ts +0 -22
- package/dist/cli/commands/backlog/add.js +0 -65
- package/dist/cli/commands/backlog/claim.d.ts +0 -19
- package/dist/cli/commands/backlog/claim.js +0 -45
- package/dist/cli/commands/backlog/complete.d.ts +0 -18
- package/dist/cli/commands/backlog/complete.js +0 -42
- package/dist/cli/commands/backlog/list.d.ts +0 -20
- package/dist/cli/commands/backlog/list.js +0 -91
- package/dist/cli/commands/backlog/pick.d.ts +0 -18
- package/dist/cli/commands/backlog/pick.js +0 -42
- package/dist/cli/commands/backlog/sync.d.ts +0 -24
- package/dist/cli/commands/backlog/sync.js +0 -109
- package/dist/cli/commands/daemon.d.ts +0 -56
- package/dist/cli/commands/daemon.js +0 -1465
- package/dist/cli/commands/docs/lint.d.ts +0 -18
- package/dist/cli/commands/docs/lint.js +0 -82
- package/dist/cli/commands/docs/sync.d.ts +0 -19
- package/dist/cli/commands/docs/sync.js +0 -76
- package/dist/cli/commands/gc.d.ts +0 -29
- package/dist/cli/commands/gc.js +0 -211
- package/dist/cli/commands/index.d.ts +0 -36
- package/dist/cli/commands/index.js +0 -228
- package/dist/cli/commands/panes/broker.d.ts +0 -17
- package/dist/cli/commands/panes/broker.js +0 -57
- package/dist/cli/commands/panes/pipe-sink.d.ts +0 -17
- package/dist/cli/commands/panes/pipe-sink.js +0 -90
- package/dist/cli/commands/panes/snapshot.d.ts +0 -20
- package/dist/cli/commands/panes/snapshot.js +0 -125
- package/dist/cli/commands/preview/init.d.ts +0 -25
- package/dist/cli/commands/preview/init.js +0 -159
- package/dist/cli/commands/preview/sync.d.ts +0 -23
- package/dist/cli/commands/preview/sync.js +0 -144
- package/dist/cli/commands/preview/watch.d.ts +0 -24
- package/dist/cli/commands/preview/watch.js +0 -153
- package/dist/cli/commands/resource/acquire.d.ts +0 -21
- package/dist/cli/commands/resource/acquire.js +0 -90
- package/dist/cli/commands/resource/list.d.ts +0 -15
- package/dist/cli/commands/resource/list.js +0 -61
- package/dist/cli/commands/resource/release.d.ts +0 -18
- package/dist/cli/commands/resource/release.js +0 -50
- package/dist/cli/commands/resource/wait.d.ts +0 -21
- package/dist/cli/commands/resource/wait.js +0 -73
- package/dist/cli/commands/session/view.d.ts +0 -24
- package/dist/cli/commands/session/view.js +0 -145
- package/dist/cli/commands/start.d.ts +0 -37
- package/dist/cli/commands/start.js +0 -234
- package/dist/cli/commands/triage/claim.d.ts +0 -23
- package/dist/cli/commands/triage/claim.js +0 -186
- package/dist/cli/commands/triage/list.d.ts +0 -22
- package/dist/cli/commands/triage/list.js +0 -112
- package/dist/cli/commands/triage/next.d.ts +0 -18
- package/dist/cli/commands/triage/next.js +0 -63
- package/dist/cli/commands/triage/pull.d.ts +0 -19
- package/dist/cli/commands/triage/pull.js +0 -82
- package/dist/cli/commands/triage/stats.d.ts +0 -16
- package/dist/cli/commands/triage/stats.js +0 -69
- package/dist/cli/commands/tunnel/list.d.ts +0 -16
- package/dist/cli/commands/tunnel/list.js +0 -98
- package/dist/cli/commands/tunnel/start.d.ts +0 -24
- package/dist/cli/commands/tunnel/start.js +0 -107
- package/dist/cli/commands/tunnel/stop.d.ts +0 -20
- package/dist/cli/commands/tunnel/stop.js +0 -90
- package/dist/cli/commands/tunnel/url.d.ts +0 -21
- package/dist/cli/commands/tunnel/url.js +0 -70
- package/dist/cli/commands/windows/context.d.ts +0 -18
- package/dist/cli/commands/windows/context.js +0 -326
- package/dist/cli/commands/windows/focus.d.ts +0 -17
- package/dist/cli/commands/windows/focus.js +0 -103
- package/dist/cli/commands/windows/list.d.ts +0 -21
- package/dist/cli/commands/windows/list.js +0 -172
- package/dist/cli/commands/windows/map.d.ts +0 -17
- package/dist/cli/commands/windows/map.js +0 -168
- package/dist/cli/commands/windows/read.d.ts +0 -21
- package/dist/cli/commands/windows/read.js +0 -241
- package/dist/cli/commands/windows/search.d.ts +0 -24
- package/dist/cli/commands/windows/search.js +0 -171
- package/dist/cli/commands/windows/show.d.ts +0 -19
- package/dist/cli/commands/windows/show.js +0 -165
- package/dist/cli/commands/windows/watch.d.ts +0 -19
- package/dist/cli/commands/windows/watch.js +0 -241
- package/dist/lib/managed-session.d.ts +0 -27
- package/dist/lib/managed-session.js +0 -105
- package/dist/lib/panes/broker.d.ts +0 -130
- package/dist/lib/panes/broker.js +0 -97
- package/dist/lib/panes/index.d.ts +0 -2
- package/dist/lib/panes/index.js +0 -1
- package/dist/lib/panes/server.d.ts +0 -17
- package/dist/lib/panes/server.js +0 -308
- package/dist/lib/preview/manager.d.ts +0 -77
- package/dist/lib/preview/manager.js +0 -369
- package/dist/lib/preview/schema.d.ts +0 -2
- package/dist/lib/preview/schema.js +0 -32
- package/dist/lib/preview/sprite.d.ts +0 -85
- package/dist/lib/preview/sprite.js +0 -321
- package/dist/lib/preview/watcher.d.ts +0 -63
- package/dist/lib/preview/watcher.js +0 -185
- package/dist/lib/project-identity.d.ts +0 -16
- package/dist/lib/project-identity.js +0 -75
- package/dist/lib/tmux/bridge.d.ts +0 -133
- package/dist/lib/tmux/bridge.js +0 -315
- package/dist/lib/tmux/context.d.ts +0 -82
- package/dist/lib/tmux/context.js +0 -239
- package/dist/lib/tmux/index.d.ts +0 -8
- package/dist/lib/tmux/index.js +0 -11
- package/dist/lib/tmux/map.d.ts +0 -57
- package/dist/lib/tmux/map.js +0 -198
- package/dist/lib/tmux/panes.d.ts +0 -27
- package/dist/lib/tmux/panes.js +0 -151
- package/dist/lib/tmux/redaction.d.ts +0 -57
- package/dist/lib/tmux/redaction.js +0 -152
- package/dist/lib/web/analytics.d.ts +0 -63
- package/dist/lib/web/analytics.js +0 -168
- package/dist/lib/web/server.d.ts +0 -26
- package/dist/lib/web/server.js +0 -697
- package/dist/lib/web/tmux-bridge.d.ts +0 -7
- package/dist/lib/web/tmux-bridge.js +0 -7
- package/dist/lib/windows/index.d.ts +0 -3
- package/dist/lib/windows/index.js +0 -2
- package/dist/lib/windows/inventory.d.ts +0 -21
- package/dist/lib/windows/inventory.js +0 -263
- package/dist/lib/windows/types.d.ts +0 -46
- 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
|
-
}
|