@getjack/jack 0.1.19 → 0.1.22
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/package.json +5 -2
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +19 -6
- package/src/commands/new.ts +56 -4
- package/src/commands/publish.ts +1 -1
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +15 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +13 -6
- package/src/lib/managed-down.ts +66 -45
- package/src/lib/progress.ts +76 -5
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +21 -31
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/zip-packager.ts +36 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +172 -5
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- package/templates/saas/wrangler.jsonc +20 -0
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"jack": "./src/index.ts"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
11
14
|
"engines": {
|
|
12
15
|
"bun": ">=1.0.0"
|
|
13
16
|
},
|
package/src/commands/down.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { fetchProjectResources } from "../lib/control-plane.ts";
|
|
|
10
10
|
import { promptSelect } from "../lib/hooks.ts";
|
|
11
11
|
import { managedDown } from "../lib/managed-down.ts";
|
|
12
12
|
import { error, info, item, output, success, warn } from "../lib/output.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { unregisterPath } from "../lib/paths-index.ts";
|
|
14
|
+
import { type LocalProjectLink, readProjectLink, unlinkProject } from "../lib/project-link.ts";
|
|
14
15
|
import { resolveProject } from "../lib/project-resolver.ts";
|
|
15
16
|
import { parseWranglerResources } from "../lib/resources.ts";
|
|
16
17
|
import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
@@ -124,6 +125,15 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
|
|
|
124
125
|
if (!deleteSuccess) {
|
|
125
126
|
process.exit(0); // User cancelled
|
|
126
127
|
}
|
|
128
|
+
|
|
129
|
+
// Clean up local tracking state
|
|
130
|
+
const localPath = resolved?.localPath || process.cwd();
|
|
131
|
+
try {
|
|
132
|
+
await unlinkProject(localPath);
|
|
133
|
+
await unregisterPath(projectId, localPath);
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-fatal: local cleanup failed but cloud deletion succeeded
|
|
136
|
+
}
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
139
|
|
package/src/commands/init.ts
CHANGED
|
@@ -11,14 +11,27 @@ import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts"
|
|
|
11
11
|
|
|
12
12
|
export async function isInitialized(): Promise<boolean> {
|
|
13
13
|
const config = await readConfig();
|
|
14
|
-
if (!config?.initialized) return false;
|
|
15
|
-
|
|
16
|
-
// Check Jack Cloud auth first (most common path)
|
|
17
14
|
const { isLoggedIn } = await import("../lib/auth/store.ts");
|
|
18
|
-
if (await isLoggedIn()) return true;
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if (config?.initialized) {
|
|
17
|
+
if ((await isLoggedIn()) || (await isAuthenticated())) return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Auto-initialize if authenticated
|
|
21
|
+
const loggedIn = await isLoggedIn();
|
|
22
|
+
const wranglerAuth = !loggedIn && (await isAuthenticated());
|
|
23
|
+
|
|
24
|
+
if (loggedIn || wranglerAuth) {
|
|
25
|
+
await writeConfig({
|
|
26
|
+
version: 1,
|
|
27
|
+
initialized: true,
|
|
28
|
+
initializedAt: new Date().toISOString(),
|
|
29
|
+
...config,
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
interface InitOptions {
|
package/src/commands/new.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getAgentDefinition,
|
|
3
|
+
getPreferredLaunchAgent,
|
|
4
|
+
launchAgent,
|
|
5
|
+
scanAgents,
|
|
6
|
+
updateAgent,
|
|
7
|
+
} from "../lib/agents.ts";
|
|
2
8
|
import { debug } from "../lib/debug.ts";
|
|
3
9
|
import { getErrorDetails } from "../lib/errors.ts";
|
|
4
10
|
import { promptSelect } from "../lib/hooks.ts";
|
|
@@ -120,9 +126,55 @@ export default async function newProject(
|
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
} else {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
// No agents configured - auto-scan and offer to open in detected agents
|
|
130
|
+
const detectionResult = await scanAgents();
|
|
131
|
+
|
|
132
|
+
if (detectionResult.detected.length > 0) {
|
|
133
|
+
// Auto-enable newly detected agents
|
|
134
|
+
for (const { id, path, launch } of detectionResult.detected) {
|
|
135
|
+
await updateAgent(id, {
|
|
136
|
+
active: true,
|
|
137
|
+
path,
|
|
138
|
+
detectedAt: new Date().toISOString(),
|
|
139
|
+
launch,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build menu options: detected agents + Skip
|
|
144
|
+
const menuOptions = detectionResult.detected.map(({ id }) => {
|
|
145
|
+
const definition = getAgentDefinition(id);
|
|
146
|
+
return definition?.name ?? id;
|
|
147
|
+
});
|
|
148
|
+
menuOptions.push("Skip");
|
|
149
|
+
|
|
150
|
+
console.error("");
|
|
151
|
+
console.error(" Open project in:");
|
|
152
|
+
console.error("");
|
|
153
|
+
const choice = await promptSelect(menuOptions);
|
|
154
|
+
|
|
155
|
+
// Launch selected agent (unless Skip or cancelled)
|
|
156
|
+
if (choice >= 0 && choice < detectionResult.detected.length) {
|
|
157
|
+
const selected = detectionResult.detected[choice];
|
|
158
|
+
const launchConfig = selected.launch;
|
|
159
|
+
if (launchConfig) {
|
|
160
|
+
const launchResult = await launchAgent(launchConfig, result.targetDir, {
|
|
161
|
+
projectName: result.projectName,
|
|
162
|
+
url: result.workerUrl,
|
|
163
|
+
});
|
|
164
|
+
if (!launchResult.success) {
|
|
165
|
+
const definition = getAgentDefinition(selected.id);
|
|
166
|
+
output.warn(`Failed to launch ${definition?.name ?? selected.id}`);
|
|
167
|
+
if (launchResult.command?.length) {
|
|
168
|
+
output.info(`Run manually: ${launchResult.command.join(" ")}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.error("");
|
|
175
|
+
output.info("No AI agents detected");
|
|
176
|
+
output.info("Install Claude Code or Codex, then run: jack agents scan");
|
|
177
|
+
}
|
|
126
178
|
}
|
|
127
179
|
}
|
|
128
180
|
}
|
package/src/commands/publish.ts
CHANGED
|
@@ -39,7 +39,7 @@ export default async function publish(): Promise<void> {
|
|
|
39
39
|
output.success(`Published as ${result.published_as}`);
|
|
40
40
|
|
|
41
41
|
console.error("");
|
|
42
|
-
output.info("
|
|
42
|
+
output.info("Share this project:");
|
|
43
43
|
output.info(` ${result.fork_command}`);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
spin.stop();
|
package/src/lib/agents.ts
CHANGED
|
@@ -434,7 +434,9 @@ export interface AgentLaunchContext {
|
|
|
434
434
|
function buildInitialPrompt(context: AgentLaunchContext): string | null {
|
|
435
435
|
if (!context.url) return null;
|
|
436
436
|
|
|
437
|
-
return `
|
|
437
|
+
return `Project "${context.projectName}" is live at ${context.url}
|
|
438
|
+
|
|
439
|
+
Read CLAUDE.md and AGENTS.md for project context, then say hi and offer to help build.`;
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
function buildLaunchCommand(
|
|
@@ -210,9 +210,9 @@ describe("ensureAuthForCreate", () => {
|
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
it("throws error when both forceManaged and forceByo are set", async () => {
|
|
213
|
-
await expect(
|
|
214
|
-
|
|
215
|
-
)
|
|
213
|
+
await expect(ensureAuthForCreate({ forceManaged: true, forceByo: true })).rejects.toThrow(
|
|
214
|
+
"Cannot use both --managed and --byo flags",
|
|
215
|
+
);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
218
|
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Control Plane API client for jack cloud
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { debug } from "./debug.ts";
|
|
6
|
+
import { formatSize } from "./format.ts";
|
|
7
|
+
|
|
5
8
|
const DEFAULT_CONTROL_API_URL = "https://control.getjack.org";
|
|
6
9
|
|
|
7
10
|
export function getControlApiUrl(): string {
|
|
@@ -109,6 +112,9 @@ export async function createManagedProject(
|
|
|
109
112
|
requestBody.use_prebuilt = options.usePrebuilt;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
debug("Creating managed project", { name, template: options?.template, usePrebuilt: options?.usePrebuilt });
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
|
|
112
118
|
const response = await authFetch(`${getControlApiUrl()}/v1/projects`, {
|
|
113
119
|
method: "POST",
|
|
114
120
|
headers: {
|
|
@@ -118,6 +124,9 @@ export async function createManagedProject(
|
|
|
118
124
|
body: JSON.stringify(requestBody),
|
|
119
125
|
});
|
|
120
126
|
|
|
127
|
+
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
128
|
+
debug(`Control plane response: ${response.status} (${duration}s)`);
|
|
129
|
+
|
|
121
130
|
if (!response.ok) {
|
|
122
131
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
123
132
|
message?: string;
|
|
@@ -586,10 +595,15 @@ export async function uploadSourceSnapshot(
|
|
|
586
595
|
const sourceFile = Bun.file(sourceZipPath);
|
|
587
596
|
formData.append("source", sourceFile);
|
|
588
597
|
|
|
589
|
-
const
|
|
598
|
+
const url = `${getControlApiUrl()}/v1/projects/${projectId}/source`;
|
|
599
|
+
debug(`Source snapshot: ${formatSize(sourceFile.size)}`);
|
|
600
|
+
|
|
601
|
+
const start = Date.now();
|
|
602
|
+
const response = await authFetch(url, {
|
|
590
603
|
method: "POST",
|
|
591
604
|
body: formData,
|
|
592
605
|
});
|
|
606
|
+
debug(`Source snapshot: ${response.status} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
593
607
|
|
|
594
608
|
if (!response.ok) {
|
|
595
609
|
const error = (await response.json().catch(() => ({ message: "Upload failed" }))) as {
|
package/src/lib/deploy-upload.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import type { AssetManifest } from "./asset-hash.ts";
|
|
7
7
|
import { authFetch } from "./auth/index.ts";
|
|
8
8
|
import { getControlApiUrl } from "./control-plane.ts";
|
|
9
|
+
import { debug } from "./debug.ts";
|
|
10
|
+
import { formatSize } from "./format.ts";
|
|
9
11
|
|
|
10
12
|
export interface DeployUploadOptions {
|
|
11
13
|
projectId: string;
|
|
@@ -31,6 +33,9 @@ export interface DeployUploadResult {
|
|
|
31
33
|
*/
|
|
32
34
|
export async function uploadDeployment(options: DeployUploadOptions): Promise<DeployUploadResult> {
|
|
33
35
|
const formData = new FormData();
|
|
36
|
+
let totalSize = 0;
|
|
37
|
+
|
|
38
|
+
const prepareStart = Date.now();
|
|
34
39
|
|
|
35
40
|
// Read files and add to form data
|
|
36
41
|
const manifestContent = await readFile(options.manifestPath);
|
|
@@ -39,17 +44,24 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
39
44
|
new Blob([manifestContent], { type: "application/json" }),
|
|
40
45
|
"manifest.json",
|
|
41
46
|
);
|
|
47
|
+
totalSize += manifestContent.length;
|
|
42
48
|
|
|
43
49
|
const bundleContent = await readFile(options.bundleZipPath);
|
|
44
50
|
formData.append("bundle", new Blob([bundleContent], { type: "application/zip" }), "bundle.zip");
|
|
51
|
+
totalSize += bundleContent.length;
|
|
52
|
+
debug(` bundle.zip: ${formatSize(bundleContent.length)}`);
|
|
45
53
|
|
|
46
54
|
const sourceContent = await readFile(options.sourceZipPath);
|
|
47
55
|
formData.append("source", new Blob([sourceContent], { type: "application/zip" }), "source.zip");
|
|
56
|
+
totalSize += sourceContent.length;
|
|
57
|
+
debug(` source.zip: ${formatSize(sourceContent.length)}`);
|
|
48
58
|
|
|
49
59
|
// Optional files
|
|
50
60
|
if (options.schemaPath) {
|
|
51
61
|
const schemaContent = await readFile(options.schemaPath);
|
|
52
62
|
formData.append("schema", new Blob([schemaContent], { type: "text/sql" }), "schema.sql");
|
|
63
|
+
totalSize += schemaContent.length;
|
|
64
|
+
debug(` schema.sql: ${formatSize(schemaContent.length)}`);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
if (options.secretsPath) {
|
|
@@ -59,27 +71,40 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
59
71
|
new Blob([secretsContent], { type: "application/json" }),
|
|
60
72
|
"secrets.json",
|
|
61
73
|
);
|
|
74
|
+
totalSize += secretsContent.length;
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
if (options.assetsZipPath) {
|
|
65
78
|
const assetsContent = await readFile(options.assetsZipPath);
|
|
66
79
|
formData.append("assets", new Blob([assetsContent], { type: "application/zip" }), "assets.zip");
|
|
80
|
+
totalSize += assetsContent.length;
|
|
81
|
+
debug(` assets.zip: ${formatSize(assetsContent.length)}`);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
if (options.assetManifest) {
|
|
85
|
+
const manifestJson = JSON.stringify(options.assetManifest);
|
|
70
86
|
formData.append(
|
|
71
87
|
"asset-manifest",
|
|
72
|
-
new Blob([
|
|
88
|
+
new Blob([manifestJson], { type: "application/json" }),
|
|
73
89
|
"asset-manifest.json",
|
|
74
90
|
);
|
|
91
|
+
totalSize += manifestJson.length;
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
const prepareMs = Date.now() - prepareStart;
|
|
95
|
+
debug(`Payload ready: ${formatSize(totalSize)} (${prepareMs}ms)`);
|
|
96
|
+
|
|
77
97
|
// POST to control plane
|
|
78
98
|
const url = `${getControlApiUrl()}/v1/projects/${options.projectId}/deployments/upload`;
|
|
99
|
+
debug(`POST ${url}`);
|
|
100
|
+
|
|
101
|
+
const uploadStart = Date.now();
|
|
79
102
|
const response = await authFetch(url, {
|
|
80
103
|
method: "POST",
|
|
81
104
|
body: formData,
|
|
82
105
|
});
|
|
106
|
+
const uploadMs = Date.now() - uploadStart;
|
|
107
|
+
debug(`Response: ${response.status} (${(uploadMs / 1000).toFixed(1)}s)`);
|
|
83
108
|
|
|
84
109
|
if (!response.ok) {
|
|
85
110
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
package/src/lib/hooks.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import type { HookAction } from "../templates/types";
|
|
5
5
|
import { applyJsonWrite } from "./json-edit";
|
|
6
|
-
import { getSavedSecrets } from "./secrets";
|
|
6
|
+
import { getSavedSecrets, saveSecrets } from "./secrets";
|
|
7
7
|
import { restoreTty } from "./tty";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -379,9 +379,87 @@ const actionHandlers: {
|
|
|
379
379
|
require: async (action, context, options) => {
|
|
380
380
|
const ui = options.output ?? noopOutput;
|
|
381
381
|
const interactive = options.interactive !== false;
|
|
382
|
+
const onMissing = action.onMissing ?? "fail";
|
|
383
|
+
|
|
382
384
|
if (action.source === "secret") {
|
|
383
385
|
const result = await checkSecretExists(action.key, context.projectDir);
|
|
386
|
+
if (result.exists) {
|
|
387
|
+
// Found existing secret - show feedback for prompt/generate modes
|
|
388
|
+
if (onMissing === "prompt" || onMissing === "generate") {
|
|
389
|
+
ui.success(`Using saved ${action.key}`);
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Secret doesn't exist - handle based on onMissing mode
|
|
384
395
|
if (!result.exists) {
|
|
396
|
+
// Handle onMissing: "generate" - run command and save output
|
|
397
|
+
if (onMissing === "generate" && action.generateCommand) {
|
|
398
|
+
const message = action.message ?? `Generating ${action.key}...`;
|
|
399
|
+
ui.info(message);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const proc = Bun.spawn(["sh", "-c", action.generateCommand], {
|
|
403
|
+
stdout: "pipe",
|
|
404
|
+
stderr: "pipe",
|
|
405
|
+
});
|
|
406
|
+
await proc.exited;
|
|
407
|
+
|
|
408
|
+
if (proc.exitCode === 0) {
|
|
409
|
+
const stdout = await new Response(proc.stdout).text();
|
|
410
|
+
const value = stdout.trim();
|
|
411
|
+
if (value) {
|
|
412
|
+
await saveSecrets([{ key: action.key, value, source: "generated" }]);
|
|
413
|
+
ui.success(`Generated ${action.key}`);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
ui.error(`Failed to generate ${action.key}`);
|
|
418
|
+
return false;
|
|
419
|
+
} catch {
|
|
420
|
+
ui.error(`Failed to run: ${action.generateCommand}`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Handle onMissing: "prompt" - ask user for value
|
|
426
|
+
if (onMissing === "prompt") {
|
|
427
|
+
if (!interactive) {
|
|
428
|
+
// Fall back to fail behavior in non-interactive mode
|
|
429
|
+
const message = action.message ?? `Missing required secret: ${action.key}`;
|
|
430
|
+
ui.error(message);
|
|
431
|
+
ui.info(`Run: jack secrets add ${action.key}`);
|
|
432
|
+
if (action.setupUrl) {
|
|
433
|
+
ui.info(`Setup: ${action.setupUrl}`);
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Show setup info and go straight to prompt (URLs are clickable in most terminals)
|
|
439
|
+
const promptMsg = action.promptMessage ?? `${action.key}:`;
|
|
440
|
+
console.error("");
|
|
441
|
+
if (action.message) {
|
|
442
|
+
console.error(` ${action.message}`);
|
|
443
|
+
}
|
|
444
|
+
if (action.setupUrl) {
|
|
445
|
+
console.error(` Get it at: \x1b[36m${action.setupUrl}\x1b[0m`);
|
|
446
|
+
}
|
|
447
|
+
console.error("");
|
|
448
|
+
|
|
449
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
450
|
+
const value = await text({ message: promptMsg });
|
|
451
|
+
|
|
452
|
+
if (isCancel(value) || !value.trim()) {
|
|
453
|
+
ui.warn(`Skipped ${action.key}`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await saveSecrets([{ key: action.key, value: value.trim(), source: "prompted" }]);
|
|
458
|
+
ui.success(`Saved ${action.key}`);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Default: onMissing: "fail"
|
|
385
463
|
const message = action.message ?? `Missing required secret: ${action.key}`;
|
|
386
464
|
ui.error(message);
|
|
387
465
|
ui.info(`Run: jack secrets add ${action.key}`);
|
|
@@ -425,6 +503,14 @@ const actionHandlers: {
|
|
|
425
503
|
// Use multi-line input for JSON validation (handles paste from Farcaster etc.)
|
|
426
504
|
if (action.validate === "json" || action.validate === "accountAssociation") {
|
|
427
505
|
rawValue = await readMultilineJson(action.message);
|
|
506
|
+
} else if (action.secret) {
|
|
507
|
+
// Use password() for sensitive input (masks the value)
|
|
508
|
+
const { isCancel, password } = await import("@clack/prompts");
|
|
509
|
+
const result = await password({ message: action.message });
|
|
510
|
+
if (isCancel(result)) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
rawValue = result;
|
|
428
514
|
} else {
|
|
429
515
|
const { isCancel, text } = await import("@clack/prompts");
|
|
430
516
|
const result = await text({ message: action.message });
|
|
@@ -538,6 +624,151 @@ const actionHandlers: {
|
|
|
538
624
|
await proc.exited;
|
|
539
625
|
return proc.exitCode === 0;
|
|
540
626
|
},
|
|
627
|
+
"stripe-setup": async (action, context, options) => {
|
|
628
|
+
const ui = options.output ?? noopOutput;
|
|
629
|
+
|
|
630
|
+
// Get Stripe API key from saved secrets
|
|
631
|
+
const savedSecrets = await getSavedSecrets();
|
|
632
|
+
const stripeKey = savedSecrets.STRIPE_SECRET_KEY;
|
|
633
|
+
|
|
634
|
+
if (!stripeKey) {
|
|
635
|
+
ui.error("Missing STRIPE_SECRET_KEY - run the secret prompt first");
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const message = action.message ?? "Setting up Stripe products and prices...";
|
|
640
|
+
ui.info(message);
|
|
641
|
+
|
|
642
|
+
// Helper to make Stripe API requests
|
|
643
|
+
async function stripeRequest(
|
|
644
|
+
method: string,
|
|
645
|
+
endpoint: string,
|
|
646
|
+
body?: Record<string, string>,
|
|
647
|
+
): Promise<{ ok: boolean; data?: Record<string, unknown>; error?: string }> {
|
|
648
|
+
const url = `https://api.stripe.com/v1${endpoint}`;
|
|
649
|
+
const headers: Record<string, string> = {
|
|
650
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const fetchOptions: RequestInit = { method, headers };
|
|
654
|
+
if (body) {
|
|
655
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
656
|
+
fetchOptions.body = new URLSearchParams(body).toString();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const response = await fetch(url, fetchOptions);
|
|
661
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
662
|
+
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
const error = data.error as { message?: string } | undefined;
|
|
665
|
+
return { ok: false, error: error?.message ?? "Stripe API error" };
|
|
666
|
+
}
|
|
667
|
+
return { ok: true, data };
|
|
668
|
+
} catch (err) {
|
|
669
|
+
return { ok: false, error: String(err) };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Search for existing price by lookup_key
|
|
674
|
+
async function findPriceByLookupKey(lookupKey: string): Promise<string | null> {
|
|
675
|
+
const result = await stripeRequest(
|
|
676
|
+
"GET",
|
|
677
|
+
`/prices?lookup_keys[]=${encodeURIComponent(lookupKey)}&active=true`,
|
|
678
|
+
);
|
|
679
|
+
if (result.ok && result.data) {
|
|
680
|
+
const prices = result.data.data as Array<{ id: string }>;
|
|
681
|
+
if (prices.length > 0) {
|
|
682
|
+
return prices[0].id;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Create a new product
|
|
689
|
+
async function createProduct(name: string, description?: string): Promise<string | null> {
|
|
690
|
+
const body: Record<string, string> = { name };
|
|
691
|
+
if (description) {
|
|
692
|
+
body.description = description;
|
|
693
|
+
}
|
|
694
|
+
const result = await stripeRequest("POST", "/products", body);
|
|
695
|
+
if (result.ok && result.data) {
|
|
696
|
+
return result.data.id as string;
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Create a new price with lookup_key
|
|
702
|
+
async function createPrice(
|
|
703
|
+
productId: string,
|
|
704
|
+
amount: number,
|
|
705
|
+
interval: "month" | "year",
|
|
706
|
+
lookupKey: string,
|
|
707
|
+
): Promise<string | null> {
|
|
708
|
+
const result = await stripeRequest("POST", "/prices", {
|
|
709
|
+
product: productId,
|
|
710
|
+
unit_amount: String(amount),
|
|
711
|
+
currency: "usd",
|
|
712
|
+
"recurring[interval]": interval,
|
|
713
|
+
lookup_key: lookupKey,
|
|
714
|
+
});
|
|
715
|
+
if (result.ok && result.data) {
|
|
716
|
+
return result.data.id as string;
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const secretsToSave: Array<{ key: string; value: string; source: string }> = [];
|
|
722
|
+
|
|
723
|
+
for (const plan of action.plans) {
|
|
724
|
+
const lookupKey = `jack_${plan.name.toLowerCase()}_${plan.interval}`;
|
|
725
|
+
|
|
726
|
+
// Check if price key already exists in secrets (manual override)
|
|
727
|
+
if (savedSecrets[plan.priceKey]) {
|
|
728
|
+
ui.success(`Using existing ${plan.priceKey}`);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Search for existing price by lookup_key
|
|
733
|
+
ui.info(`Checking for existing ${plan.name} price...`);
|
|
734
|
+
let priceId = await findPriceByLookupKey(lookupKey);
|
|
735
|
+
|
|
736
|
+
if (priceId) {
|
|
737
|
+
ui.success(`Found existing ${plan.name} price: ${priceId}`);
|
|
738
|
+
} else {
|
|
739
|
+
// Create product and price
|
|
740
|
+
ui.info(`Creating ${plan.name} product and price...`);
|
|
741
|
+
|
|
742
|
+
const productId = await createProduct(plan.name, plan.description ?? `${plan.name} plan`);
|
|
743
|
+
if (!productId) {
|
|
744
|
+
ui.error(`Failed to create ${plan.name} product`);
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
priceId = await createPrice(productId, plan.amount, plan.interval, lookupKey);
|
|
749
|
+
if (!priceId) {
|
|
750
|
+
ui.error(`Failed to create ${plan.name} price`);
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
ui.success(`Created ${plan.name}: ${priceId}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
secretsToSave.push({
|
|
758
|
+
key: plan.priceKey,
|
|
759
|
+
value: priceId,
|
|
760
|
+
source: "stripe-setup",
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Save all price IDs to secrets
|
|
765
|
+
if (secretsToSave.length > 0) {
|
|
766
|
+
await saveSecrets(secretsToSave);
|
|
767
|
+
ui.success("Saved Stripe price IDs to secrets");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return true;
|
|
771
|
+
},
|
|
541
772
|
};
|
|
542
773
|
|
|
543
774
|
async function executeAction(
|
|
@@ -12,7 +12,7 @@ import { debug } from "./debug.ts";
|
|
|
12
12
|
import { uploadDeployment } from "./deploy-upload.ts";
|
|
13
13
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
14
14
|
import { formatSize } from "./format.ts";
|
|
15
|
-
import { createUploadProgress } from "./progress.ts";
|
|
15
|
+
import { createFileCountProgress, createUploadProgress } from "./progress.ts";
|
|
16
16
|
import type { OperationReporter } from "./project-operations.ts";
|
|
17
17
|
import { getProjectTags } from "./tags.ts";
|
|
18
18
|
import { Events, track } from "./telemetry.ts";
|
|
@@ -116,10 +116,17 @@ export async function deployCodeToManagedProject(
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// Step 3: Package artifacts
|
|
120
|
-
reporter?.
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
// Step 3: Package artifacts with file-count progress
|
|
120
|
+
reporter?.stop(); // Stop reporter spinner, we'll use our own progress
|
|
121
|
+
const packagingProgress = createFileCountProgress({ label: "Packaging" });
|
|
122
|
+
packagingProgress.start();
|
|
123
|
+
pkg = await packageForDeploy({
|
|
124
|
+
projectPath,
|
|
125
|
+
buildOutput,
|
|
126
|
+
config,
|
|
127
|
+
onProgress: (current, total) => packagingProgress.update(current, total),
|
|
128
|
+
});
|
|
129
|
+
packagingProgress.complete();
|
|
123
130
|
reporter?.success("Packaged artifacts");
|
|
124
131
|
|
|
125
132
|
// Step 4: Upload to control plane
|
|
@@ -141,7 +148,7 @@ export async function deployCodeToManagedProject(
|
|
|
141
148
|
// Use custom progress with pulsing bar (since fetch doesn't support upload progress)
|
|
142
149
|
const uploadProgress = createUploadProgress({
|
|
143
150
|
totalSize: totalUploadSize,
|
|
144
|
-
label: "
|
|
151
|
+
label: "Deploying to jack cloud",
|
|
145
152
|
});
|
|
146
153
|
uploadProgress.start();
|
|
147
154
|
|