@getjack/jack 0.1.9 → 0.1.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
package/src/lib/hooks.ts CHANGED
@@ -1,10 +1,44 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import * as readline from "node:readline";
3
4
  import type { HookAction } from "../templates/types";
4
5
  import { applyJsonWrite } from "./json-edit";
5
6
  import { getSavedSecrets } from "./secrets";
6
7
  import { restoreTty } from "./tty";
7
8
 
9
+ /**
10
+ * Read multi-line JSON input from stdin
11
+ * User pastes JSON, then presses Enter on empty line to submit
12
+ */
13
+ async function readMultilineJson(prompt: string): Promise<string> {
14
+ console.error(prompt);
15
+ console.error("(Paste JSON, then press Enter on empty line to submit)\n");
16
+
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stderr,
20
+ });
21
+
22
+ const lines: string[] = [];
23
+
24
+ return new Promise((resolve) => {
25
+ rl.on("line", (line) => {
26
+ if (line.trim() === "" && lines.length > 0) {
27
+ rl.close();
28
+ resolve(lines.join("\n"));
29
+ return;
30
+ }
31
+ if (line.trim() !== "") {
32
+ lines.push(line);
33
+ }
34
+ });
35
+
36
+ rl.on("close", () => {
37
+ resolve(lines.join("\n"));
38
+ });
39
+ });
40
+ }
41
+
8
42
  export interface HookContext {
9
43
  domain?: string; // deployed domain (e.g., "my-app.username.workers.dev")
10
44
  url?: string; // full deployed URL
@@ -147,14 +181,39 @@ function resolveHookPath(filePath: string, context: HookContext): string {
147
181
  return join(context.projectDir, filePath);
148
182
  }
149
183
 
150
- function isAccountAssociation(value: unknown): value is { header: string; payload: string; signature: string } {
184
+ function isAccountAssociation(value: unknown): boolean {
151
185
  if (!value || typeof value !== "object") return false;
152
- const obj = value as { header?: unknown; payload?: unknown; signature?: unknown };
153
- return (
154
- typeof obj.header === "string" &&
155
- typeof obj.payload === "string" &&
156
- typeof obj.signature === "string"
157
- );
186
+ // Check direct format: { header, payload, signature }
187
+ const obj = value as Record<string, unknown>;
188
+ if (typeof obj.header === "string" && typeof obj.payload === "string" && typeof obj.signature === "string") {
189
+ return true;
190
+ }
191
+ // Check nested format from Farcaster: { accountAssociation: { header, payload, signature } }
192
+ if (obj.accountAssociation && typeof obj.accountAssociation === "object") {
193
+ const inner = obj.accountAssociation as Record<string, unknown>;
194
+ return typeof inner.header === "string" && typeof inner.payload === "string" && typeof inner.signature === "string";
195
+ }
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Extract the accountAssociation object (handles both nested and flat formats)
201
+ */
202
+ function extractAccountAssociation(value: unknown): { header: string; payload: string; signature: string } | null {
203
+ if (!value || typeof value !== "object") return null;
204
+ const obj = value as Record<string, unknown>;
205
+ // Direct format
206
+ if (typeof obj.header === "string" && typeof obj.payload === "string" && typeof obj.signature === "string") {
207
+ return { header: obj.header, payload: obj.payload, signature: obj.signature };
208
+ }
209
+ // Nested format from Farcaster
210
+ if (obj.accountAssociation && typeof obj.accountAssociation === "object") {
211
+ const inner = obj.accountAssociation as Record<string, unknown>;
212
+ if (typeof inner.header === "string" && typeof inner.payload === "string" && typeof inner.signature === "string") {
213
+ return { header: inner.header, payload: inner.payload, signature: inner.signature };
214
+ }
215
+ }
216
+ return null;
158
217
  }
159
218
 
160
219
  /**
@@ -363,16 +422,21 @@ const actionHandlers: {
363
422
  return true;
364
423
  }
365
424
 
366
- const { input } = await import("@inquirer/prompts");
367
-
368
425
  let rawValue = "";
369
- try {
370
- rawValue = await input({ message: action.message });
371
- } catch (err) {
372
- if (err instanceof Error && err.name === "ExitPromptError") {
373
- return true;
426
+
427
+ // Use multi-line input for JSON validation (handles paste from Farcaster etc.)
428
+ if (action.validate === "json" || action.validate === "accountAssociation") {
429
+ rawValue = await readMultilineJson(action.message);
430
+ } else {
431
+ const { input } = await import("@inquirer/prompts");
432
+ try {
433
+ rawValue = await input({ message: action.message });
434
+ } catch (err) {
435
+ if (err instanceof Error && err.name === "ExitPromptError") {
436
+ return true;
437
+ }
438
+ throw err;
374
439
  }
375
- throw err;
376
440
  }
377
441
 
378
442
  if (!rawValue.trim()) {
@@ -384,14 +448,24 @@ const actionHandlers: {
384
448
  try {
385
449
  parsedInput = JSON.parse(rawValue);
386
450
  } catch {
387
- ui.error("Invalid JSON input");
388
- return action.required ? false : true;
451
+ // Try normalizing whitespace (handles some multi-line paste issues)
452
+ try {
453
+ const normalized = rawValue.replace(/\n\s*/g, "");
454
+ parsedInput = JSON.parse(normalized);
455
+ } catch {
456
+ ui.error("Invalid JSON input");
457
+ return action.required ? false : true;
458
+ }
389
459
  }
390
460
  }
391
461
 
392
- if (action.validate === "accountAssociation" && !isAccountAssociation(parsedInput)) {
393
- ui.error("Invalid accountAssociation JSON (expected header, payload, signature)");
394
- return action.required ? false : true;
462
+ if (action.validate === "accountAssociation") {
463
+ if (!isAccountAssociation(parsedInput)) {
464
+ ui.error("Invalid accountAssociation JSON (expected header, payload, signature)");
465
+ return action.required ? false : true;
466
+ }
467
+ // Extract the actual accountAssociation object (handles nested format from Farcaster)
468
+ parsedInput = extractAccountAssociation(parsedInput);
395
469
  }
396
470
 
397
471
  if (action.writeJson) {
package/src/lib/output.ts CHANGED
@@ -148,16 +148,19 @@ export function celebrate(title: string, lines: string[]): void {
148
148
  const gradient = "░".repeat(innerWidth);
149
149
  const space = " ".repeat(innerWidth);
150
150
 
151
- const center = (text: string) => {
151
+ // Center text based on visual length, then apply colors
152
+ const center = (text: string, applyBold = false) => {
152
153
  const left = Math.floor((innerWidth - text.length) / 2);
153
- return " ".repeat(left) + text + " ".repeat(innerWidth - text.length - left);
154
+ const right = innerWidth - text.length - left;
155
+ const centered = " ".repeat(left) + text + " ".repeat(right);
156
+ return applyBold ? centered.replace(text, bold + text + reset + purple) : centered;
154
157
  };
155
158
 
156
159
  console.error("");
157
160
  console.error(` ${purple}╔${bar}╗${reset}`);
158
161
  console.error(` ${purple}║${fill}║${reset}`);
159
162
  console.error(` ${purple}║${space}║${reset}`);
160
- console.error(` ${purple}║${center(bold + title + reset + purple)}║${reset}`);
163
+ console.error(` ${purple}║${center(title, true)}║${reset}`);
161
164
  console.error(` ${purple}║${space}║${reset}`);
162
165
  for (const line of lines) {
163
166
  console.error(` ${purple}║${center(line)}║${reset}`);