@getjack/jack 0.1.2 → 0.1.4
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 +77 -29
- package/package.json +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
package/src/lib/agents.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { delimiter, extname, join } from "node:path";
|
|
5
5
|
import { type AgentConfig, type AgentLaunchConfig, readConfig, writeConfig } from "./config.ts";
|
|
6
|
+
import { debug, isDebug } from "./debug.ts";
|
|
7
|
+
import { restoreTty } from "./tty";
|
|
6
8
|
|
|
7
9
|
// Re-export AgentConfig for consumers
|
|
8
10
|
export type { AgentConfig } from "./config.ts";
|
|
@@ -39,6 +41,12 @@ export interface DetectionResult {
|
|
|
39
41
|
total: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export interface OneShotReporter {
|
|
45
|
+
info(message: string): void;
|
|
46
|
+
warn(message: string): void;
|
|
47
|
+
status?(message: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* Result of validating agent paths
|
|
44
52
|
*/
|
|
@@ -113,9 +121,7 @@ function findExecutable(command: string): string | null {
|
|
|
113
121
|
if (process.platform === "win32") {
|
|
114
122
|
const extension = extname(command);
|
|
115
123
|
const extensions =
|
|
116
|
-
extension.length > 0
|
|
117
|
-
? [""]
|
|
118
|
-
: (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";");
|
|
124
|
+
extension.length > 0 ? [""] : (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";");
|
|
119
125
|
|
|
120
126
|
for (const basePath of paths) {
|
|
121
127
|
for (const ext of extensions) {
|
|
@@ -374,14 +380,11 @@ export async function getAgentLaunch(id: string): Promise<AgentLaunchConfig | nu
|
|
|
374
380
|
return resolved ?? null;
|
|
375
381
|
}
|
|
376
382
|
|
|
377
|
-
export async function getPreferredLaunchAgent(): Promise<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
| null
|
|
384
|
-
> {
|
|
383
|
+
export async function getPreferredLaunchAgent(): Promise<{
|
|
384
|
+
id: string;
|
|
385
|
+
definition: AgentDefinition;
|
|
386
|
+
launch: AgentLaunchConfig;
|
|
387
|
+
} | null> {
|
|
385
388
|
const config = await readConfig();
|
|
386
389
|
if (!config?.agents) return null;
|
|
387
390
|
|
|
@@ -417,22 +420,48 @@ export async function getPreferredLaunchAgent(): Promise<
|
|
|
417
420
|
return null;
|
|
418
421
|
}
|
|
419
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Context to pass to the agent on launch
|
|
425
|
+
*/
|
|
426
|
+
export interface AgentLaunchContext {
|
|
427
|
+
projectName?: string;
|
|
428
|
+
url?: string | null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Build the initial prompt for the agent based on launch context
|
|
433
|
+
*/
|
|
434
|
+
function buildInitialPrompt(context: AgentLaunchContext): string | null {
|
|
435
|
+
if (!context.url) return null;
|
|
436
|
+
|
|
437
|
+
return `[jack] Project "${context.projectName}" is live at ${context.url} - say hi and let's build!`;
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
function buildLaunchCommand(
|
|
421
441
|
launch: AgentLaunchConfig,
|
|
422
442
|
projectDir: string,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
| null {
|
|
443
|
+
context?: AgentLaunchContext,
|
|
444
|
+
): {
|
|
445
|
+
command: string;
|
|
446
|
+
args: string[];
|
|
447
|
+
options: { cwd?: string; stdio: "inherit" | "ignore"; detached?: boolean };
|
|
448
|
+
waitForExit: boolean;
|
|
449
|
+
} | null {
|
|
431
450
|
if (launch.type !== "cli") return null;
|
|
432
451
|
|
|
452
|
+
const args = launch.args ? [...launch.args] : [];
|
|
453
|
+
|
|
454
|
+
// Add initial prompt if context is provided
|
|
455
|
+
if (context) {
|
|
456
|
+
const initialPrompt = buildInitialPrompt(context);
|
|
457
|
+
if (initialPrompt) {
|
|
458
|
+
args.push(initialPrompt);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
433
462
|
return {
|
|
434
463
|
command: launch.command,
|
|
435
|
-
args
|
|
464
|
+
args,
|
|
436
465
|
options: { cwd: projectDir, stdio: "inherit" },
|
|
437
466
|
waitForExit: true,
|
|
438
467
|
};
|
|
@@ -441,14 +470,16 @@ function buildLaunchCommand(
|
|
|
441
470
|
export async function launchAgent(
|
|
442
471
|
launch: AgentLaunchConfig,
|
|
443
472
|
projectDir: string,
|
|
473
|
+
context?: AgentLaunchContext,
|
|
444
474
|
): Promise<{ success: boolean; error?: string; command?: string[]; exitCode?: number | null }> {
|
|
445
|
-
const launchCommand = buildLaunchCommand(launch, projectDir);
|
|
475
|
+
const launchCommand = buildLaunchCommand(launch, projectDir, context);
|
|
446
476
|
if (!launchCommand) {
|
|
447
477
|
return { success: false, error: "No supported launch command found" };
|
|
448
478
|
}
|
|
449
479
|
|
|
450
480
|
const { command, args, options, waitForExit } = launchCommand;
|
|
451
481
|
const displayCommand = [command, ...args];
|
|
482
|
+
restoreTty();
|
|
452
483
|
|
|
453
484
|
return await new Promise((resolve) => {
|
|
454
485
|
const child = spawn(command, args, options);
|
|
@@ -543,3 +574,340 @@ export function getDefaultPreferredAgent(
|
|
|
543
574
|
|
|
544
575
|
return withPriority[0]?.id ?? null;
|
|
545
576
|
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Agents that support one-shot mode (non-interactive execution)
|
|
580
|
+
*/
|
|
581
|
+
export const ONE_SHOT_CAPABLE_AGENTS = ["claude-code", "codex"] as const;
|
|
582
|
+
type OneShotAgent = (typeof ONE_SHOT_CAPABLE_AGENTS)[number];
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Check if an agent supports one-shot mode
|
|
586
|
+
*/
|
|
587
|
+
export function isOneShotCapable(agentId: string): agentId is OneShotAgent {
|
|
588
|
+
return ONE_SHOT_CAPABLE_AGENTS.includes(agentId as OneShotAgent);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Get preferred agent for one-shot execution
|
|
593
|
+
* Returns null if no capable agent is available
|
|
594
|
+
*/
|
|
595
|
+
export async function getOneShotAgent(): Promise<OneShotAgent | null> {
|
|
596
|
+
const preferred = await getPreferredAgent();
|
|
597
|
+
|
|
598
|
+
if (preferred && isOneShotCapable(preferred)) {
|
|
599
|
+
return preferred;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Try to find any capable agent that's installed
|
|
603
|
+
for (const agentId of ONE_SHOT_CAPABLE_AGENTS) {
|
|
604
|
+
const launch = await getAgentLaunch(agentId);
|
|
605
|
+
if (launch) {
|
|
606
|
+
return agentId;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Build customization prompt for project personalization
|
|
615
|
+
*/
|
|
616
|
+
function buildCustomizationPrompt(projectDir: string, intent: string): string {
|
|
617
|
+
return `You are customizing a new project based on this intent: "${intent}"
|
|
618
|
+
|
|
619
|
+
This is the first customization from a template. The project has been scaffolded but not yet personalized.
|
|
620
|
+
|
|
621
|
+
Instructions:
|
|
622
|
+
1. Read AGENTS.md and CLAUDE.md for project context
|
|
623
|
+
2. Modify code and configuration to match the intent
|
|
624
|
+
3. Focus on code/config changes, not documentation
|
|
625
|
+
4. Keep changes minimal and focused
|
|
626
|
+
5. End with a short report:
|
|
627
|
+
- SUMMARY: 2-4 bullet points
|
|
628
|
+
- BLOCKER: none | <short reason>
|
|
629
|
+
|
|
630
|
+
Project directory: ${projectDir}
|
|
631
|
+
|
|
632
|
+
Please customize the project to match the intent.`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Run agent in one-shot mode for project customization
|
|
637
|
+
*/
|
|
638
|
+
export async function runAgentOneShot(
|
|
639
|
+
agentId: OneShotAgent,
|
|
640
|
+
projectDir: string,
|
|
641
|
+
intent: string,
|
|
642
|
+
reporter?: OneShotReporter,
|
|
643
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
644
|
+
const launch = await getAgentLaunch(agentId);
|
|
645
|
+
if (!launch || launch.type !== "cli") {
|
|
646
|
+
return { success: false, error: `Agent ${agentId} not available` };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const prompt = buildCustomizationPrompt(projectDir, intent);
|
|
650
|
+
const debugEnabled = isDebug();
|
|
651
|
+
const agentLabel = getAgentDefinition(agentId)?.name ?? agentId;
|
|
652
|
+
|
|
653
|
+
// Build command based on agent type
|
|
654
|
+
let args: string[];
|
|
655
|
+
let streamJson = false;
|
|
656
|
+
let jsonMode: "claude" | "codex" | null = null;
|
|
657
|
+
if (agentId === "claude-code") {
|
|
658
|
+
streamJson = true;
|
|
659
|
+
jsonMode = "claude";
|
|
660
|
+
args = [
|
|
661
|
+
"-p",
|
|
662
|
+
prompt,
|
|
663
|
+
"--permission-mode",
|
|
664
|
+
"acceptEdits",
|
|
665
|
+
"--output-format",
|
|
666
|
+
"stream-json",
|
|
667
|
+
"--include-partial-messages",
|
|
668
|
+
"--verbose",
|
|
669
|
+
];
|
|
670
|
+
if (debugEnabled) {
|
|
671
|
+
args.push("--debug");
|
|
672
|
+
}
|
|
673
|
+
} else if (agentId === "codex") {
|
|
674
|
+
streamJson = true;
|
|
675
|
+
jsonMode = "codex";
|
|
676
|
+
args = ["exec", prompt, "--json", "--skip-git-repo-check"];
|
|
677
|
+
} else {
|
|
678
|
+
return { success: false, error: `Unsupported agent: ${agentId}` };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (debugEnabled) {
|
|
682
|
+
debug("One-shot agent command", { command: launch.command, args, cwd: projectDir });
|
|
683
|
+
debug("One-shot prompt", prompt);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
restoreTty();
|
|
687
|
+
|
|
688
|
+
return new Promise((resolve) => {
|
|
689
|
+
const child = spawn(launch.command, args, {
|
|
690
|
+
cwd: projectDir,
|
|
691
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
let stdoutBuffer = "";
|
|
695
|
+
let stderrBuffer = "";
|
|
696
|
+
let fullOutput = "";
|
|
697
|
+
const summaryLines: string[] = [];
|
|
698
|
+
let blockerMessage: string | null = null;
|
|
699
|
+
let statusMessage: string | null = null;
|
|
700
|
+
|
|
701
|
+
const reportSummary = () => {
|
|
702
|
+
if (!reporter) return;
|
|
703
|
+
if (summaryLines.length > 0) {
|
|
704
|
+
reporter.info(`Summary: ${summaryLines.join(" | ")}`);
|
|
705
|
+
}
|
|
706
|
+
if (blockerMessage && blockerMessage.toLowerCase() !== "none") {
|
|
707
|
+
reporter.warn(`Blocker: ${blockerMessage}`);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const updateStatus = (message: string) => {
|
|
712
|
+
if (!reporter?.status) return;
|
|
713
|
+
const next = `${agentLabel}: ${message}`;
|
|
714
|
+
if (next === statusMessage) return;
|
|
715
|
+
statusMessage = next;
|
|
716
|
+
reporter.status(next);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const extractSummary = (text: string) => {
|
|
720
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim());
|
|
721
|
+
let inSummary = false;
|
|
722
|
+
|
|
723
|
+
for (const line of lines) {
|
|
724
|
+
if (!line) {
|
|
725
|
+
if (inSummary) {
|
|
726
|
+
inSummary = false;
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (line.toUpperCase().startsWith("SUMMARY:")) {
|
|
732
|
+
inSummary = true;
|
|
733
|
+
const after = line.slice("SUMMARY:".length).trim();
|
|
734
|
+
if (after) {
|
|
735
|
+
summaryLines.push(after.replace(/^[-•]\s*/, ""));
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (line.toUpperCase().startsWith("BLOCKER:")) {
|
|
741
|
+
blockerMessage = line.slice("BLOCKER:".length).trim();
|
|
742
|
+
inSummary = false;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (inSummary) {
|
|
747
|
+
summaryLines.push(line.replace(/^[-•]\s*/, ""));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const handleClaudeLine = (line: string) => {
|
|
753
|
+
if (!line.trim()) return;
|
|
754
|
+
if (debugEnabled) {
|
|
755
|
+
process.stderr.write(`${line}\n`);
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
const parsed = JSON.parse(line) as {
|
|
759
|
+
type?: string;
|
|
760
|
+
event?: {
|
|
761
|
+
type?: string;
|
|
762
|
+
content_block?: { type?: string; name?: string };
|
|
763
|
+
delta?: { type?: string; text_delta?: string };
|
|
764
|
+
};
|
|
765
|
+
result?: string;
|
|
766
|
+
is_error?: boolean;
|
|
767
|
+
permission_denials?: Array<{ reason?: string }>;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
if (parsed.type === "result") {
|
|
771
|
+
if (typeof parsed.result === "string" && parsed.result.trim().length > 0) {
|
|
772
|
+
fullOutput = parsed.result;
|
|
773
|
+
}
|
|
774
|
+
if (parsed.permission_denials?.length && reporter) {
|
|
775
|
+
const details = parsed.permission_denials
|
|
776
|
+
.map((denial) => denial.reason)
|
|
777
|
+
.filter(Boolean)
|
|
778
|
+
.join(" | ");
|
|
779
|
+
reporter.warn(details ? `Permission denied: ${details}` : "Permission denied");
|
|
780
|
+
updateStatus("permission denied");
|
|
781
|
+
}
|
|
782
|
+
if (parsed.is_error) {
|
|
783
|
+
stderrBuffer = stderrBuffer || "Claude returned an error";
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (parsed.type === "stream_event") {
|
|
789
|
+
const eventType = parsed.event?.type;
|
|
790
|
+
const blockType = parsed.event?.content_block?.type;
|
|
791
|
+
if (eventType === "message_start") {
|
|
792
|
+
updateStatus("thinking");
|
|
793
|
+
} else if (eventType === "content_block_start") {
|
|
794
|
+
if (blockType === "tool_use") {
|
|
795
|
+
const toolName = parsed.event?.content_block?.name;
|
|
796
|
+
updateStatus(toolName ? `running ${toolName}` : "running tool");
|
|
797
|
+
} else if (blockType === "text") {
|
|
798
|
+
updateStatus("responding");
|
|
799
|
+
}
|
|
800
|
+
} else if (eventType === "message_stop") {
|
|
801
|
+
updateStatus("finalizing");
|
|
802
|
+
}
|
|
803
|
+
const delta = parsed.event?.delta;
|
|
804
|
+
if (delta?.type === "text_delta" && delta.text_delta) {
|
|
805
|
+
fullOutput += delta.text_delta;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
// Ignore malformed lines in non-debug mode
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const handleCodexLine = (line: string) => {
|
|
814
|
+
if (!line.trim()) return;
|
|
815
|
+
if (debugEnabled) {
|
|
816
|
+
process.stderr.write(`${line}\n`);
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
const parsed = JSON.parse(line) as {
|
|
820
|
+
type?: string;
|
|
821
|
+
item?: { type?: string; text?: string };
|
|
822
|
+
error?: { message?: string };
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
if (parsed.type === "thread.started") {
|
|
826
|
+
updateStatus("starting");
|
|
827
|
+
}
|
|
828
|
+
if (parsed.type === "turn.started") {
|
|
829
|
+
updateStatus("thinking");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (parsed.type === "item.completed" && parsed.item?.type === "agent_message") {
|
|
833
|
+
if (parsed.item.text) {
|
|
834
|
+
fullOutput += parsed.item.text;
|
|
835
|
+
}
|
|
836
|
+
updateStatus("responding");
|
|
837
|
+
}
|
|
838
|
+
if (parsed.type === "item.completed" && parsed.item?.type === "reasoning") {
|
|
839
|
+
updateStatus("reasoning");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (parsed.type === "error") {
|
|
843
|
+
if (parsed.error?.message) {
|
|
844
|
+
stderrBuffer = parsed.error.message;
|
|
845
|
+
} else {
|
|
846
|
+
stderrBuffer = stderrBuffer || "Codex returned an error";
|
|
847
|
+
}
|
|
848
|
+
updateStatus("error");
|
|
849
|
+
}
|
|
850
|
+
if (parsed.type === "turn.completed") {
|
|
851
|
+
updateStatus("finalizing");
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
// Ignore malformed lines in non-debug mode
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
const handleJsonLine =
|
|
859
|
+
jsonMode === "claude" ? handleClaudeLine : jsonMode === "codex" ? handleCodexLine : null;
|
|
860
|
+
|
|
861
|
+
child.stdout.on("data", (chunk) => {
|
|
862
|
+
const text = chunk.toString();
|
|
863
|
+
if (!streamJson) {
|
|
864
|
+
fullOutput += text;
|
|
865
|
+
if (debugEnabled) {
|
|
866
|
+
process.stderr.write(text);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
stdoutBuffer += text;
|
|
872
|
+
let newlineIndex = stdoutBuffer.indexOf("\n");
|
|
873
|
+
while (newlineIndex !== -1) {
|
|
874
|
+
const line = stdoutBuffer.slice(0, newlineIndex);
|
|
875
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
876
|
+
handleJsonLine?.(line);
|
|
877
|
+
newlineIndex = stdoutBuffer.indexOf("\n");
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
child.stderr.on("data", (chunk) => {
|
|
882
|
+
const text = chunk.toString();
|
|
883
|
+
stderrBuffer += text;
|
|
884
|
+
if (debugEnabled) {
|
|
885
|
+
process.stderr.write(text);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
child.once("error", (err) => {
|
|
890
|
+
resolve({ success: false, error: err.message });
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
child.once("exit", (code) => {
|
|
894
|
+
if (streamJson && stdoutBuffer.trim().length > 0) {
|
|
895
|
+
handleJsonLine?.(stdoutBuffer);
|
|
896
|
+
stdoutBuffer = "";
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const exitOk = code === 0;
|
|
900
|
+
if (!debugEnabled && fullOutput.trim().length > 0) {
|
|
901
|
+
extractSummary(fullOutput);
|
|
902
|
+
reportSummary();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!exitOk && stderrBuffer.trim().length > 0) {
|
|
906
|
+
resolve({ success: false, error: stderrBuffer.trim() });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
resolve(exitOk ? { success: true } : { success: false, error: `Exit code ${code}` });
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset hash computation utilities for content-addressable asset management.
|
|
3
|
+
* Uses SHA-256 hashing with base64 content and file extension.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a single entry in the asset manifest.
|
|
8
|
+
*/
|
|
9
|
+
export interface AssetManifestEntry {
|
|
10
|
+
hash: string;
|
|
11
|
+
size: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maps asset paths to their manifest entries.
|
|
16
|
+
*/
|
|
17
|
+
export type AssetManifest = Record<string, AssetManifestEntry>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Computes a content-addressable hash for an asset.
|
|
21
|
+
*
|
|
22
|
+
* Algorithm: SHA-256(base64(content) + extension).slice(0, 32)
|
|
23
|
+
* - Extension is extracted from filePath without the leading dot
|
|
24
|
+
* - Uses Web Crypto API for SHA-256 computation
|
|
25
|
+
*
|
|
26
|
+
* @param content - The raw binary content of the asset
|
|
27
|
+
* @param filePath - The file path used to extract the extension
|
|
28
|
+
* @returns A 32-character hex hash string
|
|
29
|
+
*/
|
|
30
|
+
export async function computeAssetHash(content: Uint8Array, filePath: string): Promise<string> {
|
|
31
|
+
// Extract extension without the dot (e.g., "js" not ".js")
|
|
32
|
+
const extension = filePath.includes(".") ? filePath.split(".").pop() || "" : "";
|
|
33
|
+
|
|
34
|
+
// Convert content to base64
|
|
35
|
+
const base64 = Buffer.from(content).toString("base64");
|
|
36
|
+
|
|
37
|
+
// Create hash input: base64 content + extension
|
|
38
|
+
const hashInput = new TextEncoder().encode(base64 + extension);
|
|
39
|
+
|
|
40
|
+
// Compute SHA-256 hash using Web Crypto API
|
|
41
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", hashInput);
|
|
42
|
+
|
|
43
|
+
// Convert to hex string and truncate to 32 characters
|
|
44
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
45
|
+
const hashHex = Array.from(hashArray)
|
|
46
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
47
|
+
.join("");
|
|
48
|
+
|
|
49
|
+
return hashHex.slice(0, 32);
|
|
50
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getAuthApiUrl } from "./constants.ts";
|
|
2
|
+
import { type AuthCredentials, getCredentials, isTokenExpired, saveCredentials } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
export interface DeviceAuthResponse {
|
|
5
|
+
device_code: string;
|
|
6
|
+
user_code: string;
|
|
7
|
+
verification_uri: string;
|
|
8
|
+
verification_uri_complete: string;
|
|
9
|
+
expires_in: number;
|
|
10
|
+
interval: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TokenResponse {
|
|
14
|
+
access_token: string;
|
|
15
|
+
refresh_token: string;
|
|
16
|
+
expires_in: number;
|
|
17
|
+
user: {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
first_name: string | null;
|
|
21
|
+
last_name: string | null;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function startDeviceAuth(): Promise<DeviceAuthResponse> {
|
|
26
|
+
const response = await fetch(`${getAuthApiUrl()}/auth/device/authorize`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorBody = (await response.json().catch(() => ({}))) as { message?: string };
|
|
32
|
+
throw new Error(errorBody.message || "Failed to start device authorization");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return response.json() as Promise<DeviceAuthResponse>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function pollDeviceToken(deviceCode: string): Promise<TokenResponse | null> {
|
|
39
|
+
const response = await fetch(`${getAuthApiUrl()}/auth/device/token`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (response.status === 202) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (response.status === 410) {
|
|
50
|
+
throw new Error("Device code expired. Please try again.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const errorBody = (await response.json().catch(() => ({}))) as { message?: string };
|
|
55
|
+
throw new Error(errorBody.message || "Failed to get token");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response.json() as Promise<TokenResponse>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function refreshToken(refreshTokenValue: string): Promise<TokenResponse> {
|
|
62
|
+
const response = await fetch(`${getAuthApiUrl()}/auth/refresh`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error("Failed to refresh token. Please login again.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return response.json() as Promise<TokenResponse>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getValidAccessToken(): Promise<string | null> {
|
|
76
|
+
const creds = await getCredentials();
|
|
77
|
+
if (!creds) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isTokenExpired(creds)) {
|
|
82
|
+
try {
|
|
83
|
+
const newTokens = await refreshToken(creds.refresh_token);
|
|
84
|
+
// Default to 5 minutes if expires_in not provided
|
|
85
|
+
const expiresIn = newTokens.expires_in ?? 300;
|
|
86
|
+
const newCreds: AuthCredentials = {
|
|
87
|
+
access_token: newTokens.access_token,
|
|
88
|
+
refresh_token: newTokens.refresh_token,
|
|
89
|
+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
90
|
+
user: newTokens.user,
|
|
91
|
+
};
|
|
92
|
+
await saveCredentials(newCreds);
|
|
93
|
+
return newCreds.access_token;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return creds.access_token;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
|
103
|
+
const token = await getValidAccessToken();
|
|
104
|
+
if (!token) {
|
|
105
|
+
throw new Error("Not authenticated. Run 'jack login' first.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return fetch(url, {
|
|
109
|
+
...options,
|
|
110
|
+
headers: {
|
|
111
|
+
...options.headers,
|
|
112
|
+
Authorization: `Bearer ${token}`,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { JackError, JackErrorCode } from "../errors.ts";
|
|
2
|
+
import { info } from "../output.ts";
|
|
3
|
+
import { getValidAccessToken } from "./client.ts";
|
|
4
|
+
import { getCredentials } from "./store.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Require auth - throws error if not logged in (for non-interactive contexts)
|
|
8
|
+
*/
|
|
9
|
+
export async function requireAuth(): Promise<string> {
|
|
10
|
+
const token = await getValidAccessToken();
|
|
11
|
+
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new JackError(
|
|
14
|
+
JackErrorCode.AUTH_FAILED,
|
|
15
|
+
"Not logged in",
|
|
16
|
+
"Run 'jack login' to sign in to jack cloud",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return token;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Require auth with auto-login - starts login flow if needed (omakase style)
|
|
25
|
+
* Use this for interactive CLI commands that need auth.
|
|
26
|
+
*/
|
|
27
|
+
export async function requireAuthOrLogin(): Promise<string> {
|
|
28
|
+
const token = await getValidAccessToken();
|
|
29
|
+
|
|
30
|
+
if (token) {
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Auto-start login flow
|
|
35
|
+
info("Signing in to jack cloud...");
|
|
36
|
+
|
|
37
|
+
const { default: login } = await import("../../commands/login.ts");
|
|
38
|
+
await login({ silent: true });
|
|
39
|
+
|
|
40
|
+
// After login, get the token
|
|
41
|
+
const newToken = await getValidAccessToken();
|
|
42
|
+
if (!newToken) {
|
|
43
|
+
throw new JackError(
|
|
44
|
+
JackErrorCode.AUTH_FAILED,
|
|
45
|
+
"Login failed",
|
|
46
|
+
"Please try again with 'jack login'",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.error(""); // Space before continuing with original command
|
|
51
|
+
return newToken;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function getCurrentUser() {
|
|
55
|
+
const creds = await getCredentials();
|
|
56
|
+
return creds?.user ?? null;
|
|
57
|
+
}
|