@blinkdotnew/cli 0.1.3 → 0.1.5
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/dist/cli.js +42 -15
- package/package.json +1 -1
- package/src/commands/ai.ts +32 -4
- package/src/commands/deploy.ts +1 -1
- package/src/commands/notify.ts +13 -10
package/dist/cli.js
CHANGED
|
@@ -199,6 +199,22 @@ async function withSpinner(label, fn) {
|
|
|
199
199
|
|
|
200
200
|
// src/commands/ai.ts
|
|
201
201
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
202
|
+
function extractImageUrls(result) {
|
|
203
|
+
const r = result;
|
|
204
|
+
const data = r?.result?.data;
|
|
205
|
+
if (Array.isArray(data)) return data.map((d) => d.url).filter(Boolean);
|
|
206
|
+
if (Array.isArray(r?.urls)) return r.urls;
|
|
207
|
+
if (r?.url) return [r.url];
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
function extractVideoUrl(result) {
|
|
211
|
+
const r = result;
|
|
212
|
+
const inner = r?.result;
|
|
213
|
+
if (inner?.url) return inner.url;
|
|
214
|
+
if (inner?.video_url) return inner.video_url;
|
|
215
|
+
if (inner?.video?.url) return inner.video.url;
|
|
216
|
+
return r?.url ?? r?.video_url ?? "";
|
|
217
|
+
}
|
|
202
218
|
function registerAiCommands(program2) {
|
|
203
219
|
const ai = program2.command("ai").description("AI generation \u2014 image, video, text, speech, transcription").addHelpText("after", `
|
|
204
220
|
Commands:
|
|
@@ -270,7 +286,11 @@ To edit an existing image, use: blink ai image-edit
|
|
|
270
286
|
})
|
|
271
287
|
);
|
|
272
288
|
if (isJsonMode()) return printJson(result);
|
|
273
|
-
const urls =
|
|
289
|
+
const urls = extractImageUrls(result);
|
|
290
|
+
if (!urls.length) {
|
|
291
|
+
console.log("No image URL in response");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
274
294
|
for (const url of urls) {
|
|
275
295
|
if (opts.output && urls.length === 1) {
|
|
276
296
|
const res = await fetch(url);
|
|
@@ -296,7 +316,11 @@ Examples:
|
|
|
296
316
|
})
|
|
297
317
|
);
|
|
298
318
|
if (isJsonMode()) return printJson(result);
|
|
299
|
-
const url = result
|
|
319
|
+
const url = extractImageUrls(result)[0];
|
|
320
|
+
if (!url) {
|
|
321
|
+
console.log("No image URL in response");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
300
324
|
if (opts.output) {
|
|
301
325
|
const res = await fetch(url);
|
|
302
326
|
writeFileSync2(opts.output, Buffer.from(await res.arrayBuffer()));
|
|
@@ -323,7 +347,7 @@ To animate an existing image, use: blink ai animate
|
|
|
323
347
|
})
|
|
324
348
|
);
|
|
325
349
|
if (isJsonMode()) return printJson(result);
|
|
326
|
-
printUrl("Video", result
|
|
350
|
+
printUrl("Video", extractVideoUrl(result));
|
|
327
351
|
});
|
|
328
352
|
ai.command("animate <prompt> <image-source>").description("Animate an image into video (accepts URL or local file path)").option("--model <model>", "Model (fal-ai/veo3.1/fast/image-to-video | fal-ai/sora-2/image-to-video/pro)", "fal-ai/veo3.1/fast/image-to-video").option("--duration <dur>", "Duration (e.g. 5s, 10s)", "5s").addHelpText("after", `
|
|
329
353
|
Examples:
|
|
@@ -355,7 +379,7 @@ Local files are automatically uploaded before animating.
|
|
|
355
379
|
})
|
|
356
380
|
);
|
|
357
381
|
if (isJsonMode()) return printJson(result);
|
|
358
|
-
printUrl("Video", result
|
|
382
|
+
printUrl("Video", extractVideoUrl(result));
|
|
359
383
|
});
|
|
360
384
|
ai.command("speech <text>").description("Convert text to speech and save as MP3").option("--voice <voice>", "Voice: alloy | echo | fable | onyx | nova | shimmer", "alloy").option("--model <model>", "TTS model", "tts-1").option("--output <file>", "Output file path", "speech.mp3").addHelpText("after", `
|
|
361
385
|
Examples:
|
|
@@ -845,29 +869,32 @@ Examples:
|
|
|
845
869
|
$ blink notify email user@example.com "Newsletter" --file ./email.html
|
|
846
870
|
$ blink notify email proj_xxx user@example.com "Subject" "Body"
|
|
847
871
|
`);
|
|
848
|
-
notify.command("email
|
|
872
|
+
notify.command("email <arg1> [arg2] [arg3] [arg4]").description("Send an email via your project's notification settings").option("--file <path>", "Read email body from an HTML file instead of inline body").addHelpText("after", `
|
|
849
873
|
Examples:
|
|
850
874
|
$ blink notify email user@example.com "Welcome!" "Thanks for signing up."
|
|
851
875
|
$ blink notify email user@example.com "Your report is ready" --file ./report.html
|
|
852
876
|
$ blink notify email proj_xxx user@example.com "Order confirmed" "Your order #42 is ready."
|
|
853
877
|
|
|
854
878
|
The email is sent from your project's configured sender address (set in blink.new project settings).
|
|
855
|
-
`).action(async (
|
|
879
|
+
`).action(async (arg1, arg2, arg3, arg4, opts) => {
|
|
856
880
|
requireToken();
|
|
857
881
|
let projectId;
|
|
858
882
|
let to;
|
|
859
883
|
let subject;
|
|
860
884
|
let body;
|
|
861
|
-
if (
|
|
862
|
-
projectId = requireProjectId(
|
|
863
|
-
to =
|
|
864
|
-
subject =
|
|
865
|
-
body =
|
|
885
|
+
if (arg1.startsWith("proj_") && arg2) {
|
|
886
|
+
projectId = requireProjectId(arg1);
|
|
887
|
+
to = arg2;
|
|
888
|
+
subject = arg3 ?? "";
|
|
889
|
+
body = arg4 ?? "";
|
|
890
|
+
} else if (arg1.startsWith("proj_")) {
|
|
891
|
+
process.stderr.write("error: missing required argument 'to'\n");
|
|
892
|
+
process.exit(1);
|
|
866
893
|
} else {
|
|
867
894
|
projectId = requireProjectId();
|
|
868
|
-
to =
|
|
869
|
-
subject =
|
|
870
|
-
body =
|
|
895
|
+
to = arg1;
|
|
896
|
+
subject = arg2 ?? "";
|
|
897
|
+
body = arg3 ?? arg4 ?? "";
|
|
871
898
|
}
|
|
872
899
|
if (opts.file) body = readFileSync7(opts.file, "utf-8");
|
|
873
900
|
await withSpinner(
|
|
@@ -1087,7 +1114,7 @@ Project resolution:
|
|
|
1087
1114
|
program2.command("rollback [project] [deployment_id]").description("Rollback production to a previous deployment (use blink deployments to find IDs)").action(async (projectArg, deploymentIdArg) => {
|
|
1088
1115
|
requireToken();
|
|
1089
1116
|
const projectId = requireProjectId(projectArg?.startsWith("proj_") ? projectArg : void 0);
|
|
1090
|
-
const deploymentId = deploymentIdArg ?? projectArg;
|
|
1117
|
+
const deploymentId = deploymentIdArg ?? (projectArg?.startsWith("proj_") ? void 0 : projectArg);
|
|
1091
1118
|
const result = await withSpinner(
|
|
1092
1119
|
"Rolling back...",
|
|
1093
1120
|
() => appRequest(`/api/project/${projectId}/rollback`, { body: { deployment_id: deploymentId } })
|
package/package.json
CHANGED
package/src/commands/ai.ts
CHANGED
|
@@ -4,6 +4,32 @@ import { requireToken } from '../lib/auth.js'
|
|
|
4
4
|
import { printJson, printUrl, isJsonMode, withSpinner } from '../lib/output.js'
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
6
6
|
|
|
7
|
+
// Response shape from blink-apis /api/v1/ai/* is:
|
|
8
|
+
// { result: { data: [{url}] } } for images
|
|
9
|
+
// { result: { url | video_url } } for video
|
|
10
|
+
// Helpers to extract the URL regardless of nesting
|
|
11
|
+
function extractImageUrls(result: unknown): string[] {
|
|
12
|
+
const r = result as Record<string, unknown>
|
|
13
|
+
// nested: result.result.data[].url
|
|
14
|
+
const data = (r?.result as Record<string, unknown>)?.data
|
|
15
|
+
if (Array.isArray(data)) return data.map((d: Record<string, unknown>) => d.url as string).filter(Boolean)
|
|
16
|
+
// flat: result.urls[] or result.url
|
|
17
|
+
if (Array.isArray(r?.urls)) return r.urls as string[]
|
|
18
|
+
if (r?.url) return [r.url as string]
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractVideoUrl(result: unknown): string {
|
|
23
|
+
const r = result as Record<string, unknown>
|
|
24
|
+
// nested: result.result.url or result.result.video_url or result.result.video.url
|
|
25
|
+
const inner = r?.result as Record<string, unknown>
|
|
26
|
+
if (inner?.url) return inner.url as string
|
|
27
|
+
if (inner?.video_url) return inner.video_url as string
|
|
28
|
+
if ((inner?.video as Record<string, unknown>)?.url) return (inner.video as Record<string, unknown>).url as string
|
|
29
|
+
// flat fallback
|
|
30
|
+
return (r?.url ?? r?.video_url ?? '') as string
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export function registerAiCommands(program: Command) {
|
|
8
34
|
const ai = program.command('ai')
|
|
9
35
|
.description('AI generation — image, video, text, speech, transcription')
|
|
@@ -88,7 +114,8 @@ To edit an existing image, use: blink ai image-edit
|
|
|
88
114
|
})
|
|
89
115
|
)
|
|
90
116
|
if (isJsonMode()) return printJson(result)
|
|
91
|
-
const urls
|
|
117
|
+
const urls = extractImageUrls(result)
|
|
118
|
+
if (!urls.length) { console.log('No image URL in response'); return }
|
|
92
119
|
for (const url of urls) {
|
|
93
120
|
if (opts.output && urls.length === 1) {
|
|
94
121
|
const res = await fetch(url)
|
|
@@ -119,7 +146,8 @@ Examples:
|
|
|
119
146
|
})
|
|
120
147
|
)
|
|
121
148
|
if (isJsonMode()) return printJson(result)
|
|
122
|
-
const url = result
|
|
149
|
+
const url = extractImageUrls(result)[0]
|
|
150
|
+
if (!url) { console.log('No image URL in response'); return }
|
|
123
151
|
if (opts.output) {
|
|
124
152
|
const res = await fetch(url)
|
|
125
153
|
writeFileSync(opts.output, Buffer.from(await res.arrayBuffer()))
|
|
@@ -153,7 +181,7 @@ To animate an existing image, use: blink ai animate
|
|
|
153
181
|
})
|
|
154
182
|
)
|
|
155
183
|
if (isJsonMode()) return printJson(result)
|
|
156
|
-
printUrl('Video', result
|
|
184
|
+
printUrl('Video', extractVideoUrl(result))
|
|
157
185
|
})
|
|
158
186
|
|
|
159
187
|
ai.command('animate <prompt> <image-source>')
|
|
@@ -189,7 +217,7 @@ Local files are automatically uploaded before animating.
|
|
|
189
217
|
})
|
|
190
218
|
)
|
|
191
219
|
if (isJsonMode()) return printJson(result)
|
|
192
|
-
printUrl('Video', result
|
|
220
|
+
printUrl('Video', extractVideoUrl(result))
|
|
193
221
|
})
|
|
194
222
|
|
|
195
223
|
ai.command('speech <text>')
|
package/src/commands/deploy.ts
CHANGED
|
@@ -128,7 +128,7 @@ Project resolution:
|
|
|
128
128
|
.action(async (projectArg: string | undefined, deploymentIdArg: string | undefined) => {
|
|
129
129
|
requireToken()
|
|
130
130
|
const projectId = requireProjectId(projectArg?.startsWith('proj_') ? projectArg : undefined)
|
|
131
|
-
const deploymentId = deploymentIdArg ?? projectArg
|
|
131
|
+
const deploymentId = deploymentIdArg ?? (projectArg?.startsWith('proj_') ? undefined : projectArg)
|
|
132
132
|
const result = await withSpinner('Rolling back...', () =>
|
|
133
133
|
appRequest(`/api/project/${projectId}/rollback`, { body: { deployment_id: deploymentId } })
|
|
134
134
|
)
|
package/src/commands/notify.ts
CHANGED
|
@@ -15,7 +15,7 @@ Examples:
|
|
|
15
15
|
$ blink notify email proj_xxx user@example.com "Subject" "Body"
|
|
16
16
|
`)
|
|
17
17
|
|
|
18
|
-
notify.command('email
|
|
18
|
+
notify.command('email <arg1> [arg2] [arg3] [arg4]')
|
|
19
19
|
.description('Send an email via your project\'s notification settings')
|
|
20
20
|
.option('--file <path>', 'Read email body from an HTML file instead of inline body')
|
|
21
21
|
.addHelpText('after', `
|
|
@@ -26,23 +26,26 @@ Examples:
|
|
|
26
26
|
|
|
27
27
|
The email is sent from your project's configured sender address (set in blink.new project settings).
|
|
28
28
|
`)
|
|
29
|
-
.action(async (
|
|
29
|
+
.action(async (arg1: string, arg2: string | undefined, arg3: string | undefined, arg4: string | undefined, opts) => {
|
|
30
30
|
requireToken()
|
|
31
31
|
let projectId: string
|
|
32
32
|
let to: string
|
|
33
33
|
let subject: string
|
|
34
34
|
let body: string
|
|
35
35
|
|
|
36
|
-
if (
|
|
37
|
-
projectId = requireProjectId(
|
|
38
|
-
to =
|
|
39
|
-
subject =
|
|
40
|
-
body =
|
|
36
|
+
if (arg1.startsWith('proj_') && arg2) {
|
|
37
|
+
projectId = requireProjectId(arg1)
|
|
38
|
+
to = arg2
|
|
39
|
+
subject = arg3 ?? ''
|
|
40
|
+
body = arg4 ?? ''
|
|
41
|
+
} else if (arg1.startsWith('proj_')) {
|
|
42
|
+
process.stderr.write('error: missing required argument \'to\'\n')
|
|
43
|
+
process.exit(1)
|
|
41
44
|
} else {
|
|
42
45
|
projectId = requireProjectId()
|
|
43
|
-
to =
|
|
44
|
-
subject =
|
|
45
|
-
body =
|
|
46
|
+
to = arg1
|
|
47
|
+
subject = arg2 ?? ''
|
|
48
|
+
body = arg3 ?? arg4 ?? ''
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
if (opts.file) body = readFileSync(opts.file, 'utf-8')
|