@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.
Files changed (91) hide show
  1. package/README.md +77 -29
  2. package/package.json +54 -47
  3. package/src/commands/agents.ts +145 -10
  4. package/src/commands/down.ts +110 -102
  5. package/src/commands/feedback.ts +189 -0
  6. package/src/commands/init.ts +8 -12
  7. package/src/commands/login.ts +88 -0
  8. package/src/commands/logout.ts +14 -0
  9. package/src/commands/logs.ts +21 -0
  10. package/src/commands/mcp.ts +134 -7
  11. package/src/commands/new.ts +43 -17
  12. package/src/commands/open.ts +13 -6
  13. package/src/commands/projects.ts +269 -143
  14. package/src/commands/secrets.ts +413 -0
  15. package/src/commands/services.ts +96 -123
  16. package/src/commands/ship.ts +5 -1
  17. package/src/commands/whoami.ts +31 -0
  18. package/src/index.ts +218 -144
  19. package/src/lib/agent-files.ts +34 -0
  20. package/src/lib/agents.ts +390 -22
  21. package/src/lib/asset-hash.ts +50 -0
  22. package/src/lib/auth/client.ts +115 -0
  23. package/src/lib/auth/constants.ts +5 -0
  24. package/src/lib/auth/guard.ts +57 -0
  25. package/src/lib/auth/index.ts +18 -0
  26. package/src/lib/auth/store.ts +54 -0
  27. package/src/lib/binding-validator.ts +136 -0
  28. package/src/lib/build-helper.ts +211 -0
  29. package/src/lib/cloudflare-api.ts +24 -0
  30. package/src/lib/config.ts +5 -6
  31. package/src/lib/control-plane.ts +295 -0
  32. package/src/lib/debug.ts +3 -1
  33. package/src/lib/deploy-mode.ts +93 -0
  34. package/src/lib/deploy-upload.ts +92 -0
  35. package/src/lib/errors.ts +2 -0
  36. package/src/lib/github.ts +31 -1
  37. package/src/lib/hooks.ts +4 -12
  38. package/src/lib/intent.ts +88 -0
  39. package/src/lib/jsonc.ts +125 -0
  40. package/src/lib/local-paths.test.ts +902 -0
  41. package/src/lib/local-paths.ts +258 -0
  42. package/src/lib/managed-deploy.ts +175 -0
  43. package/src/lib/managed-down.ts +159 -0
  44. package/src/lib/mcp-config.ts +55 -34
  45. package/src/lib/names.ts +9 -29
  46. package/src/lib/project-operations.ts +676 -249
  47. package/src/lib/project-resolver.ts +476 -0
  48. package/src/lib/registry.ts +76 -37
  49. package/src/lib/resources.ts +196 -0
  50. package/src/lib/schema.ts +30 -1
  51. package/src/lib/storage/file-filter.ts +1 -0
  52. package/src/lib/storage/index.ts +5 -1
  53. package/src/lib/telemetry.ts +14 -0
  54. package/src/lib/tty.ts +15 -0
  55. package/src/lib/zip-packager.ts +255 -0
  56. package/src/mcp/resources/index.ts +8 -2
  57. package/src/mcp/server.ts +32 -4
  58. package/src/mcp/tools/index.ts +35 -13
  59. package/src/mcp/types.ts +6 -0
  60. package/src/mcp/utils.ts +1 -1
  61. package/src/templates/index.ts +42 -4
  62. package/src/templates/types.ts +13 -0
  63. package/templates/CLAUDE.md +166 -0
  64. package/templates/api/.jack.json +4 -0
  65. package/templates/api/bun.lock +1 -0
  66. package/templates/api/wrangler.jsonc +5 -0
  67. package/templates/hello/.jack.json +28 -0
  68. package/templates/hello/package.json +10 -0
  69. package/templates/hello/src/index.ts +11 -0
  70. package/templates/hello/tsconfig.json +11 -0
  71. package/templates/hello/wrangler.jsonc +5 -0
  72. package/templates/miniapp/.jack.json +15 -4
  73. package/templates/miniapp/bun.lock +135 -40
  74. package/templates/miniapp/index.html +1 -0
  75. package/templates/miniapp/package.json +3 -1
  76. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  77. package/templates/miniapp/public/icon.png +0 -0
  78. package/templates/miniapp/public/og.png +0 -0
  79. package/templates/miniapp/schema.sql +8 -0
  80. package/templates/miniapp/src/App.tsx +254 -3
  81. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  82. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  83. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  84. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  85. package/templates/miniapp/src/index.css +15 -0
  86. package/templates/miniapp/src/lib/api.ts +2 -1
  87. package/templates/miniapp/src/worker.ts +515 -1
  88. package/templates/miniapp/wrangler.jsonc +15 -3
  89. package/LICENSE +0 -190
  90. package/src/commands/cloud.ts +0 -230
  91. package/templates/api/wrangler.toml +0 -3
@@ -0,0 +1,18 @@
1
+ export { getAuthApiUrl } from "./constants.ts";
2
+ export {
3
+ getValidAccessToken,
4
+ authFetch,
5
+ pollDeviceToken,
6
+ refreshToken,
7
+ startDeviceAuth,
8
+ } from "./client.ts";
9
+ export { requireAuth, requireAuthOrLogin, getCurrentUser } from "./guard.ts";
10
+ export {
11
+ deleteCredentials,
12
+ getCredentials,
13
+ isLoggedIn,
14
+ isTokenExpired,
15
+ saveCredentials,
16
+ type AuthCredentials,
17
+ type AuthUser,
18
+ } from "./store.ts";
@@ -0,0 +1,54 @@
1
+ import { existsSync } from "node:fs";
2
+ import { chmod } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { CONFIG_DIR } from "../config.ts";
5
+
6
+ export interface AuthUser {
7
+ id: string;
8
+ email: string;
9
+ first_name: string | null;
10
+ last_name: string | null;
11
+ }
12
+
13
+ export interface AuthCredentials {
14
+ access_token: string;
15
+ refresh_token: string;
16
+ expires_at: number; // Unix timestamp (seconds)
17
+ user: AuthUser;
18
+ }
19
+
20
+ const AUTH_PATH = join(CONFIG_DIR, "auth.json");
21
+
22
+ export async function getCredentials(): Promise<AuthCredentials | null> {
23
+ if (!existsSync(AUTH_PATH)) {
24
+ return null;
25
+ }
26
+ try {
27
+ return await Bun.file(AUTH_PATH).json();
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export async function saveCredentials(creds: AuthCredentials): Promise<void> {
34
+ await Bun.write(AUTH_PATH, JSON.stringify(creds, null, 2));
35
+ await chmod(AUTH_PATH, 0o600);
36
+ }
37
+
38
+ export async function deleteCredentials(): Promise<void> {
39
+ if (existsSync(AUTH_PATH)) {
40
+ const { unlink } = await import("node:fs/promises");
41
+ await unlink(AUTH_PATH);
42
+ }
43
+ }
44
+
45
+ export async function isLoggedIn(): Promise<boolean> {
46
+ const creds = await getCredentials();
47
+ return creds !== null;
48
+ }
49
+
50
+ export function isTokenExpired(creds: AuthCredentials): boolean {
51
+ const now = Math.floor(Date.now() / 1000);
52
+ const buffer = 5 * 60; // 5 minutes
53
+ return creds.expires_at < now + buffer;
54
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Binding validator for jack cloud managed deployments.
3
+ *
4
+ * Validates that project bindings are supported by jack cloud and
5
+ * that required resources (like assets directories) exist.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import type { WranglerConfig } from "./build-helper.ts";
11
+
12
+ /**
13
+ * Bindings supported by jack cloud managed deployments.
14
+ */
15
+ export const SUPPORTED_BINDINGS = ["d1_databases", "ai", "assets", "vars"] as const;
16
+
17
+ /**
18
+ * Bindings not yet supported by jack cloud.
19
+ * These will cause validation errors if present in wrangler config.
20
+ */
21
+ export const UNSUPPORTED_BINDINGS = [
22
+ "kv_namespaces",
23
+ "durable_objects",
24
+ "queues",
25
+ "services",
26
+ "r2_buckets",
27
+ "hyperdrive",
28
+ "vectorize",
29
+ "browser",
30
+ "mtls_certificates",
31
+ ] as const;
32
+
33
+ /**
34
+ * Human-readable names for unsupported bindings.
35
+ */
36
+ const BINDING_DISPLAY_NAMES: Record<string, string> = {
37
+ kv_namespaces: "KV Namespaces",
38
+ durable_objects: "Durable Objects",
39
+ queues: "Queues",
40
+ services: "Service Bindings",
41
+ r2_buckets: "R2 Buckets",
42
+ hyperdrive: "Hyperdrive",
43
+ vectorize: "Vectorize",
44
+ browser: "Browser Rendering",
45
+ mtls_certificates: "mTLS Certificates",
46
+ };
47
+
48
+ export interface BindingValidationResult {
49
+ valid: boolean;
50
+ errors: string[];
51
+ }
52
+
53
+ /**
54
+ * Validates that a wrangler config only uses supported bindings.
55
+ *
56
+ * @param config - Parsed wrangler configuration
57
+ * @param projectPath - Absolute path to project directory (for assets validation)
58
+ * @returns Validation result with errors if invalid
59
+ */
60
+ export function validateBindings(
61
+ config: WranglerConfig,
62
+ projectPath: string,
63
+ ): BindingValidationResult {
64
+ const errors: string[] = [];
65
+
66
+ // Check for unsupported bindings
67
+ for (const binding of UNSUPPORTED_BINDINGS) {
68
+ const value = config[binding as keyof WranglerConfig];
69
+ if (value !== undefined && value !== null) {
70
+ const displayName = BINDING_DISPLAY_NAMES[binding] || binding;
71
+ // Special message for R2 - suggest using Workers Assets instead
72
+ if (binding === "r2_buckets") {
73
+ errors.push(
74
+ `✗ R2 buckets not supported in managed deploy.\n For static files, use Workers Assets instead (assets.directory in wrangler.jsonc).\n Fix: Replace r2_buckets with assets config, or use 'wrangler deploy' for full control.`,
75
+ );
76
+ } else {
77
+ errors.push(
78
+ `✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
79
+ );
80
+ }
81
+ }
82
+ }
83
+
84
+ // Validate assets directory if configured
85
+ const assetsValidation = validateAssetsDirectory(config, projectPath);
86
+ if (!assetsValidation.valid) {
87
+ errors.push(...assetsValidation.errors);
88
+ }
89
+
90
+ return {
91
+ valid: errors.length === 0,
92
+ errors,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Validates that the assets directory exists and is not empty.
98
+ *
99
+ * @param config - Parsed wrangler configuration
100
+ * @param projectPath - Absolute path to project directory
101
+ * @returns Validation result with errors if invalid
102
+ */
103
+ export function validateAssetsDirectory(
104
+ config: WranglerConfig,
105
+ projectPath: string,
106
+ ): BindingValidationResult {
107
+ const errors: string[] = [];
108
+
109
+ if (config.assets?.directory) {
110
+ const assetsDir = config.assets.directory;
111
+ const assetsPath = join(projectPath, assetsDir);
112
+
113
+ if (!existsSync(assetsPath)) {
114
+ errors.push(
115
+ `✗ Assets directory not found: ${assetsDir}\n The assets.directory specified in wrangler.jsonc does not exist.\n Fix: Run your build command first, or update assets.directory in wrangler.jsonc.`,
116
+ );
117
+ } else {
118
+ // Check if directory is empty
119
+ try {
120
+ const files = readdirSync(assetsPath);
121
+ if (files.length === 0) {
122
+ errors.push(
123
+ `✗ Assets directory is empty: ${assetsDir}\n No files found in the assets directory.\n Fix: Run your build command to generate assets.`,
124
+ );
125
+ }
126
+ } catch {
127
+ // If we can't read the directory, the deploy will fail anyway
128
+ }
129
+ }
130
+ }
131
+
132
+ return {
133
+ valid: errors.length === 0,
134
+ errors,
135
+ };
136
+ }
@@ -0,0 +1,211 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, readdir } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { $ } from "bun";
6
+ import { JackError, JackErrorCode } from "./errors.ts";
7
+ import { parseJsonc } from "./jsonc.ts";
8
+ import type { OperationReporter } from "./project-operations.ts";
9
+
10
+ export interface BuildOutput {
11
+ outDir: string;
12
+ entrypoint: string;
13
+ assetsDir: string | null;
14
+ compatibilityDate: string;
15
+ compatibilityFlags: string[];
16
+ moduleFormat: "esm";
17
+ }
18
+
19
+ export interface BuildOptions {
20
+ projectPath: string;
21
+ reporter?: OperationReporter;
22
+ }
23
+
24
+ export interface WranglerConfig {
25
+ main?: string;
26
+ compatibility_date?: string;
27
+ compatibility_flags?: string[];
28
+ // Supported bindings
29
+ d1_databases?: Array<{ binding: string; database_name?: string; database_id?: string }>;
30
+ ai?: { binding?: string };
31
+ assets?: {
32
+ directory?: string;
33
+ binding?: string;
34
+ not_found_handling?: "single-page-application" | "404-page" | "none";
35
+ html_handling?: "auto-trailing-slash" | "force-trailing-slash" | "drop-trailing-slash" | "none";
36
+ run_worker_first?: boolean;
37
+ };
38
+ vars?: Record<string, string>;
39
+ // Unsupported bindings (for validation)
40
+ kv_namespaces?: unknown;
41
+ durable_objects?: unknown;
42
+ queues?: unknown;
43
+ services?: unknown;
44
+ r2_buckets?: unknown;
45
+ hyperdrive?: unknown;
46
+ vectorize?: unknown;
47
+ browser?: unknown;
48
+ mtls_certificates?: unknown;
49
+ }
50
+
51
+ /**
52
+ * Parses wrangler.jsonc configuration from project directory
53
+ * @param projectPath - Absolute path to project directory
54
+ * @returns Parsed wrangler configuration
55
+ */
56
+ export async function parseWranglerConfig(projectPath: string): Promise<WranglerConfig> {
57
+ const wranglerPath = join(projectPath, "wrangler.jsonc");
58
+
59
+ if (!existsSync(wranglerPath)) {
60
+ throw new JackError(
61
+ JackErrorCode.VALIDATION_ERROR,
62
+ "wrangler.jsonc not found",
63
+ "Ensure your project has a wrangler.jsonc configuration file",
64
+ );
65
+ }
66
+
67
+ const content = await readFile(wranglerPath, "utf-8");
68
+ return parseJsonc<WranglerConfig>(content);
69
+ }
70
+
71
+ /**
72
+ * Checks if project requires Vite build by detecting vite config files
73
+ * @param projectPath - Absolute path to project directory
74
+ * @returns true if vite.config.ts or vite.config.js exists
75
+ */
76
+ export async function needsViteBuild(projectPath: string): Promise<boolean> {
77
+ return (
78
+ existsSync(join(projectPath, "vite.config.ts")) ||
79
+ existsSync(join(projectPath, "vite.config.js")) ||
80
+ existsSync(join(projectPath, "vite.config.mjs")) ||
81
+ existsSync(join(projectPath, "vite.config.mts"))
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Runs Vite build for the project
87
+ * @param projectPath - Absolute path to project directory
88
+ * @throws JackError if build fails
89
+ */
90
+ export async function runViteBuild(projectPath: string): Promise<void> {
91
+ // Jack controls the build for managed projects (omakase)
92
+ // Users wanting tsc can run `bun run build` manually before shipping
93
+ const buildResult = await $`bunx vite build`.cwd(projectPath).nothrow().quiet();
94
+
95
+ if (buildResult.exitCode !== 0) {
96
+ throw new JackError(
97
+ JackErrorCode.BUILD_FAILED,
98
+ "Vite build failed",
99
+ "Check your vite.config and source files for errors",
100
+ {
101
+ exitCode: buildResult.exitCode,
102
+ stderr: buildResult.stderr.toString(),
103
+ },
104
+ );
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Builds a Cloudflare Worker project using wrangler dry-run
110
+ * @param options - Build options with project path and optional reporter
111
+ * @returns BuildOutput containing build artifacts and metadata
112
+ * @throws JackError if build fails
113
+ */
114
+ export async function buildProject(options: BuildOptions): Promise<BuildOutput> {
115
+ const { projectPath, reporter } = options;
116
+
117
+ // Parse wrangler config first
118
+ const config = await parseWranglerConfig(projectPath);
119
+
120
+ // Check if Vite build is needed and run it
121
+ const hasVite = await needsViteBuild(projectPath);
122
+ if (hasVite) {
123
+ reporter?.start("Building with Vite...");
124
+ await runViteBuild(projectPath);
125
+ reporter?.stop();
126
+ reporter?.success("Built with Vite");
127
+ }
128
+
129
+ // Create unique temp directory for build output
130
+ const buildId = `jack-build-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
131
+ const outDir = join(tmpdir(), buildId);
132
+ await mkdir(outDir, { recursive: true });
133
+
134
+ // Run wrangler dry-run to build without deploying
135
+ reporter?.start("Building worker...");
136
+
137
+ const dryRunResult = await $`wrangler deploy --dry-run --outdir=${outDir}`
138
+ .cwd(projectPath)
139
+ .nothrow()
140
+ .quiet();
141
+
142
+ if (dryRunResult.exitCode !== 0) {
143
+ reporter?.stop();
144
+ reporter?.error("Worker build failed");
145
+ throw new JackError(
146
+ JackErrorCode.BUILD_FAILED,
147
+ "Worker build failed",
148
+ "Check your wrangler.jsonc and worker code for errors",
149
+ {
150
+ exitCode: dryRunResult.exitCode,
151
+ stderr: dryRunResult.stderr.toString(),
152
+ },
153
+ );
154
+ }
155
+
156
+ reporter?.stop();
157
+ reporter?.success("Built worker");
158
+
159
+ const entrypoint = await resolveEntrypoint(outDir, config.main);
160
+
161
+ // Determine assets directory if configured
162
+ let assetsDir: string | null = null;
163
+ if (config.assets) {
164
+ // Default to "dist" if assets binding exists but directory not specified (Vite convention)
165
+ const directory = config.assets.directory || "dist";
166
+ const assetsDirPath = join(projectPath, directory);
167
+ if (existsSync(assetsDirPath)) {
168
+ assetsDir = assetsDirPath;
169
+ }
170
+ }
171
+
172
+ return {
173
+ outDir,
174
+ entrypoint,
175
+ assetsDir,
176
+ compatibilityDate: config.compatibility_date || "2024-01-01",
177
+ compatibilityFlags: config.compatibility_flags || [],
178
+ moduleFormat: "esm",
179
+ };
180
+ }
181
+
182
+ async function resolveEntrypoint(outDir: string, main?: string): Promise<string> {
183
+ const candidates: string[] = [];
184
+
185
+ if (main) {
186
+ const base = basename(main).replace(/\.[^.]+$/, "");
187
+ if (base) {
188
+ candidates.push(`${base}.js`, `${base}.mjs`);
189
+ }
190
+ }
191
+
192
+ candidates.push("index.js", "worker.js");
193
+
194
+ for (const candidate of candidates) {
195
+ if (existsSync(join(outDir, candidate))) {
196
+ return candidate;
197
+ }
198
+ }
199
+
200
+ const files = await readdir(outDir);
201
+ const jsFiles = files.filter((file) => file.endsWith(".js"));
202
+ if (jsFiles.length === 1) {
203
+ return jsFiles[0] as string;
204
+ }
205
+
206
+ throw new JackError(
207
+ JackErrorCode.BUILD_FAILED,
208
+ "Could not determine build entrypoint",
209
+ "Ensure wrangler outputs a single entry file (index.js or worker.js)",
210
+ );
211
+ }
@@ -64,6 +64,30 @@ export async function deleteDatabase(dbName: string): Promise<void> {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * List D1 databases for the current account
69
+ */
70
+ export async function listD1Databases(): Promise<Array<{ name?: string; uuid?: string }>> {
71
+ const result = await $`wrangler d1 list --json`.nothrow().quiet();
72
+
73
+ if (result.exitCode !== 0) {
74
+ const stderr = result.stderr.toString().trim();
75
+ throw new Error(stderr || "Failed to list D1 databases");
76
+ }
77
+
78
+ try {
79
+ const output = result.stdout.toString().trim();
80
+ const parsed = JSON.parse(output);
81
+ if (Array.isArray(parsed)) return parsed;
82
+ if (Array.isArray(parsed?.result)) return parsed.result;
83
+ if (Array.isArray(parsed?.databases)) return parsed.databases;
84
+ } catch {
85
+ // Fall through to empty list
86
+ }
87
+
88
+ return [];
89
+ }
90
+
67
91
  /**
68
92
  * List all workers for the current account
69
93
  * Parses the output of `wrangler deployments list` to extract worker names
package/src/lib/config.ts CHANGED
@@ -6,12 +6,11 @@ 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
- };
9
+ export type AgentLaunchConfig = {
10
+ type: "cli";
11
+ command: string;
12
+ args?: string[];
13
+ };
15
14
 
16
15
  export interface AgentConfig {
17
16
  active: boolean;