@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 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 = Array.isArray(result?.urls) ? result.urls : [result?.url];
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?.url ?? result?.urls?.[0];
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?.url ?? result?.video_url);
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?.url ?? result?.video_url);
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 [project] <to> <subject> [body]").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", `
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 (projectArg, toArg, subjectArg, bodyArg, opts) => {
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 (projectArg.startsWith("proj_")) {
862
- projectId = requireProjectId(projectArg);
863
- to = toArg;
864
- subject = subjectArg;
865
- body = bodyArg ?? "";
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 = projectArg;
869
- subject = toArg;
870
- body = subjectArg ?? bodyArg ?? "";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Blink platform CLI — deploy apps, manage databases, generate AI content",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
@@ -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: string[] = Array.isArray(result?.urls) ? result.urls : [result?.url]
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?.url ?? result?.urls?.[0]
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?.url ?? result?.video_url)
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?.url ?? result?.video_url)
220
+ printUrl('Video', extractVideoUrl(result))
193
221
  })
194
222
 
195
223
  ai.command('speech <text>')
@@ -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
  )
@@ -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 [project] <to> <subject> [body]')
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 (projectArg: string, toArg: string, subjectArg: string, bodyArg: string | undefined, opts) => {
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 (projectArg.startsWith('proj_')) {
37
- projectId = requireProjectId(projectArg)
38
- to = toArg
39
- subject = subjectArg
40
- body = bodyArg ?? ''
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 = projectArg
44
- subject = toArg
45
- body = subjectArg ?? bodyArg ?? ''
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')