speqs 0.5.1 → 0.7.0

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 (40) hide show
  1. package/dist/commands/config.js +13 -4
  2. package/dist/commands/iteration.js +64 -13
  3. package/dist/commands/simulation.d.ts +1 -1
  4. package/dist/commands/simulation.js +454 -121
  5. package/dist/commands/study.js +140 -27
  6. package/dist/commands/tester-profile.js +17 -4
  7. package/dist/commands/tester.js +12 -3
  8. package/dist/commands/workspace.js +51 -6
  9. package/dist/config.d.ts +2 -0
  10. package/dist/index.js +3 -1
  11. package/dist/lib/alias-store.d.ts +22 -3
  12. package/dist/lib/alias-store.js +60 -12
  13. package/dist/lib/api-client.d.ts +31 -0
  14. package/dist/lib/api-client.js +83 -27
  15. package/dist/lib/auth.js +4 -1
  16. package/dist/lib/command-helpers.d.ts +4 -0
  17. package/dist/lib/command-helpers.js +41 -4
  18. package/dist/lib/local-sim/actions.d.ts +22 -0
  19. package/dist/lib/local-sim/actions.js +379 -0
  20. package/dist/lib/local-sim/browser.d.ts +63 -0
  21. package/dist/lib/local-sim/browser.js +332 -0
  22. package/dist/lib/local-sim/debug-report.d.ts +21 -0
  23. package/dist/lib/local-sim/debug-report.js +186 -0
  24. package/dist/lib/local-sim/debug.d.ts +44 -0
  25. package/dist/lib/local-sim/debug.js +103 -0
  26. package/dist/lib/local-sim/install.d.ts +25 -0
  27. package/dist/lib/local-sim/install.js +72 -0
  28. package/dist/lib/local-sim/loop.d.ts +60 -0
  29. package/dist/lib/local-sim/loop.js +526 -0
  30. package/dist/lib/local-sim/types.d.ts +232 -0
  31. package/dist/lib/local-sim/types.js +8 -0
  32. package/dist/lib/local-sim/upload.d.ts +6 -0
  33. package/dist/lib/local-sim/upload.js +24 -0
  34. package/dist/lib/output.d.ts +16 -1
  35. package/dist/lib/output.js +250 -61
  36. package/dist/lib/types.d.ts +7 -30
  37. package/dist/lib/types.js +9 -1
  38. package/dist/lib/upload.d.ts +47 -0
  39. package/dist/lib/upload.js +178 -0
  40. package/package.json +3 -2
@@ -0,0 +1,178 @@
1
+ /**
2
+ * File upload utilities for media modalities.
3
+ *
4
+ * Implements the same backend-mediated upload flow as the frontend:
5
+ * 1. POST /studies/{id}/content/upload → signed URL + content_url
6
+ * 2. PUT signed_upload_url → upload raw bytes
7
+ * 3. PUT /studies/{id}/content/upload/complete → mark done
8
+ *
9
+ * Detects local file paths vs URLs automatically and handles
10
+ * MIME type detection, file validation, and progress reporting.
11
+ */
12
+ import { readFile } from "node:fs/promises";
13
+ import { access, stat, constants } from "node:fs/promises";
14
+ import { readFileSync } from "node:fs";
15
+ import { extname, basename, resolve as resolvePath } from "node:path";
16
+ // ---------------------------------------------------------------------------
17
+ // MIME type detection (inline map — zero dependencies)
18
+ // ---------------------------------------------------------------------------
19
+ const MIME_MAP = {
20
+ // Video
21
+ ".mp4": "video/mp4",
22
+ ".webm": "video/webm",
23
+ ".mov": "video/quicktime",
24
+ ".avi": "video/x-msvideo",
25
+ // Audio
26
+ ".mp3": "audio/mpeg",
27
+ ".wav": "audio/wav",
28
+ ".ogg": "audio/ogg",
29
+ ".m4a": "audio/mp4",
30
+ ".flac": "audio/flac",
31
+ // Image
32
+ ".png": "image/png",
33
+ ".jpg": "image/jpeg",
34
+ ".jpeg": "image/jpeg",
35
+ ".gif": "image/gif",
36
+ ".webp": "image/webp",
37
+ ".svg": "image/svg+xml",
38
+ ".bmp": "image/bmp",
39
+ // Document
40
+ ".pdf": "application/pdf",
41
+ ".doc": "application/msword",
42
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
43
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
44
+ ".ppt": "application/vnd.ms-powerpoint",
45
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
46
+ // Text
47
+ ".txt": "text/plain",
48
+ ".html": "text/html",
49
+ ".htm": "text/html",
50
+ ".csv": "text/csv",
51
+ ".json": "application/json",
52
+ ".md": "text/markdown",
53
+ };
54
+ export function detectMimeType(filePath) {
55
+ const ext = extname(filePath).toLowerCase();
56
+ return MIME_MAP[ext] || "application/octet-stream";
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Path detection
60
+ // ---------------------------------------------------------------------------
61
+ export function isLocalPath(value) {
62
+ // URLs have a scheme followed by "://"; anything else is a local path
63
+ if (value.startsWith("http://") || value.startsWith("https://"))
64
+ return false;
65
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value)) {
66
+ throw new Error(`Unsupported URL scheme: ${value.split("://")[0]}. Use http(s):// URLs or local file paths.`);
67
+ }
68
+ return true;
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // File validation
72
+ // ---------------------------------------------------------------------------
73
+ const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB
74
+ export async function validateFile(filePath) {
75
+ const resolved = resolvePath(filePath);
76
+ try {
77
+ await access(resolved, constants.R_OK);
78
+ }
79
+ catch {
80
+ throw new Error(`File not found or not readable: ${filePath}`);
81
+ }
82
+ const info = await stat(resolved);
83
+ if (!info.isFile()) {
84
+ throw new Error(`Not a regular file: ${filePath}`);
85
+ }
86
+ if (info.size === 0) {
87
+ throw new Error(`File is empty: ${filePath}`);
88
+ }
89
+ if (info.size > MAX_FILE_SIZE) {
90
+ const sizeMB = (info.size / (1024 * 1024)).toFixed(1);
91
+ throw new Error(`File too large (${sizeMB} MB). Maximum is ${MAX_FILE_SIZE / (1024 * 1024)} MB.`);
92
+ }
93
+ return { size: info.size, mime: detectMimeType(filePath) };
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Core upload: 3-step backend-mediated flow
97
+ // ---------------------------------------------------------------------------
98
+ /**
99
+ * Upload a local file to Supabase Storage via the backend's signed URL flow.
100
+ * Returns the public content_url for use in iteration details.
101
+ */
102
+ export async function uploadStudyContent(client, studyId, filePath, opts) {
103
+ const resolved = resolvePath(filePath);
104
+ const { size, mime: detectedMime } = await validateFile(filePath);
105
+ const mime = opts?.mimeTypeOverride || detectedMime;
106
+ const name = basename(filePath);
107
+ const sizeMB = (size / (1024 * 1024)).toFixed(1);
108
+ const log = (msg) => { if (!opts?.quiet)
109
+ process.stderr.write(msg); };
110
+ // Step 1: Request a signed upload URL from the backend
111
+ log(`Uploading ${name} (${sizeMB} MB)...`);
112
+ const uploadResp = await client.post(`/studies/${studyId}/content/upload`, { content_type: mime, file_size_bytes: size });
113
+ // Step 2: PUT the raw file bytes to the signed URL
114
+ const fileBuffer = await readFile(resolved);
115
+ const putResp = await fetch(uploadResp.upload_info.signed_upload_url, {
116
+ method: "PUT",
117
+ headers: {
118
+ "Content-Type": mime,
119
+ "Content-Length": String(fileBuffer.byteLength),
120
+ },
121
+ body: fileBuffer,
122
+ signal: AbortSignal.timeout(300_000), // 5 min timeout for large files
123
+ });
124
+ if (!putResp.ok) {
125
+ const body = await putResp.text().catch(() => "");
126
+ throw new Error(`Upload failed (HTTP ${putResp.status}): ${body}`);
127
+ }
128
+ // Step 3: Mark the upload as complete
129
+ await client.put(`/studies/${studyId}/content/upload/complete`, {
130
+ file_path: uploadResp.upload_info.file_path,
131
+ is_uploaded: true,
132
+ });
133
+ log(" done.\n");
134
+ return uploadResp.content_url;
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // High-level resolvers (URL passthrough or upload)
138
+ // ---------------------------------------------------------------------------
139
+ /**
140
+ * If the value is a URL, return it as-is. If it's a local file path,
141
+ * upload it and return the resulting content_url.
142
+ */
143
+ export async function resolveContentUrl(client, studyId, value, opts) {
144
+ if (!isLocalPath(value))
145
+ return value;
146
+ return uploadStudyContent(client, studyId, value, opts);
147
+ }
148
+ /**
149
+ * Resolve a comma-separated list of URLs or file paths.
150
+ * Each value is independently resolved (some may be URLs, some files).
151
+ */
152
+ export async function resolveContentUrls(client, studyId, commaSeparated, opts) {
153
+ const values = commaSeparated.split(",").map((s) => s.trim()).filter(Boolean);
154
+ const results = [];
155
+ for (const v of values) {
156
+ results.push(await resolveContentUrl(client, studyId, v, opts));
157
+ }
158
+ return results;
159
+ }
160
+ /**
161
+ * Resolve text content. If the value starts with '@', read the file at
162
+ * the path that follows (curl-style convention). Otherwise return as-is.
163
+ */
164
+ export function resolveTextContent(value) {
165
+ if (!value.startsWith("@"))
166
+ return value;
167
+ const filePath = value.slice(1);
168
+ if (!filePath) {
169
+ throw new Error("Missing file path after @. Usage: --content-text @./file.txt");
170
+ }
171
+ const resolved = resolvePath(filePath);
172
+ try {
173
+ return readFileSync(resolved, "utf-8");
174
+ }
175
+ catch {
176
+ throw new Error(`Cannot read text file: ${filePath}`);
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speqs",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "The command-line interface for Speqs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "email": "support@speqs.io"
34
34
  },
35
35
  "dependencies": {
36
- "commander": "^13.0.0"
36
+ "commander": "^13.0.0",
37
+ "playwright-core": "^1.58.2"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/node": "^22.0.0",