@getjack/jack 0.1.0 → 0.1.1
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/README.md +16 -0
- package/package.json +47 -39
- package/src/commands/agents.ts +40 -9
- package/src/commands/cloud.ts +8 -4
- package/src/commands/down.ts +120 -69
- package/src/commands/init.ts +41 -3
- package/src/commands/mcp.ts +18 -0
- package/src/commands/new.ts +64 -334
- package/src/commands/projects.ts +139 -143
- package/src/commands/services.ts +315 -0
- package/src/commands/ship.ts +33 -139
- package/src/index.ts +27 -3
- package/src/lib/agent-files.ts +0 -41
- package/src/lib/agents.ts +238 -64
- package/src/lib/cloudflare-api.ts +3 -2
- package/src/lib/config.ts +8 -0
- package/src/lib/errors.ts +53 -0
- package/src/lib/hooks.ts +93 -41
- package/src/lib/mcp-config.ts +175 -0
- package/src/lib/project-operations.ts +793 -0
- package/src/lib/prompts.ts +15 -7
- package/src/lib/registry.ts +29 -1
- package/src/lib/services/db.ts +81 -0
- package/src/lib/services/index.ts +27 -0
- package/src/lib/telemetry.ts +10 -1
- package/src/mcp/README.md +142 -0
- package/src/mcp/resources/index.ts +87 -0
- package/src/mcp/server.ts +32 -0
- package/src/mcp/tools/index.ts +261 -0
- package/src/mcp/types.ts +29 -0
- package/src/mcp/utils.ts +147 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/types.ts +16 -8
- package/templates/CLAUDE.md +105 -4
- package/templates/api/.jack.json +20 -1
- package/templates/api/src/index.ts +1 -1
- package/templates/miniapp/.jack.json +7 -5
package/src/lib/agents.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
+
import { delimiter, extname, join } from "node:path";
|
|
5
|
+
import { type AgentConfig, type AgentLaunchConfig, readConfig, writeConfig } from "./config.ts";
|
|
4
6
|
|
|
5
7
|
// Re-export AgentConfig for consumers
|
|
6
8
|
export type { AgentConfig } from "./config.ts";
|
|
@@ -20,16 +22,20 @@ export interface ProjectFile {
|
|
|
20
22
|
export interface AgentDefinition {
|
|
21
23
|
id: string;
|
|
22
24
|
name: string;
|
|
23
|
-
searchPaths: string[];
|
|
24
25
|
projectFiles: ProjectFile[];
|
|
25
26
|
priority: number; // Lower = higher priority for default selection (claude-code=1, codex=2, etc.)
|
|
27
|
+
launch?: AgentLaunchDefinition;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentLaunchDefinition {
|
|
31
|
+
cliCommands?: Array<{ command: string; args?: string[] }>;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* Result of scanning for agents
|
|
30
36
|
*/
|
|
31
37
|
export interface DetectionResult {
|
|
32
|
-
detected: Array<{ id: string; path: string }>;
|
|
38
|
+
detected: Array<{ id: string; path: string; launch?: AgentLaunchConfig }>;
|
|
33
39
|
total: number;
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -51,11 +57,9 @@ export const AGENT_REGISTRY: AgentDefinition[] = [
|
|
|
51
57
|
id: "claude-code",
|
|
52
58
|
name: "Claude Code",
|
|
53
59
|
priority: 1,
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
"%APPDATA%/Claude", // Windows
|
|
58
|
-
],
|
|
60
|
+
launch: {
|
|
61
|
+
cliCommands: [{ command: "claude" }],
|
|
62
|
+
},
|
|
59
63
|
projectFiles: [
|
|
60
64
|
{ path: "CLAUDE.md", template: "claude-md" },
|
|
61
65
|
{ path: "AGENTS.md", template: "agents-md", shared: true },
|
|
@@ -65,41 +69,11 @@ export const AGENT_REGISTRY: AgentDefinition[] = [
|
|
|
65
69
|
id: "codex",
|
|
66
70
|
name: "Codex",
|
|
67
71
|
priority: 2,
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
],
|
|
72
|
+
launch: {
|
|
73
|
+
cliCommands: [{ command: "codex" }],
|
|
74
|
+
},
|
|
72
75
|
projectFiles: [{ path: "AGENTS.md", template: "agents-md", shared: true }],
|
|
73
76
|
},
|
|
74
|
-
{
|
|
75
|
-
id: "cursor",
|
|
76
|
-
name: "Cursor",
|
|
77
|
-
priority: 10,
|
|
78
|
-
searchPaths: [
|
|
79
|
-
"/Applications/Cursor.app",
|
|
80
|
-
"~/.cursor",
|
|
81
|
-
"%PROGRAMFILES%/Cursor", // Windows
|
|
82
|
-
"/usr/share/cursor", // Linux
|
|
83
|
-
],
|
|
84
|
-
projectFiles: [
|
|
85
|
-
{ path: ".cursorrules", template: "cursorrules" },
|
|
86
|
-
{ path: "AGENTS.md", template: "agents-md", shared: true },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "windsurf",
|
|
91
|
-
name: "Windsurf",
|
|
92
|
-
priority: 10,
|
|
93
|
-
searchPaths: [
|
|
94
|
-
"/Applications/Windsurf.app",
|
|
95
|
-
"~/.windsurf",
|
|
96
|
-
"%PROGRAMFILES%/Windsurf", // Windows
|
|
97
|
-
],
|
|
98
|
-
projectFiles: [
|
|
99
|
-
{ path: ".windsurfrules", template: "windsurfrules" },
|
|
100
|
-
{ path: "AGENTS.md", template: "agents-md", shared: true },
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
77
|
];
|
|
104
78
|
|
|
105
79
|
/**
|
|
@@ -127,6 +101,72 @@ export function pathExists(path: string): boolean {
|
|
|
127
101
|
}
|
|
128
102
|
}
|
|
129
103
|
|
|
104
|
+
function findExecutable(command: string): string | null {
|
|
105
|
+
const expanded = expandPath(command);
|
|
106
|
+
if (expanded.includes("/") || expanded.includes("\\")) {
|
|
107
|
+
return existsSync(expanded) ? expanded : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const pathEnv = process.env.PATH ?? "";
|
|
111
|
+
const paths = pathEnv.split(delimiter).filter(Boolean);
|
|
112
|
+
|
|
113
|
+
if (process.platform === "win32") {
|
|
114
|
+
const extension = extname(command);
|
|
115
|
+
const extensions =
|
|
116
|
+
extension.length > 0
|
|
117
|
+
? [""]
|
|
118
|
+
: (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";");
|
|
119
|
+
|
|
120
|
+
for (const basePath of paths) {
|
|
121
|
+
for (const ext of extensions) {
|
|
122
|
+
const candidate = join(basePath, `${command}${ext}`);
|
|
123
|
+
if (existsSync(candidate)) {
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const basePath of paths) {
|
|
132
|
+
const candidate = join(basePath, command);
|
|
133
|
+
if (existsSync(candidate)) {
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveCliLaunch(definition: AgentDefinition): AgentLaunchConfig | null {
|
|
142
|
+
const candidates = definition.launch?.cliCommands ?? [];
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
const resolved = findExecutable(candidate.command);
|
|
145
|
+
if (resolved) {
|
|
146
|
+
return { type: "cli", command: resolved, args: candidate.args };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveAgentLaunch(definition: AgentDefinition): AgentLaunchConfig | null {
|
|
153
|
+
return resolveCliLaunch(definition);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeLaunchConfig(launch: AgentLaunchConfig): AgentLaunchConfig | null {
|
|
157
|
+
if (launch.type === "cli") {
|
|
158
|
+
const resolved = findExecutable(launch.command);
|
|
159
|
+
if (!resolved) return null;
|
|
160
|
+
return { type: "cli", command: resolved, args: launch.args };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getLaunchPath(launch: AgentLaunchConfig): string | null {
|
|
166
|
+
if (launch.type === "cli") return launch.command;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
130
170
|
/**
|
|
131
171
|
* Get agent definition by ID
|
|
132
172
|
*/
|
|
@@ -135,17 +175,16 @@ export function getAgentDefinition(id: string): AgentDefinition | undefined {
|
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
/**
|
|
138
|
-
* Scan for installed agents by checking
|
|
178
|
+
* Scan for installed agents by checking launch commands
|
|
139
179
|
*/
|
|
140
180
|
export async function scanAgents(): Promise<DetectionResult> {
|
|
141
|
-
const detected: Array<{ id: string; path: string }> = [];
|
|
181
|
+
const detected: Array<{ id: string; path: string; launch?: AgentLaunchConfig }> = [];
|
|
142
182
|
|
|
143
183
|
for (const agent of AGENT_REGISTRY) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
184
|
+
const launch = resolveAgentLaunch(agent);
|
|
185
|
+
const installPath = launch ? getLaunchPath(launch) : null;
|
|
186
|
+
if (launch && installPath && pathExists(installPath)) {
|
|
187
|
+
detected.push({ id: agent.id, path: installPath, launch });
|
|
149
188
|
}
|
|
150
189
|
}
|
|
151
190
|
|
|
@@ -194,25 +233,25 @@ export async function updateAgent(id: string, config: AgentConfig): Promise<void
|
|
|
194
233
|
/**
|
|
195
234
|
* Add agent to config (auto-detect or use custom path)
|
|
196
235
|
*/
|
|
197
|
-
export async function addAgent(
|
|
236
|
+
export async function addAgent(
|
|
237
|
+
id: string,
|
|
238
|
+
options: { launch?: AgentLaunchConfig } = {},
|
|
239
|
+
): Promise<void> {
|
|
198
240
|
const definition = getAgentDefinition(id);
|
|
199
241
|
if (!definition) {
|
|
200
242
|
throw new Error(`Unknown agent: ${id}`);
|
|
201
243
|
}
|
|
202
244
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
245
|
+
const launchOverride = options.launch ? normalizeLaunchConfig(options.launch) : null;
|
|
246
|
+
const detectedLaunch = launchOverride ?? resolveAgentLaunch(definition);
|
|
247
|
+
const detectedPath = detectedLaunch ? getLaunchPath(detectedLaunch) : null;
|
|
248
|
+
|
|
249
|
+
if (!detectedLaunch) {
|
|
250
|
+
throw new Error(`Could not detect ${definition.name}`);
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
if (!detectedPath) {
|
|
215
|
-
throw new Error(`Could not
|
|
254
|
+
throw new Error(`Could not determine install path for ${definition.name}`);
|
|
216
255
|
}
|
|
217
256
|
|
|
218
257
|
if (!pathExists(detectedPath)) {
|
|
@@ -221,8 +260,9 @@ export async function addAgent(id: string, path?: string): Promise<void> {
|
|
|
221
260
|
|
|
222
261
|
await updateAgent(id, {
|
|
223
262
|
active: true,
|
|
224
|
-
path: detectedPath,
|
|
263
|
+
path: expandPath(detectedPath),
|
|
225
264
|
detectedAt: new Date().toISOString(),
|
|
265
|
+
launch: detectedLaunch,
|
|
226
266
|
});
|
|
227
267
|
}
|
|
228
268
|
|
|
@@ -287,10 +327,12 @@ export async function validateAgentPaths(): Promise<ValidationResult> {
|
|
|
287
327
|
|
|
288
328
|
for (const [id, agentConfig] of Object.entries(agents)) {
|
|
289
329
|
if (agentConfig.active) {
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
const launch = agentConfig.launch ? normalizeLaunchConfig(agentConfig.launch) : null;
|
|
331
|
+
const path = launch ? getLaunchPath(launch) : agentConfig.path;
|
|
332
|
+
if (launch && path && pathExists(path)) {
|
|
333
|
+
valid.push({ id, path });
|
|
292
334
|
} else {
|
|
293
|
-
invalid.push({ id, path: agentConfig.path });
|
|
335
|
+
invalid.push({ id, path: path ?? agentConfig.path });
|
|
294
336
|
}
|
|
295
337
|
}
|
|
296
338
|
}
|
|
@@ -298,6 +340,138 @@ export async function validateAgentPaths(): Promise<ValidationResult> {
|
|
|
298
340
|
return { valid, invalid };
|
|
299
341
|
}
|
|
300
342
|
|
|
343
|
+
function launchConfigsEqual(
|
|
344
|
+
left?: AgentLaunchConfig | null,
|
|
345
|
+
right?: AgentLaunchConfig | null,
|
|
346
|
+
): boolean {
|
|
347
|
+
if (!left || !right) return left === right;
|
|
348
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function getAgentLaunch(id: string): Promise<AgentLaunchConfig | null> {
|
|
352
|
+
const definition = getAgentDefinition(id);
|
|
353
|
+
if (!definition) return null;
|
|
354
|
+
|
|
355
|
+
const config = await readConfig();
|
|
356
|
+
const agentConfig = config?.agents?.[id];
|
|
357
|
+
|
|
358
|
+
const normalized = agentConfig?.launch ? normalizeLaunchConfig(agentConfig.launch) : null;
|
|
359
|
+
if (normalized) {
|
|
360
|
+
if (agentConfig && config && !launchConfigsEqual(normalized, agentConfig.launch)) {
|
|
361
|
+
agentConfig.launch = normalized;
|
|
362
|
+
await writeConfig(config);
|
|
363
|
+
}
|
|
364
|
+
return normalized;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const resolved = resolveAgentLaunch(definition);
|
|
368
|
+
if (resolved && agentConfig && config) {
|
|
369
|
+
if (!launchConfigsEqual(resolved, agentConfig.launch)) {
|
|
370
|
+
agentConfig.launch = resolved;
|
|
371
|
+
await writeConfig(config);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return resolved ?? null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function getPreferredLaunchAgent(): Promise<
|
|
378
|
+
| {
|
|
379
|
+
id: string;
|
|
380
|
+
definition: AgentDefinition;
|
|
381
|
+
launch: AgentLaunchConfig;
|
|
382
|
+
}
|
|
383
|
+
| null
|
|
384
|
+
> {
|
|
385
|
+
const config = await readConfig();
|
|
386
|
+
if (!config?.agents) return null;
|
|
387
|
+
|
|
388
|
+
const activeAgents: Array<{ id: string; config: AgentConfig; definition: AgentDefinition }> = [];
|
|
389
|
+
for (const [id, agentConfig] of Object.entries(config.agents)) {
|
|
390
|
+
if (!agentConfig.active) continue;
|
|
391
|
+
const definition = getAgentDefinition(id);
|
|
392
|
+
if (definition) {
|
|
393
|
+
activeAgents.push({ id, config: agentConfig, definition });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (activeAgents.length === 0) return null;
|
|
398
|
+
|
|
399
|
+
if (config.preferredAgent) {
|
|
400
|
+
const preferred = activeAgents.find((agent) => agent.id === config.preferredAgent);
|
|
401
|
+
if (preferred) {
|
|
402
|
+
const launch = await getAgentLaunch(preferred.id);
|
|
403
|
+
if (launch) {
|
|
404
|
+
return { id: preferred.id, definition: preferred.definition, launch };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
activeAgents.sort((a, b) => a.definition.priority - b.definition.priority);
|
|
410
|
+
for (const agent of activeAgents) {
|
|
411
|
+
const launch = await getAgentLaunch(agent.id);
|
|
412
|
+
if (launch) {
|
|
413
|
+
return { id: agent.id, definition: agent.definition, launch };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function buildLaunchCommand(
|
|
421
|
+
launch: AgentLaunchConfig,
|
|
422
|
+
projectDir: string,
|
|
423
|
+
):
|
|
424
|
+
| {
|
|
425
|
+
command: string;
|
|
426
|
+
args: string[];
|
|
427
|
+
options: { cwd?: string; stdio: "inherit" | "ignore"; detached?: boolean };
|
|
428
|
+
waitForExit: boolean;
|
|
429
|
+
}
|
|
430
|
+
| null {
|
|
431
|
+
if (launch.type !== "cli") return null;
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
command: launch.command,
|
|
435
|
+
args: launch.args ?? [],
|
|
436
|
+
options: { cwd: projectDir, stdio: "inherit" },
|
|
437
|
+
waitForExit: true,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function launchAgent(
|
|
442
|
+
launch: AgentLaunchConfig,
|
|
443
|
+
projectDir: string,
|
|
444
|
+
): Promise<{ success: boolean; error?: string; command?: string[]; exitCode?: number | null }> {
|
|
445
|
+
const launchCommand = buildLaunchCommand(launch, projectDir);
|
|
446
|
+
if (!launchCommand) {
|
|
447
|
+
return { success: false, error: "No supported launch command found" };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const { command, args, options, waitForExit } = launchCommand;
|
|
451
|
+
const displayCommand = [command, ...args];
|
|
452
|
+
|
|
453
|
+
return await new Promise((resolve) => {
|
|
454
|
+
const child = spawn(command, args, options);
|
|
455
|
+
|
|
456
|
+
child.once("error", (err) => {
|
|
457
|
+
resolve({ success: false, error: err.message, command: displayCommand });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
child.once("spawn", () => {
|
|
461
|
+
if (!waitForExit) {
|
|
462
|
+
child.unref();
|
|
463
|
+
resolve({ success: true, command: displayCommand });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (waitForExit) {
|
|
468
|
+
child.once("exit", (code) => {
|
|
469
|
+
resolve({ success: true, command: displayCommand, exitCode: code });
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
301
475
|
/**
|
|
302
476
|
* Get the user's preferred agent ID
|
|
303
477
|
* Falls back to highest priority detected agent if not set
|
|
@@ -355,7 +529,7 @@ export async function setPreferredAgent(id: string): Promise<void> {
|
|
|
355
529
|
* Used during jack init to set initial preference
|
|
356
530
|
*/
|
|
357
531
|
export function getDefaultPreferredAgent(
|
|
358
|
-
detected: Array<{ id: string; path: string }>,
|
|
532
|
+
detected: Array<{ id: string; path: string; launch?: AgentLaunchConfig }>,
|
|
359
533
|
): string | null {
|
|
360
534
|
if (detected.length === 0) return null;
|
|
361
535
|
|
|
@@ -34,10 +34,11 @@ export async function checkWorkerExists(name: string): Promise<boolean> {
|
|
|
34
34
|
* Delete a worker with force flag (no confirmation)
|
|
35
35
|
*/
|
|
36
36
|
export async function deleteWorker(name: string): Promise<void> {
|
|
37
|
-
const result = await $`wrangler delete ${name} --force`.nothrow().quiet();
|
|
37
|
+
const result = await $`wrangler delete --name ${name} --force`.nothrow().quiet();
|
|
38
38
|
|
|
39
39
|
if (result.exitCode !== 0) {
|
|
40
|
-
|
|
40
|
+
const stderr = result.stderr.toString().trim();
|
|
41
|
+
throw new Error(stderr || `Failed to delete worker ${name}`);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
package/src/lib/config.ts
CHANGED
|
@@ -6,10 +6,18 @@ import { join } from "node:path";
|
|
|
6
6
|
/**
|
|
7
7
|
* Agent configuration stored in jack config
|
|
8
8
|
*/
|
|
9
|
+
export type AgentLaunchConfig =
|
|
10
|
+
| {
|
|
11
|
+
type: "cli";
|
|
12
|
+
command: string;
|
|
13
|
+
args?: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
9
16
|
export interface AgentConfig {
|
|
10
17
|
active: boolean;
|
|
11
18
|
path: string;
|
|
12
19
|
detectedAt: string;
|
|
20
|
+
launch?: AgentLaunchConfig;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export enum JackErrorCode {
|
|
2
|
+
AUTH_FAILED = "AUTH_FAILED",
|
|
3
|
+
WRANGLER_AUTH_EXPIRED = "WRANGLER_AUTH_EXPIRED",
|
|
4
|
+
PROJECT_NOT_FOUND = "PROJECT_NOT_FOUND",
|
|
5
|
+
TEMPLATE_NOT_FOUND = "TEMPLATE_NOT_FOUND",
|
|
6
|
+
BUILD_FAILED = "BUILD_FAILED",
|
|
7
|
+
DEPLOY_FAILED = "DEPLOY_FAILED",
|
|
8
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
9
|
+
INTERNAL_ERROR = "INTERNAL_ERROR",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface JackErrorMeta {
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
missingSecrets?: string[];
|
|
15
|
+
stderr?: string;
|
|
16
|
+
reported?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class JackError extends Error {
|
|
20
|
+
code: JackErrorCode;
|
|
21
|
+
suggestion?: string;
|
|
22
|
+
meta?: JackErrorMeta;
|
|
23
|
+
|
|
24
|
+
constructor(code: JackErrorCode, message: string, suggestion?: string, meta?: JackErrorMeta) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "JackError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.suggestion = suggestion;
|
|
29
|
+
this.meta = meta;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isJackError(error: unknown): error is JackError {
|
|
34
|
+
return error instanceof JackError;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getErrorDetails(error: unknown): {
|
|
38
|
+
message: string;
|
|
39
|
+
suggestion?: string;
|
|
40
|
+
meta?: JackErrorMeta;
|
|
41
|
+
code?: JackErrorCode;
|
|
42
|
+
} {
|
|
43
|
+
if (isJackError(error)) {
|
|
44
|
+
return {
|
|
45
|
+
message: error.message,
|
|
46
|
+
suggestion: error.suggestion,
|
|
47
|
+
meta: error.meta,
|
|
48
|
+
code: error.code,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
package/src/lib/hooks.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { HookAction } from "../templates/types";
|
|
4
|
-
import { output } from "./output";
|
|
5
4
|
import { getSavedSecrets } from "./secrets";
|
|
6
5
|
|
|
7
6
|
export interface HookContext {
|
|
@@ -11,6 +10,27 @@ export interface HookContext {
|
|
|
11
10
|
projectDir?: string; // absolute path to project directory
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
export interface HookOutput {
|
|
14
|
+
info(message: string): void;
|
|
15
|
+
warn(message: string): void;
|
|
16
|
+
error(message: string): void;
|
|
17
|
+
success(message: string): void;
|
|
18
|
+
box(title: string, lines: string[]): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HookOptions {
|
|
22
|
+
interactive?: boolean;
|
|
23
|
+
output?: HookOutput;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const noopOutput: HookOutput = {
|
|
27
|
+
info() {},
|
|
28
|
+
warn() {},
|
|
29
|
+
error() {},
|
|
30
|
+
success() {},
|
|
31
|
+
box() {},
|
|
32
|
+
};
|
|
33
|
+
|
|
14
34
|
/**
|
|
15
35
|
* Prompt user with numbered options (Claude Code style)
|
|
16
36
|
* Returns the selected option index (0-based) or -1 if cancelled
|
|
@@ -197,99 +217,127 @@ async function checkEnvExists(env: string, projectDir?: string): Promise<boolean
|
|
|
197
217
|
* Execute a single hook action
|
|
198
218
|
* Returns true if should continue, false if should abort
|
|
199
219
|
*/
|
|
200
|
-
async function executeAction(
|
|
220
|
+
async function executeAction(
|
|
221
|
+
action: HookAction,
|
|
222
|
+
context: HookContext,
|
|
223
|
+
options?: HookOptions,
|
|
224
|
+
): Promise<boolean> {
|
|
225
|
+
const interactive = options?.interactive !== false;
|
|
226
|
+
const ui = options?.output ?? noopOutput;
|
|
227
|
+
|
|
201
228
|
switch (action.action) {
|
|
202
229
|
case "message": {
|
|
203
|
-
|
|
230
|
+
ui.info(substituteVars(action.text, context));
|
|
204
231
|
return true;
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
case "box": {
|
|
208
235
|
const title = substituteVars(action.title, context);
|
|
209
236
|
const lines = action.lines.map((line) => substituteVars(line, context));
|
|
210
|
-
|
|
237
|
+
ui.box(title, lines);
|
|
211
238
|
return true;
|
|
212
239
|
}
|
|
213
240
|
|
|
214
|
-
case "
|
|
241
|
+
case "url": {
|
|
215
242
|
const url = substituteVars(action.url, context);
|
|
216
243
|
const label = action.label ?? "Link";
|
|
244
|
+
if (!interactive) {
|
|
245
|
+
ui.info(`${label}: ${url}`);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
217
248
|
console.error("");
|
|
218
249
|
console.error(` ${label}: \x1b[36m${url}\x1b[0m`);
|
|
219
250
|
|
|
251
|
+
if (action.open) {
|
|
252
|
+
ui.info(`Opening: ${url}`);
|
|
253
|
+
await openBrowser(url);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
220
257
|
if (action.prompt !== false) {
|
|
221
258
|
console.error("");
|
|
222
259
|
const choice = await promptSelect(["Open in browser", "Skip"]);
|
|
223
260
|
if (choice === 0) {
|
|
224
261
|
await openBrowser(url);
|
|
225
|
-
|
|
262
|
+
ui.success("Opened in browser");
|
|
226
263
|
}
|
|
227
264
|
}
|
|
228
265
|
return true;
|
|
229
266
|
}
|
|
230
267
|
|
|
231
|
-
case "
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (choice === 0) {
|
|
250
|
-
await openBrowser(action.setupUrl);
|
|
268
|
+
case "require": {
|
|
269
|
+
if (action.source === "secret") {
|
|
270
|
+
const result = await checkSecretExists(action.key, context.projectDir);
|
|
271
|
+
if (!result.exists) {
|
|
272
|
+
const message = action.message ?? `Missing required secret: ${action.key}`;
|
|
273
|
+
ui.error(message);
|
|
274
|
+
ui.info(`Run: jack secrets add ${action.key}`);
|
|
275
|
+
|
|
276
|
+
if (action.setupUrl) {
|
|
277
|
+
if (interactive) {
|
|
278
|
+
console.error("");
|
|
279
|
+
const choice = await promptSelect(["Open setup page", "Skip"]);
|
|
280
|
+
if (choice === 0) {
|
|
281
|
+
await openBrowser(action.setupUrl);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
ui.info(`Setup: ${action.setupUrl}`);
|
|
285
|
+
}
|
|
251
286
|
}
|
|
287
|
+
return false;
|
|
252
288
|
}
|
|
253
|
-
return
|
|
289
|
+
return true;
|
|
254
290
|
}
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
291
|
|
|
258
|
-
|
|
259
|
-
const exists = await checkEnvExists(action.env, context.projectDir);
|
|
292
|
+
const exists = await checkEnvExists(action.key, context.projectDir);
|
|
260
293
|
if (!exists) {
|
|
261
|
-
const message = action.message ?? `Missing required env var: ${action.
|
|
262
|
-
|
|
294
|
+
const message = action.message ?? `Missing required env var: ${action.key}`;
|
|
295
|
+
ui.error(message);
|
|
296
|
+
if (action.setupUrl) {
|
|
297
|
+
ui.info(`Setup: ${action.setupUrl}`);
|
|
298
|
+
}
|
|
263
299
|
return false;
|
|
264
300
|
}
|
|
265
301
|
return true;
|
|
266
302
|
}
|
|
267
303
|
|
|
268
|
-
case "
|
|
304
|
+
case "clipboard": {
|
|
269
305
|
const text = substituteVars(action.text, context);
|
|
306
|
+
if (!interactive) {
|
|
307
|
+
ui.info(text);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
270
310
|
const success = await copyToClipboard(text);
|
|
271
311
|
if (success) {
|
|
272
312
|
const message = action.message ?? "Copied to clipboard";
|
|
273
|
-
|
|
313
|
+
ui.success(message);
|
|
274
314
|
} else {
|
|
275
|
-
|
|
315
|
+
ui.warn("Could not copy to clipboard");
|
|
276
316
|
}
|
|
277
317
|
return true;
|
|
278
318
|
}
|
|
279
319
|
|
|
280
|
-
case "
|
|
320
|
+
case "pause": {
|
|
321
|
+
if (!interactive) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
281
324
|
await waitForEnter(action.message);
|
|
282
325
|
return true;
|
|
283
326
|
}
|
|
284
327
|
|
|
285
|
-
case "
|
|
328
|
+
case "shell": {
|
|
286
329
|
const command = substituteVars(action.command, context);
|
|
287
330
|
if (action.message) {
|
|
288
|
-
|
|
331
|
+
ui.info(action.message);
|
|
289
332
|
}
|
|
290
333
|
const cwd = action.cwd === "project" ? context.projectDir : undefined;
|
|
334
|
+
// Resume stdin in case previous prompts paused it
|
|
335
|
+
if (interactive) {
|
|
336
|
+
process.stdin.resume();
|
|
337
|
+
}
|
|
291
338
|
const proc = Bun.spawn(["sh", "-c", command], {
|
|
292
339
|
cwd,
|
|
340
|
+
stdin: interactive ? "inherit" : "ignore",
|
|
293
341
|
stdout: "inherit",
|
|
294
342
|
stderr: "inherit",
|
|
295
343
|
});
|
|
@@ -306,9 +354,13 @@ async function executeAction(action: HookAction, context: HookContext): Promise<
|
|
|
306
354
|
* Run a list of hook actions
|
|
307
355
|
* Returns true if all succeeded, false if any failed (for preDeploy checks)
|
|
308
356
|
*/
|
|
309
|
-
export async function runHook(
|
|
357
|
+
export async function runHook(
|
|
358
|
+
actions: HookAction[],
|
|
359
|
+
context: HookContext,
|
|
360
|
+
options?: HookOptions,
|
|
361
|
+
): Promise<boolean> {
|
|
310
362
|
for (const action of actions) {
|
|
311
|
-
const shouldContinue = await executeAction(action, context);
|
|
363
|
+
const shouldContinue = await executeAction(action, context, options);
|
|
312
364
|
if (!shouldContinue) {
|
|
313
365
|
return false;
|
|
314
366
|
}
|