@getjack/jack 0.1.26 → 0.1.28
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 +1 -1
- package/src/commands/logs.ts +74 -12
- package/src/commands/services.ts +255 -4
- package/src/index.ts +4 -1
- package/src/lib/control-plane.ts +39 -0
- package/src/lib/hooks.ts +4 -0
- package/src/lib/services/db-execute.ts +6 -3
- package/src/lib/services/storage-config.ts +669 -0
- package/src/lib/services/storage-create.ts +152 -0
- package/src/lib/services/storage-delete.ts +89 -0
- package/src/lib/services/storage-info.ts +105 -0
- package/src/lib/services/storage-list.ts +42 -0
- package/src/mcp/resources/index.ts +1 -1
- package/src/mcp/tools/index.ts +480 -0
package/package.json
CHANGED
package/src/commands/logs.ts
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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");
|
package/src/commands/services.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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": {
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/hooks.ts
CHANGED
|
@@ -11,6 +11,10 @@ import { restoreTty } from "./tty";
|
|
|
11
11
|
* User pastes JSON, then presses Enter on empty line to submit
|
|
12
12
|
*/
|
|
13
13
|
async function readMultilineJson(prompt: string): Promise<string> {
|
|
14
|
+
// Ensure TTY is in a clean state before starting readline
|
|
15
|
+
// This prevents conflicts with previous @clack/prompts selections
|
|
16
|
+
restoreTty();
|
|
17
|
+
|
|
14
18
|
console.error(prompt);
|
|
15
19
|
console.error("(Paste JSON, then press Enter on empty line to submit)\n");
|
|
16
20
|
|
|
@@ -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) {
|