@ishlabs/cli 0.8.1
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/LICENSE +6 -0
- package/README.md +69 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +102 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/iteration.d.ts +5 -0
- package/dist/commands/iteration.js +134 -0
- package/dist/commands/simulation.d.ts +10 -0
- package/dist/commands/simulation.js +647 -0
- package/dist/commands/study.d.ts +5 -0
- package/dist/commands/study.js +283 -0
- package/dist/commands/tester-profile.d.ts +5 -0
- package/dist/commands/tester-profile.js +109 -0
- package/dist/commands/tester.d.ts +5 -0
- package/dist/commands/tester.js +73 -0
- package/dist/commands/workspace.d.ts +5 -0
- package/dist/commands/workspace.js +133 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +25 -0
- package/dist/connect.d.ts +4 -0
- package/dist/connect.js +573 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/lib/alias-store.d.ts +49 -0
- package/dist/lib/alias-store.js +138 -0
- package/dist/lib/api-client.d.ts +58 -0
- package/dist/lib/api-client.js +177 -0
- package/dist/lib/auth.d.ts +8 -0
- package/dist/lib/auth.js +73 -0
- package/dist/lib/command-helpers.d.ts +28 -0
- package/dist/lib/command-helpers.js +131 -0
- package/dist/lib/local-sim/actions.d.ts +22 -0
- package/dist/lib/local-sim/actions.js +379 -0
- package/dist/lib/local-sim/browser.d.ts +63 -0
- package/dist/lib/local-sim/browser.js +332 -0
- package/dist/lib/local-sim/debug-report.d.ts +21 -0
- package/dist/lib/local-sim/debug-report.js +186 -0
- package/dist/lib/local-sim/debug.d.ts +44 -0
- package/dist/lib/local-sim/debug.js +103 -0
- package/dist/lib/local-sim/install.d.ts +25 -0
- package/dist/lib/local-sim/install.js +72 -0
- package/dist/lib/local-sim/loop.d.ts +60 -0
- package/dist/lib/local-sim/loop.js +526 -0
- package/dist/lib/local-sim/types.d.ts +232 -0
- package/dist/lib/local-sim/types.js +8 -0
- package/dist/lib/local-sim/upload.d.ts +6 -0
- package/dist/lib/local-sim/upload.js +24 -0
- package/dist/lib/output.d.ts +34 -0
- package/dist/lib/output.js +675 -0
- package/dist/lib/types.d.ts +179 -0
- package/dist/lib/types.js +12 -0
- package/dist/lib/upload.d.ts +47 -0
- package/dist/lib/upload.js +178 -0
- package/dist/upgrade.d.ts +1 -0
- package/dist/upgrade.js +94 -0
- package/package.json +43 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for Ish API entities.
|
|
3
|
+
*/
|
|
4
|
+
export interface Product {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
base_url?: string;
|
|
10
|
+
product_type?: string;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ProductCreateInput {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
base_url?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProductUpdateInput {
|
|
21
|
+
name?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
color?: string;
|
|
24
|
+
base_url?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Assignment {
|
|
27
|
+
id?: string;
|
|
28
|
+
name: string;
|
|
29
|
+
instructions: string;
|
|
30
|
+
}
|
|
31
|
+
export interface InterviewQuestion {
|
|
32
|
+
id?: string;
|
|
33
|
+
question: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
timing?: string;
|
|
36
|
+
min?: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
step?: number;
|
|
39
|
+
labels?: string[];
|
|
40
|
+
options?: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface Study {
|
|
43
|
+
id: string;
|
|
44
|
+
product_id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
modality?: string;
|
|
48
|
+
content_type?: string;
|
|
49
|
+
status?: string;
|
|
50
|
+
start_frame_id?: string;
|
|
51
|
+
source_study_id?: string;
|
|
52
|
+
assignments?: Assignment[];
|
|
53
|
+
interview_questions?: InterviewQuestion[];
|
|
54
|
+
frames?: unknown[];
|
|
55
|
+
iterations?: Iteration[];
|
|
56
|
+
created_at: string;
|
|
57
|
+
updated_at: string;
|
|
58
|
+
created_by?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface StudyCreateInput {
|
|
61
|
+
product_id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
modality?: string;
|
|
65
|
+
content_type?: string;
|
|
66
|
+
assignments?: Assignment[];
|
|
67
|
+
interview_questions?: InterviewQuestion[];
|
|
68
|
+
start_frame_id?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface StudyUpdateInput {
|
|
71
|
+
name?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
modality?: string;
|
|
75
|
+
content_type?: string;
|
|
76
|
+
assignments?: Assignment[];
|
|
77
|
+
interview_questions?: InterviewQuestion[];
|
|
78
|
+
start_frame_id?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface StudyGenerateInput {
|
|
81
|
+
problem_description: string;
|
|
82
|
+
target_url?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface Iteration {
|
|
85
|
+
id: string;
|
|
86
|
+
study_id: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
label?: string;
|
|
90
|
+
details?: Record<string, unknown>;
|
|
91
|
+
testers?: Tester[];
|
|
92
|
+
created_at: string;
|
|
93
|
+
updated_at: string;
|
|
94
|
+
}
|
|
95
|
+
export interface IterationCreateInput {
|
|
96
|
+
name: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
details?: Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
export interface IterationUpdateInput {
|
|
101
|
+
name?: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
details?: Record<string, unknown>;
|
|
104
|
+
label?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface TesterProfile {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
}
|
|
111
|
+
export interface Tester {
|
|
112
|
+
id: string;
|
|
113
|
+
iteration_id: string;
|
|
114
|
+
tester_profile_id: string;
|
|
115
|
+
instance_name?: string;
|
|
116
|
+
instance_number?: number;
|
|
117
|
+
status: string;
|
|
118
|
+
language?: string;
|
|
119
|
+
platform?: string;
|
|
120
|
+
tester_type?: string;
|
|
121
|
+
viewport_width?: number;
|
|
122
|
+
viewport_height?: number;
|
|
123
|
+
created_at: string;
|
|
124
|
+
}
|
|
125
|
+
export interface TesterCreateInput {
|
|
126
|
+
tester_profile_id: string;
|
|
127
|
+
instance_name?: string;
|
|
128
|
+
status?: string;
|
|
129
|
+
language?: string;
|
|
130
|
+
platform?: string;
|
|
131
|
+
tester_type?: string;
|
|
132
|
+
}
|
|
133
|
+
export interface SimulationStartResponse {
|
|
134
|
+
tester_id: string;
|
|
135
|
+
study_id: string;
|
|
136
|
+
job_id: string | null;
|
|
137
|
+
message: string;
|
|
138
|
+
}
|
|
139
|
+
export interface SimulationBatchStartResponse {
|
|
140
|
+
results: SimulationStartResponse[];
|
|
141
|
+
}
|
|
142
|
+
export interface MediaSimulationStartInput {
|
|
143
|
+
product_id: string;
|
|
144
|
+
study_id: string;
|
|
145
|
+
tester_id: string;
|
|
146
|
+
config_id: string;
|
|
147
|
+
max_interactions?: number;
|
|
148
|
+
language?: string;
|
|
149
|
+
config_overrides?: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
export interface MediaBatchStartResponse {
|
|
152
|
+
results: SimulationStartResponse[];
|
|
153
|
+
}
|
|
154
|
+
export declare const MEDIA_MODALITIES: readonly ["video", "audio", "text", "image", "document"];
|
|
155
|
+
export type MediaModality = typeof MEDIA_MODALITIES[number];
|
|
156
|
+
export declare const VALID_CONTENT_TYPES: Record<string, string[]>;
|
|
157
|
+
export interface SimulationStatus {
|
|
158
|
+
job_id: string;
|
|
159
|
+
status: string | null;
|
|
160
|
+
create_time?: string;
|
|
161
|
+
completion_time?: string;
|
|
162
|
+
error?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface SimulationCancelResponse {
|
|
165
|
+
job_id: string;
|
|
166
|
+
success: boolean;
|
|
167
|
+
message: string;
|
|
168
|
+
}
|
|
169
|
+
export interface SimulationConfig {
|
|
170
|
+
id: string;
|
|
171
|
+
name: string;
|
|
172
|
+
model_settings?: Record<string, unknown>;
|
|
173
|
+
simulation_settings?: Record<string, unknown>;
|
|
174
|
+
prompts?: Record<string, unknown>;
|
|
175
|
+
outputs?: Record<string, unknown>;
|
|
176
|
+
source_type?: string;
|
|
177
|
+
created_at: string;
|
|
178
|
+
updated_at: string;
|
|
179
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for Ish API entities.
|
|
3
|
+
*/
|
|
4
|
+
// --- Modality constants ---
|
|
5
|
+
export const MEDIA_MODALITIES = ["video", "audio", "text", "image", "document"];
|
|
6
|
+
export const VALID_CONTENT_TYPES = {
|
|
7
|
+
text: ["narrative", "informational", "commercial", "editorial", "reference", "email", "news"],
|
|
8
|
+
video: ["tutorial", "documentary", "entertainment", "review", "lifestyle", "news", "social_post", "ad"],
|
|
9
|
+
audio: ["music", "narration", "conversation", "speech", "soundscape", "news", "ad"],
|
|
10
|
+
image: ["product", "photography", "infographic", "artwork", "interface", "social_post", "ad"],
|
|
11
|
+
document: ["deck", "presentation", "report", "brochure", "guide"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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 type { ApiClient } from "./api-client.js";
|
|
13
|
+
export declare function detectMimeType(filePath: string): string;
|
|
14
|
+
export declare function isLocalPath(value: string): boolean;
|
|
15
|
+
export declare function validateFile(filePath: string): Promise<{
|
|
16
|
+
size: number;
|
|
17
|
+
mime: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Upload a local file to Supabase Storage via the backend's signed URL flow.
|
|
21
|
+
* Returns the public content_url for use in iteration details.
|
|
22
|
+
*/
|
|
23
|
+
export declare function uploadStudyContent(client: ApiClient, studyId: string, filePath: string, opts?: {
|
|
24
|
+
mimeTypeOverride?: string;
|
|
25
|
+
quiet?: boolean;
|
|
26
|
+
}): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* If the value is a URL, return it as-is. If it's a local file path,
|
|
29
|
+
* upload it and return the resulting content_url.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveContentUrl(client: ApiClient, studyId: string, value: string, opts?: {
|
|
32
|
+
mimeTypeOverride?: string;
|
|
33
|
+
quiet?: boolean;
|
|
34
|
+
}): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a comma-separated list of URLs or file paths.
|
|
37
|
+
* Each value is independently resolved (some may be URLs, some files).
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveContentUrls(client: ApiClient, studyId: string, commaSeparated: string, opts?: {
|
|
40
|
+
mimeTypeOverride?: string;
|
|
41
|
+
quiet?: boolean;
|
|
42
|
+
}): Promise<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve text content. If the value starts with '@', read the file at
|
|
45
|
+
* the path that follows (curl-style convention). Otherwise return as-is.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveTextContent(value: string): string;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function upgrade(currentVersion: string, targetVersion?: string): Promise<void>;
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createWriteStream, renameSync, unlinkSync, chmodSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
const BASE_URL = "https://ishlabs.io";
|
|
6
|
+
function getPlatformTarget() {
|
|
7
|
+
const platform = process.platform;
|
|
8
|
+
const arch = process.arch;
|
|
9
|
+
const targets = {
|
|
10
|
+
darwin: { arm64: "darwin-arm64", x64: "darwin-x64" },
|
|
11
|
+
linux: { arm64: "linux-arm64", x64: "linux-x64" },
|
|
12
|
+
win32: { x64: "windows-x64" },
|
|
13
|
+
};
|
|
14
|
+
const target = targets[platform]?.[arch];
|
|
15
|
+
if (!target) {
|
|
16
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
17
|
+
}
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
async function getLatestVersion() {
|
|
21
|
+
const res = await fetch(`${BASE_URL}/api/releases/latest`);
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new Error(`Failed to fetch latest version: ${res.statusText}`);
|
|
24
|
+
const data = (await res.json());
|
|
25
|
+
return data.version;
|
|
26
|
+
}
|
|
27
|
+
export async function upgrade(currentVersion, targetVersion) {
|
|
28
|
+
if (targetVersion && !/^\d+\.\d+\.\d+/.test(targetVersion)) {
|
|
29
|
+
throw new Error(`Invalid version format: ${targetVersion}`);
|
|
30
|
+
}
|
|
31
|
+
const latest = targetVersion || (await getLatestVersion());
|
|
32
|
+
if (latest === currentVersion) {
|
|
33
|
+
console.log(`Already up to date (v${currentVersion}).`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(`Updating ish v${currentVersion} → v${latest}...`);
|
|
37
|
+
const target = getPlatformTarget();
|
|
38
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
39
|
+
const assetName = `ish-${target}${ext}`;
|
|
40
|
+
const url = `${BASE_URL}/api/releases/v${latest}/${assetName}`;
|
|
41
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`Download failed: ${res.statusText} (${url})`);
|
|
44
|
+
}
|
|
45
|
+
if (!res.body) {
|
|
46
|
+
throw new Error(`Download failed: empty response body (${url})`);
|
|
47
|
+
}
|
|
48
|
+
const execPath = process.execPath;
|
|
49
|
+
// Use same directory as the binary to avoid cross-device rename issues
|
|
50
|
+
const tmpPath = join(dirname(execPath), `.ish-upgrade-${Date.now()}${ext}`);
|
|
51
|
+
const fileStream = createWriteStream(tmpPath);
|
|
52
|
+
try {
|
|
53
|
+
await pipeline(Readable.fromWeb(res.body), fileStream);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(tmpPath);
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
if (process.platform === "win32") {
|
|
63
|
+
const oldPath = execPath + ".old";
|
|
64
|
+
try {
|
|
65
|
+
unlinkSync(oldPath);
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
renameSync(execPath, oldPath);
|
|
69
|
+
try {
|
|
70
|
+
renameSync(tmpPath, execPath);
|
|
71
|
+
try {
|
|
72
|
+
unlinkSync(oldPath);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// Restore original binary on failure
|
|
78
|
+
try {
|
|
79
|
+
renameSync(oldPath, execPath);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(tmpPath);
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
chmodSync(tmpPath, 0o755);
|
|
91
|
+
renameSync(tmpPath, execPath);
|
|
92
|
+
}
|
|
93
|
+
console.log(`Updated to v${latest}.`);
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ishlabs/cli",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "The command-line interface for Ish",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ish": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"build:binary": "bun build --compile --external playwright-core --external chromium-bidi --external electron src/index.ts --outfile ish",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ish",
|
|
25
|
+
"connect",
|
|
26
|
+
"localhost",
|
|
27
|
+
"testing"
|
|
28
|
+
],
|
|
29
|
+
"author": "Ish Labs",
|
|
30
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
31
|
+
"homepage": "https://ishlabs.io",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"email": "support@ishlabs.io"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^13.0.0",
|
|
37
|
+
"playwright-core": "^1.58.2"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"typescript": "^5.7.0"
|
|
42
|
+
}
|
|
43
|
+
}
|