@getjack/jack 0.1.26 → 0.1.27

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.26",
3
+ "version": "0.1.27",
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",
@@ -1,14 +1,85 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { output } from "../lib/output.ts";
3
- import { getDeployMode } from "../lib/project-link.ts";
3
+ import { getDeployMode, getProjectId } from "../lib/project-link.ts";
4
+ import { authFetch } from "../lib/auth/index.ts";
5
+ import { getControlApiUrl, startLogSession } from "../lib/control-plane.ts";
4
6
 
5
7
  // Lines containing these strings will be filtered out
6
8
  const FILTERED_PATTERNS = ["⛅️ wrangler"];
7
9
 
8
10
  const shouldFilter = (line: string) => FILTERED_PATTERNS.some((pattern) => line.includes(pattern));
9
11
 
10
- export default async function logs(): Promise<void> {
11
- // Check for wrangler config
12
+ export interface LogsOptions {
13
+ label?: string;
14
+ }
15
+
16
+ async function streamManagedLogs(projectId: string, label?: string): Promise<void> {
17
+ const session = await startLogSession(projectId, label);
18
+ const streamUrl = `${getControlApiUrl()}${session.stream.url}`;
19
+
20
+ output.info(`Log session active until ${session.session.expires_at}`);
21
+ output.info("Streaming logs (JSON). Press Ctrl+C to stop.\n");
22
+
23
+ const response = await authFetch(streamUrl, {
24
+ method: "GET",
25
+ headers: { Accept: "text/event-stream" },
26
+ });
27
+
28
+ if (!response.ok || !response.body) {
29
+ const err = (await response.json().catch(() => ({ message: "Failed to open log stream" }))) as {
30
+ message?: string;
31
+ };
32
+ throw new Error(err.message || `Failed to open log stream: ${response.status}`);
33
+ }
34
+
35
+ const reader = response.body.getReader();
36
+ const decoder = new TextDecoder();
37
+ let buffer = "";
38
+
39
+ while (true) {
40
+ const { done, value } = await reader.read();
41
+ if (done) break;
42
+
43
+ buffer += decoder.decode(value, { stream: true });
44
+ const lines = buffer.split("\n");
45
+ buffer = lines.pop() || "";
46
+
47
+ for (const line of lines) {
48
+ if (!line.startsWith("data:")) continue;
49
+ const data = line.slice(5).trim();
50
+ if (!data) continue;
51
+ try {
52
+ const parsed = JSON.parse(data) as { type?: string };
53
+ if (parsed.type === "heartbeat") continue;
54
+ } catch {
55
+ // If it's not JSON, pass through.
56
+ }
57
+ process.stdout.write(`${data}\n`);
58
+ }
59
+ }
60
+ }
61
+
62
+ export default async function logs(options: LogsOptions = {}): Promise<void> {
63
+ // Check if this is a managed project (read from .jack/project.json)
64
+ const deployMode = await getDeployMode(process.cwd());
65
+ if (deployMode === "managed") {
66
+ const projectId = await getProjectId(process.cwd());
67
+ if (!projectId) {
68
+ output.error("No .jack/project.json found");
69
+ output.info("Run this from a linked jack cloud project directory");
70
+ process.exit(1);
71
+ }
72
+
73
+ try {
74
+ await streamManagedLogs(projectId, options.label);
75
+ return;
76
+ } catch (err) {
77
+ output.error(err instanceof Error ? err.message : String(err));
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ // BYOC requires a wrangler config in the working directory.
12
83
  const hasWranglerJson = existsSync("wrangler.jsonc") || existsSync("wrangler.json");
13
84
  const hasWranglerToml = existsSync("wrangler.toml");
14
85
 
@@ -18,15 +89,6 @@ export default async function logs(): Promise<void> {
18
89
  process.exit(1);
19
90
  }
20
91
 
21
- // Check if this is a managed project (read from .jack/project.json)
22
- const deployMode = await getDeployMode(process.cwd());
23
- if (deployMode === "managed") {
24
- output.warn("Real-time logs not yet available for managed projects");
25
- output.info("Logs are being collected - web UI coming soon");
26
- output.info("Track progress: https://github.com/getjack-org/jack/issues/2");
27
- return;
28
- }
29
-
30
92
  // BYOC project - use wrangler tail
31
93
  output.info("Streaming logs from Cloudflare Worker...");
32
94
  output.info("Press Ctrl+C to stop\n");
@@ -20,6 +20,10 @@ import {
20
20
  getDatabaseInfo as getWranglerDatabaseInfo,
21
21
  } from "../lib/services/db.ts";
22
22
  import { getRiskDescription } from "../lib/services/sql-classifier.ts";
23
+ import { createStorageBucket } from "../lib/services/storage-create.ts";
24
+ import { deleteStorageBucket } from "../lib/services/storage-delete.ts";
25
+ import { getStorageBucketInfo } from "../lib/services/storage-info.ts";
26
+ import { listStorageBuckets } from "../lib/services/storage-list.ts";
23
27
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
24
28
  import { Events, track } from "../lib/telemetry.ts";
25
29
 
@@ -105,9 +109,11 @@ export default async function services(
105
109
  switch (subcommand) {
106
110
  case "db":
107
111
  return await dbCommand(args, options);
112
+ case "storage":
113
+ return await storageCommand(args, options);
108
114
  default:
109
115
  error(`Unknown service: ${subcommand}`);
110
- info("Available: db");
116
+ info("Available: db, storage");
111
117
  process.exit(1);
112
118
  }
113
119
  }
@@ -118,6 +124,7 @@ function showHelp(): void {
118
124
  console.error("");
119
125
  console.error("Commands:");
120
126
  console.error(" db Manage database");
127
+ console.error(" storage Manage storage (R2 buckets)");
121
128
  console.error("");
122
129
  console.error("Run 'jack services <command>' for more information.");
123
130
  console.error("");
@@ -805,7 +812,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
805
812
  return;
806
813
  }
807
814
 
808
- // NOW execute with confirmation (interactive: false means "already confirmed")
815
+ // NOW execute with confirmation
809
816
  outputSpinner.start("Executing SQL...");
810
817
  if (execArgs.filePath) {
811
818
  result = await executeSqlFile({
@@ -813,7 +820,8 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
813
820
  filePath: execArgs.filePath,
814
821
  databaseName: execArgs.databaseName,
815
822
  allowWrite: true,
816
- interactive: false, // Already confirmed, execute now
823
+ interactive: true,
824
+ confirmed: true, // Already confirmed, skip re-prompting
817
825
  });
818
826
  } else {
819
827
  result = await executeSql({
@@ -821,7 +829,8 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
821
829
  sql: execArgs.sql!,
822
830
  databaseName: execArgs.databaseName,
823
831
  allowWrite: true,
824
- interactive: false, // Already confirmed, execute now
832
+ interactive: true,
833
+ confirmed: true, // Already confirmed, skip re-prompting
825
834
  });
826
835
  }
827
836
  }
@@ -918,3 +927,245 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
918
927
  process.exit(1);
919
928
  }
920
929
  }
930
+
931
+ // ============================================================================
932
+ // Storage (R2) Commands
933
+ // ============================================================================
934
+
935
+ function showStorageHelp(): void {
936
+ console.error("");
937
+ info("jack services storage - Manage storage buckets");
938
+ console.error("");
939
+ console.error("Actions:");
940
+ console.error(" info [name] Show bucket information (default)");
941
+ console.error(" create Create a new storage bucket");
942
+ console.error(" list List all storage buckets in the project");
943
+ console.error(" delete <name> Delete a storage bucket");
944
+ console.error("");
945
+ console.error("Examples:");
946
+ console.error(
947
+ " jack services storage Show info about the default bucket",
948
+ );
949
+ console.error(" jack services storage create Create a new storage bucket");
950
+ console.error(" jack services storage list List all storage buckets");
951
+ console.error(" jack services storage delete my-bucket Delete a bucket");
952
+ console.error("");
953
+ }
954
+
955
+ async function storageCommand(args: string[], options: ServiceOptions): Promise<void> {
956
+ const action = args[0] || "info"; // Default to info
957
+
958
+ switch (action) {
959
+ case "--help":
960
+ case "-h":
961
+ case "help":
962
+ return showStorageHelp();
963
+ case "info":
964
+ return await storageInfo(args.slice(1), options);
965
+ case "create":
966
+ return await storageCreate(args.slice(1), options);
967
+ case "list":
968
+ return await storageList(options);
969
+ case "delete":
970
+ return await storageDelete(args.slice(1), options);
971
+ default:
972
+ error(`Unknown action: ${action}`);
973
+ info("Available: info, create, list, delete");
974
+ process.exit(1);
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Show storage bucket information
980
+ */
981
+ async function storageInfo(args: string[], options: ServiceOptions): Promise<void> {
982
+ const bucketName = parseNameFlag(args);
983
+ const projectDir = process.cwd();
984
+
985
+ outputSpinner.start("Fetching storage info...");
986
+ try {
987
+ const bucketInfo = await getStorageBucketInfo(projectDir, bucketName);
988
+ outputSpinner.stop();
989
+
990
+ if (!bucketInfo) {
991
+ console.error("");
992
+ error(bucketName ? `Bucket "${bucketName}" not found` : "No storage buckets found for this project");
993
+ info("Create one with: jack services storage create");
994
+ console.error("");
995
+ return;
996
+ }
997
+
998
+ console.error("");
999
+ success(`Bucket: ${bucketInfo.name}`);
1000
+ console.error("");
1001
+ item(`Binding: ${bucketInfo.binding}`);
1002
+ item(`Source: ${bucketInfo.source === "control-plane" ? "managed (jack cloud)" : "BYO (wrangler)"}`);
1003
+ console.error("");
1004
+ } catch (err) {
1005
+ outputSpinner.stop();
1006
+ console.error("");
1007
+ error(`Failed to fetch storage info: ${err instanceof Error ? err.message : String(err)}`);
1008
+ console.error("");
1009
+ process.exit(1);
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Create a new storage bucket
1015
+ */
1016
+ async function storageCreate(args: string[], options: ServiceOptions): Promise<void> {
1017
+ // Parse --name flag
1018
+ const name = parseNameFlag(args);
1019
+
1020
+ outputSpinner.start("Creating storage bucket...");
1021
+ try {
1022
+ const result = await createStorageBucket(process.cwd(), {
1023
+ name,
1024
+ interactive: true,
1025
+ });
1026
+ outputSpinner.stop();
1027
+
1028
+ // Track telemetry
1029
+ track(Events.SERVICE_CREATED, {
1030
+ service_type: "r2",
1031
+ binding_name: result.bindingName,
1032
+ created: result.created,
1033
+ });
1034
+
1035
+ console.error("");
1036
+ if (result.created) {
1037
+ success(`Storage bucket created: ${result.bucketName}`);
1038
+ } else {
1039
+ success(`Using existing bucket: ${result.bucketName}`);
1040
+ }
1041
+ console.error("");
1042
+ item(`Binding: ${result.bindingName}`);
1043
+
1044
+ // Auto-deploy to activate the storage binding
1045
+ console.error("");
1046
+ outputSpinner.start("Deploying to activate storage binding...");
1047
+ try {
1048
+ const { deployProject } = await import("../lib/project-operations.ts");
1049
+ await deployProject(process.cwd(), { interactive: true });
1050
+ outputSpinner.stop();
1051
+ console.error("");
1052
+ success("Storage ready");
1053
+ console.error("");
1054
+ } catch (err) {
1055
+ outputSpinner.stop();
1056
+ console.error("");
1057
+ warn("Deploy failed - run 'jack ship' to activate the binding");
1058
+ console.error("");
1059
+ }
1060
+ } catch (err) {
1061
+ outputSpinner.stop();
1062
+ console.error("");
1063
+ error(`Failed to create storage bucket: ${err instanceof Error ? err.message : String(err)}`);
1064
+ process.exit(1);
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * List all storage buckets in the project
1070
+ */
1071
+ async function storageList(options: ServiceOptions): Promise<void> {
1072
+ outputSpinner.start("Fetching storage buckets...");
1073
+ try {
1074
+ const buckets = await listStorageBuckets(process.cwd());
1075
+ outputSpinner.stop();
1076
+
1077
+ if (buckets.length === 0) {
1078
+ console.error("");
1079
+ info("No storage buckets found in this project.");
1080
+ console.error("");
1081
+ info("Create one with: jack services storage create");
1082
+ console.error("");
1083
+ return;
1084
+ }
1085
+
1086
+ console.error("");
1087
+ success(`Found ${buckets.length} storage bucket${buckets.length === 1 ? "" : "s"}:`);
1088
+ console.error("");
1089
+
1090
+ for (const bucket of buckets) {
1091
+ item(`${bucket.name} (${bucket.binding})`);
1092
+ }
1093
+ console.error("");
1094
+ } catch (err) {
1095
+ outputSpinner.stop();
1096
+ console.error("");
1097
+ error(`Failed to list storage buckets: ${err instanceof Error ? err.message : String(err)}`);
1098
+ process.exit(1);
1099
+ }
1100
+ }
1101
+
1102
+ /**
1103
+ * Delete a storage bucket
1104
+ */
1105
+ async function storageDelete(args: string[], options: ServiceOptions): Promise<void> {
1106
+ const bucketName = parseNameFlag(args);
1107
+
1108
+ if (!bucketName) {
1109
+ console.error("");
1110
+ error("Bucket name required");
1111
+ info("Usage: jack services storage delete <bucket-name>");
1112
+ console.error("");
1113
+ process.exit(1);
1114
+ }
1115
+
1116
+ const projectDir = process.cwd();
1117
+
1118
+ // Get bucket info first
1119
+ outputSpinner.start("Fetching bucket info...");
1120
+ const bucketInfo = await getStorageBucketInfo(projectDir, bucketName);
1121
+ outputSpinner.stop();
1122
+
1123
+ if (!bucketInfo) {
1124
+ console.error("");
1125
+ error(`Bucket "${bucketName}" not found in this project`);
1126
+ console.error("");
1127
+ process.exit(1);
1128
+ }
1129
+
1130
+ // Show what will be deleted
1131
+ console.error("");
1132
+ info(`Bucket: ${bucketInfo.name}`);
1133
+ item(`Binding: ${bucketInfo.binding}`);
1134
+ console.error("");
1135
+ warn("This will permanently delete the bucket and all its contents");
1136
+ console.error("");
1137
+
1138
+ // Confirm deletion
1139
+ const { promptSelect } = await import("../lib/hooks.ts");
1140
+ const choice = await promptSelect(["Yes, delete", "No, cancel"], `Delete bucket '${bucketName}'?`);
1141
+
1142
+ if (choice !== 0) {
1143
+ info("Cancelled");
1144
+ return;
1145
+ }
1146
+
1147
+ outputSpinner.start("Deleting bucket...");
1148
+
1149
+ try {
1150
+ const result = await deleteStorageBucket(projectDir, bucketName);
1151
+ outputSpinner.stop();
1152
+
1153
+ // Track telemetry
1154
+ track(Events.SERVICE_DELETED, {
1155
+ service_type: "r2",
1156
+ deleted: result.deleted,
1157
+ });
1158
+
1159
+ console.error("");
1160
+ success("Bucket deleted");
1161
+ if (result.bindingRemoved) {
1162
+ item("Binding removed from wrangler.jsonc");
1163
+ }
1164
+ console.error("");
1165
+ } catch (err) {
1166
+ outputSpinner.stop();
1167
+ console.error("");
1168
+ error(`Failed to delete bucket: ${err instanceof Error ? err.message : String(err)}`);
1169
+ process.exit(1);
1170
+ }
1171
+ }
package/src/index.ts CHANGED
@@ -89,6 +89,9 @@ const cli = meow(
89
89
  as: {
90
90
  type: "string",
91
91
  },
92
+ label: {
93
+ type: "string",
94
+ },
92
95
  dash: {
93
96
  type: "boolean",
94
97
  default: false,
@@ -261,7 +264,7 @@ try {
261
264
  case "logs":
262
265
  case "tail": {
263
266
  const { default: logs } = await import("./commands/logs.ts");
264
- await withTelemetry("logs", logs)();
267
+ await withTelemetry("logs", logs)({ label: cli.flags.label });
265
268
  break;
266
269
  }
267
270
  case "agents": {
@@ -742,3 +742,42 @@ export async function downloadProjectSource(slug: string): Promise<Buffer> {
742
742
 
743
743
  return Buffer.from(await response.arrayBuffer());
744
744
  }
745
+
746
+ export interface LogSessionInfo {
747
+ id: string;
748
+ project_id: string;
749
+ label: string | null;
750
+ status: "active" | "expired" | "revoked" | string;
751
+ expires_at: string;
752
+ }
753
+
754
+ export interface StartLogSessionResponse {
755
+ success: boolean;
756
+ session: LogSessionInfo;
757
+ stream: { url: string; type: "sse" };
758
+ }
759
+
760
+ /**
761
+ * Start or renew a 1-hour log tailing session for a managed (jack cloud) project.
762
+ */
763
+ export async function startLogSession(
764
+ projectId: string,
765
+ label?: string,
766
+ ): Promise<StartLogSessionResponse> {
767
+ const { authFetch } = await import("./auth/index.ts");
768
+
769
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/logs/session`, {
770
+ method: "POST",
771
+ headers: { "Content-Type": "application/json" },
772
+ body: JSON.stringify(label ? { label } : {}),
773
+ });
774
+
775
+ if (!response.ok) {
776
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
777
+ message?: string;
778
+ };
779
+ throw new Error(err.message || `Failed to start log session: ${response.status}`);
780
+ }
781
+
782
+ return response.json() as Promise<StartLogSessionResponse>;
783
+ }
@@ -34,6 +34,8 @@ export interface ExecuteSqlOptions {
34
34
  allowWrite?: boolean;
35
35
  /** Allow interactive confirmation for destructive ops. Default: true */
36
36
  interactive?: boolean;
37
+ /** Skip destructive confirmation (already confirmed by CLI). Default: false */
38
+ confirmed?: boolean;
37
39
  /** For MCP: wrap results with anti-injection header */
38
40
  wrapResults?: boolean;
39
41
  }
@@ -328,6 +330,7 @@ export async function executeSql(options: ExecuteSqlOptions): Promise<ExecuteSql
328
330
  databaseName,
329
331
  allowWrite = false,
330
332
  interactive = true,
333
+ confirmed = false,
331
334
  wrapResults = false,
332
335
  } = options;
333
336
 
@@ -350,7 +353,7 @@ export async function executeSql(options: ExecuteSqlOptions): Promise<ExecuteSql
350
353
  }
351
354
 
352
355
  // Check for destructive operations
353
- if (highestRisk === "destructive") {
356
+ if (highestRisk === "destructive" && !confirmed) {
354
357
  const destructiveStmt = statements.find((s) => s.risk === "destructive");
355
358
  if (destructiveStmt) {
356
359
  if (!interactive) {
@@ -476,7 +479,7 @@ export async function executeSql(options: ExecuteSqlOptions): Promise<ExecuteSql
476
479
  export async function executeSqlFile(
477
480
  options: Omit<ExecuteSqlOptions, "sql"> & { filePath: string },
478
481
  ): Promise<ExecuteSqlResult> {
479
- const { projectDir, filePath, databaseName, allowWrite = false, interactive = true } = options;
482
+ const { projectDir, filePath, databaseName, allowWrite = false, interactive = true, confirmed = false } = options;
480
483
 
481
484
  // Read the file
482
485
  if (!existsSync(filePath)) {
@@ -509,7 +512,7 @@ export async function executeSqlFile(
509
512
  }
510
513
 
511
514
  // Check for destructive operations
512
- if (highestRisk === "destructive") {
515
+ if (highestRisk === "destructive" && !confirmed) {
513
516
  const destructiveStmt = statements.find((s) => s.risk === "destructive");
514
517
  if (destructiveStmt) {
515
518
  if (!interactive) {