@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,413 @@
1
+ /**
2
+ * jack secrets - Manage project secrets securely
3
+ *
4
+ * Secrets are stored in Cloudflare and never written to disk.
5
+ * For managed projects: uses jack cloud control plane.
6
+ * For BYO projects: uses wrangler secret commands.
7
+ */
8
+
9
+ import { password as passwordPrompt } from "@inquirer/prompts";
10
+ import { $ } from "bun";
11
+ import { getControlApiUrl } from "../lib/control-plane.ts";
12
+ import { JackError, JackErrorCode } from "../lib/errors.ts";
13
+ import { error, info, output, success, warn } from "../lib/output.ts";
14
+ import { type Project, getProject } from "../lib/registry.ts";
15
+ import { getProjectNameFromDir } from "../lib/storage/index.ts";
16
+
17
+ interface SecretsOptions {
18
+ project?: string;
19
+ }
20
+
21
+ export default async function secrets(
22
+ subcommand?: string,
23
+ args: string[] = [],
24
+ options: SecretsOptions = {},
25
+ ): Promise<void> {
26
+ if (!subcommand) {
27
+ return showHelp();
28
+ }
29
+
30
+ switch (subcommand) {
31
+ case "set":
32
+ case "add":
33
+ return await setSecret(args, options);
34
+ case "list":
35
+ case "ls":
36
+ return await listSecrets(options);
37
+ case "rm":
38
+ case "remove":
39
+ case "delete":
40
+ return await removeSecret(args, options);
41
+ default:
42
+ error(`Unknown subcommand: ${subcommand}`);
43
+ info("Available: set, list, rm");
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ function showHelp(): void {
49
+ console.error("");
50
+ info("jack secrets - Manage project secrets");
51
+ console.error("");
52
+ console.error("Commands:");
53
+ console.error(" set <KEY> Set a secret (interactive prompt)");
54
+ console.error(" set KEY=VALUE Set a secret (warning: visible in shell history)");
55
+ console.error(" list List secret names");
56
+ console.error(" rm <KEY> Remove a secret");
57
+ console.error("");
58
+ console.error("Options:");
59
+ console.error(" --project, -p Project name (auto-detected from cwd)");
60
+ console.error("");
61
+ console.error("Piping from stdin (for CI/CD):");
62
+ console.error(" echo $SECRET | jack secrets set KEY");
63
+ console.error("");
64
+ }
65
+
66
+ /**
67
+ * Resolve project and determine if it's managed or BYO
68
+ */
69
+ async function resolveProjectContext(options: SecretsOptions): Promise<{
70
+ projectName: string;
71
+ project: Project | null;
72
+ isManaged: boolean;
73
+ projectId: string | null;
74
+ }> {
75
+ let projectName: string;
76
+
77
+ if (options.project) {
78
+ projectName = options.project;
79
+ } else {
80
+ try {
81
+ projectName = await getProjectNameFromDir(process.cwd());
82
+ } catch {
83
+ error("Could not determine project");
84
+ info("Run from a project directory, or use --project <name>");
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ const project = await getProject(projectName);
90
+ const isManaged = project?.deploy_mode === "managed";
91
+ const projectId = project?.remote?.project_id ?? null;
92
+
93
+ return { projectName, project, isManaged, projectId };
94
+ }
95
+
96
+ /**
97
+ * Read a secret value interactively without echoing
98
+ * Uses @inquirer/prompts password for robust handling of typing, pasting, and TTY
99
+ */
100
+ async function readSecretInteractive(keyName: string): Promise<string> {
101
+ try {
102
+ const value = await passwordPrompt({
103
+ message: `Enter value for ${keyName}`,
104
+ mask: "*",
105
+ });
106
+ return value;
107
+ } catch (err) {
108
+ // Handle Ctrl+C / Escape - inquirer throws ExitPromptError
109
+ if (err instanceof Error && err.name === "ExitPromptError") {
110
+ throw new Error("Cancelled");
111
+ }
112
+ throw err;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Read secret value from stdin (for piping in CI/CD)
118
+ */
119
+ async function readSecretFromStdin(): Promise<string> {
120
+ const chunks: Buffer[] = [];
121
+
122
+ for await (const chunk of process.stdin) {
123
+ chunks.push(chunk);
124
+ }
125
+
126
+ return Buffer.concat(chunks).toString("utf-8").trim();
127
+ }
128
+
129
+ /**
130
+ * Set a secret value
131
+ */
132
+ async function setSecret(args: string[], options: SecretsOptions): Promise<void> {
133
+ const arg = args[0];
134
+
135
+ if (!arg) {
136
+ error("Missing secret key");
137
+ info("Usage: jack secrets set <KEY> or jack secrets set KEY=VALUE");
138
+ process.exit(1);
139
+ }
140
+
141
+ let keyName: string;
142
+ let value: string | undefined;
143
+
144
+ // Check for KEY=VALUE format
145
+ const equalsIndex = arg.indexOf("=");
146
+ if (equalsIndex > 0) {
147
+ keyName = arg.slice(0, equalsIndex);
148
+ value = arg.slice(equalsIndex + 1);
149
+
150
+ // Warn about shell history exposure
151
+ warn("Value visible in shell history. Use interactive mode for sensitive secrets.");
152
+ } else {
153
+ keyName = arg;
154
+ }
155
+
156
+ // Validate key name
157
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(keyName)) {
158
+ error("Invalid secret name");
159
+ info(
160
+ "Must start with a letter or underscore, and contain only letters, numbers, and underscores",
161
+ );
162
+ process.exit(1);
163
+ }
164
+
165
+ // Get value if not provided inline
166
+ if (value === undefined) {
167
+ // Check if stdin is piped
168
+ if (!process.stdin.isTTY) {
169
+ value = await readSecretFromStdin();
170
+ if (!value) {
171
+ error("No value provided via stdin");
172
+ process.exit(1);
173
+ }
174
+ } else {
175
+ // Interactive prompt
176
+ try {
177
+ value = await readSecretInteractive(keyName);
178
+ if (!value) {
179
+ error("No value provided");
180
+ process.exit(1);
181
+ }
182
+ } catch (err) {
183
+ if (err instanceof Error && err.message === "Cancelled") {
184
+ info("Cancelled");
185
+ process.exit(0);
186
+ }
187
+ throw err;
188
+ }
189
+ }
190
+ }
191
+
192
+ const { projectName, isManaged, projectId } = await resolveProjectContext(options);
193
+
194
+ output.start("Setting secret...");
195
+
196
+ if (isManaged && projectId) {
197
+ // Managed mode: use control plane API
198
+ await setSecretManaged(projectId, keyName, value);
199
+ } else {
200
+ // BYO mode: use wrangler
201
+ await setSecretByo(projectName, keyName, value);
202
+ }
203
+
204
+ output.stop();
205
+ success(`Secret set: ${keyName}`);
206
+
207
+ // Warn about VITE_ prefix (informative, not blocking)
208
+ if (keyName.startsWith("VITE_")) {
209
+ info("Note: VITE_* variables are embedded at build time, not runtime.");
210
+ info("For frontend access, add to .env and redeploy.");
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Set secret via control plane (managed mode)
216
+ */
217
+ async function setSecretManaged(projectId: string, name: string, value: string): Promise<void> {
218
+ const { authFetch } = await import("../lib/auth/index.ts");
219
+
220
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/secrets`, {
221
+ method: "POST",
222
+ headers: { "Content-Type": "application/json" },
223
+ body: JSON.stringify({ name, value }),
224
+ });
225
+
226
+ if (!response.ok) {
227
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
228
+ message?: string;
229
+ };
230
+
231
+ output.stop();
232
+ throw new JackError(
233
+ JackErrorCode.INTERNAL_ERROR,
234
+ err.message || `Failed to set secret: ${response.status}`,
235
+ );
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Set secret via wrangler (BYO mode)
241
+ */
242
+ async function setSecretByo(projectName: string, name: string, value: string): Promise<void> {
243
+ // Use wrangler secret put with stdin
244
+ const result = await $`echo ${value} | wrangler secret put ${name}`.nothrow().quiet();
245
+
246
+ if (result.exitCode !== 0) {
247
+ output.stop();
248
+ const stderr = result.stderr.toString();
249
+ throw new JackError(
250
+ JackErrorCode.DEPLOY_FAILED,
251
+ `Failed to set secret: ${stderr}`,
252
+ "Make sure wrangler is configured and the project is deployed",
253
+ );
254
+ }
255
+ }
256
+
257
+ /**
258
+ * List secrets
259
+ */
260
+ async function listSecrets(options: SecretsOptions): Promise<void> {
261
+ const { projectName, isManaged, projectId } = await resolveProjectContext(options);
262
+
263
+ output.start("Loading secrets...");
264
+
265
+ let secrets: Array<{ name: string }>;
266
+
267
+ if (isManaged && projectId) {
268
+ secrets = await listSecretsManaged(projectId);
269
+ } else {
270
+ secrets = await listSecretsByo(projectName);
271
+ }
272
+
273
+ output.stop();
274
+
275
+ if (secrets.length === 0) {
276
+ info("No secrets configured");
277
+ return;
278
+ }
279
+
280
+ console.error("");
281
+ for (const secret of secrets) {
282
+ console.error(` ${secret.name}`);
283
+ }
284
+ console.error("");
285
+ }
286
+
287
+ /**
288
+ * List secrets via control plane (managed mode)
289
+ */
290
+ async function listSecretsManaged(projectId: string): Promise<Array<{ name: string }>> {
291
+ const { authFetch } = await import("../lib/auth/index.ts");
292
+
293
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/secrets`);
294
+
295
+ if (!response.ok) {
296
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
297
+ message?: string;
298
+ };
299
+
300
+ output.stop();
301
+ throw new JackError(
302
+ JackErrorCode.INTERNAL_ERROR,
303
+ err.message || `Failed to list secrets: ${response.status}`,
304
+ );
305
+ }
306
+
307
+ const data = (await response.json()) as { secrets: Array<{ name: string }> };
308
+ return data.secrets;
309
+ }
310
+
311
+ /**
312
+ * List secrets via wrangler (BYO mode)
313
+ */
314
+ async function listSecretsByo(_projectName: string): Promise<Array<{ name: string }>> {
315
+ const result = await $`wrangler secret list --format json`.nothrow().quiet();
316
+
317
+ if (result.exitCode !== 0) {
318
+ // If no secrets or project not deployed, return empty list
319
+ const stderr = result.stderr.toString();
320
+ if (stderr.includes("not found") || stderr.includes("does not exist")) {
321
+ return [];
322
+ }
323
+
324
+ output.stop();
325
+ throw new JackError(
326
+ JackErrorCode.DEPLOY_FAILED,
327
+ `Failed to list secrets: ${stderr}`,
328
+ "Make sure wrangler is configured and the project is deployed",
329
+ );
330
+ }
331
+
332
+ try {
333
+ const stdout = result.stdout.toString().trim();
334
+ if (!stdout) {
335
+ return [];
336
+ }
337
+
338
+ const secrets = JSON.parse(stdout) as Array<{ name: string; type: string }>;
339
+ return secrets.map((s) => ({ name: s.name }));
340
+ } catch {
341
+ // If JSON parsing fails, try line-by-line parsing
342
+ const lines = result.stdout.toString().trim().split("\n");
343
+ return lines.filter((line) => line.trim()).map((line) => ({ name: line.trim() }));
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Remove a secret
349
+ */
350
+ async function removeSecret(args: string[], options: SecretsOptions): Promise<void> {
351
+ const keyName = args[0];
352
+
353
+ if (!keyName) {
354
+ error("Missing secret key");
355
+ info("Usage: jack secrets rm <KEY>");
356
+ process.exit(1);
357
+ }
358
+
359
+ const { projectName, isManaged, projectId } = await resolveProjectContext(options);
360
+
361
+ output.start("Removing secret...");
362
+
363
+ if (isManaged && projectId) {
364
+ await removeSecretManaged(projectId, keyName);
365
+ } else {
366
+ await removeSecretByo(projectName, keyName);
367
+ }
368
+
369
+ output.stop();
370
+ success(`Secret removed: ${keyName}`);
371
+ }
372
+
373
+ /**
374
+ * Remove secret via control plane (managed mode)
375
+ */
376
+ async function removeSecretManaged(projectId: string, name: string): Promise<void> {
377
+ const { authFetch } = await import("../lib/auth/index.ts");
378
+
379
+ const response = await authFetch(
380
+ `${getControlApiUrl()}/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
381
+ { method: "DELETE" },
382
+ );
383
+
384
+ if (!response.ok) {
385
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
386
+ message?: string;
387
+ };
388
+
389
+ output.stop();
390
+ throw new JackError(
391
+ JackErrorCode.INTERNAL_ERROR,
392
+ err.message || `Failed to remove secret: ${response.status}`,
393
+ );
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Remove secret via wrangler (BYO mode)
399
+ */
400
+ async function removeSecretByo(_projectName: string, name: string): Promise<void> {
401
+ // Use yes | to auto-confirm the deletion
402
+ const result = await $`yes | wrangler secret delete ${name}`.nothrow().quiet();
403
+
404
+ if (result.exitCode !== 0) {
405
+ output.stop();
406
+ const stderr = result.stderr.toString();
407
+ throw new JackError(
408
+ JackErrorCode.DEPLOY_FAILED,
409
+ `Failed to remove secret: ${stderr}`,
410
+ "Make sure wrangler is configured and the project is deployed",
411
+ );
412
+ }
413
+ }