gipity 1.0.427 → 1.0.429

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.
@@ -28,11 +28,24 @@ export const deployCommand = new Command('deploy')
28
28
  .option('--no-optimize', 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"')
29
29
  .option('--json', 'Output as JSON')
30
30
  .option('--inspect [path]', 'After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.')
31
+ // The settle knobs `page inspect` already has. An app whose first paint is behind
32
+ // a model load / async boot inspects as an empty page without them, so a caller
33
+ // that reaches for `--inspect` on such an app needs them HERE - it should not have
34
+ // to abandon the combined command and re-run inspect standalone just to wait.
35
+ .option('--wait <ms>', 'With --inspect: sleep this many ms before capturing (max 30000)')
36
+ .option('--wait-for <selector>', "With --inspect: wait until this CSS selector appears before capturing (deterministic; the app's own ready signal)")
37
+ .option('--wait-timeout <ms>', 'With --inspect: max ms to wait for --wait-for before giving up', '5000')
31
38
  .action((target, opts) => run('Deploy', async () => {
32
39
  if (target !== 'dev' && target !== 'prod') {
33
40
  console.error(clrError('Target must be "dev" or "prod"'));
34
41
  process.exit(1);
35
42
  }
43
+ // A settle flag only means one thing on a deploy: "verify the page once it's
44
+ // up, and give it time to come up." Asking for it without --inspect is not an
45
+ // error to bounce back — it's the intent, so honour it.
46
+ if (!opts.inspect && (opts.waitFor || deployCommand.getOptionValueSource('wait') !== undefined)) {
47
+ opts.inspect = true;
48
+ }
36
49
  const config = requireConfig();
37
50
  await syncBeforeAction(opts);
38
51
  const doDeploy = () => post(`/projects/${config.projectGuid}/deploy`, {
@@ -61,7 +74,12 @@ export const deployCommand = new Command('deploy')
61
74
  if (!opts.json)
62
75
  console.log('');
63
76
  try {
64
- await inspectPage(url, { json: opts.json });
77
+ await inspectPage(url, {
78
+ json: opts.json,
79
+ wait: opts.wait,
80
+ waitFor: opts.waitFor,
81
+ waitTimeout: opts.waitTimeout,
82
+ });
65
83
  }
66
84
  catch (err) {
67
85
  console.error(warning(`Inspect failed (deploy itself succeeded): ${err?.message ?? err}`));
@@ -25,12 +25,66 @@ async function downloadFile(url, filename) {
25
25
  if (!res.ok)
26
26
  throw new Error(`Download failed: ${res.status}`);
27
27
  const buffer = Buffer.from(await res.arrayBuffer());
28
- ensureOutputDir(filename);
29
- writeFileSync(filename, buffer);
30
- const savedPath = resolvePath(filename);
28
+ const outPath = correctExtension(filename, buffer);
29
+ ensureOutputDir(outPath);
30
+ writeFileSync(outPath, buffer);
31
+ const savedPath = resolvePath(outPath);
31
32
  await pushGenerated(savedPath);
32
33
  return savedPath;
33
34
  }
35
+ /** What these bytes ACTUALLY are, by magic number — the only trustworthy source.
36
+ * A provider hands back whatever its model produced (BFL's "png" request comes
37
+ * back as JPEG), so neither the caller's -o extension nor the response's
38
+ * content_type describes the file on disk. */
39
+ function sniffExt(buf) {
40
+ // subarray past the end yields a short string, so every compare below is
41
+ // length-safe on its own — no blanket minimum (which would skip the check
42
+ // entirely on a small file and hand back the misnamed path).
43
+ const ascii = (start, end) => buf.subarray(start, end).toString('latin1');
44
+ if (buf.length < 4)
45
+ return undefined;
46
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
47
+ return 'jpg';
48
+ if (ascii(0, 8) === '\x89PNG\r\n\x1a\n')
49
+ return 'png';
50
+ if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP')
51
+ return 'webp';
52
+ if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WAVE')
53
+ return 'wav';
54
+ if (ascii(0, 3) === 'GIF')
55
+ return 'gif';
56
+ if (ascii(4, 8) === 'ftyp')
57
+ return 'mp4';
58
+ if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3)
59
+ return 'webm';
60
+ if (ascii(0, 3) === 'ID3' || (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0))
61
+ return 'mp3';
62
+ return undefined;
63
+ }
64
+ /** Extensions that name the same format, so a .jpeg holding JPEG bytes is right. */
65
+ const EXT_ALIASES = { jpeg: 'jpg', htm: 'html', yml: 'yaml' };
66
+ const canonicalExt = (ext) => EXT_ALIASES[ext] ?? ext;
67
+ /** The format's name, for the note — "JPEG", not the "JPG" of its extension. */
68
+ const formatName = (ext) => (canonicalExt(ext) === 'jpg' ? 'JPEG' : ext.toUpperCase());
69
+ /** Save under an extension that matches the bytes. A file whose extension lies
70
+ * about its format is not a cosmetic problem: `page eval --camera` validates and
71
+ * dispatches on the extension, image tools and browsers sniff it, and an agent
72
+ * that Reads a "*.png" full of JPEG bytes has to stop and work out which one is
73
+ * lying. The caller's -o path is a request, not a fact — honour its directory and
74
+ * stem, and let the bytes name the format. */
75
+ function correctExtension(filename, buf) {
76
+ const actual = sniffExt(buf);
77
+ if (!actual)
78
+ return filename; // unrecognized bytes — nothing better to say
79
+ const dot = filename.lastIndexOf('.');
80
+ const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : '';
81
+ if (canonicalExt(asked) === actual)
82
+ return filename;
83
+ const corrected = `${dot > 0 ? filename.slice(0, dot) : filename}.${actual}`;
84
+ console.error(muted(`Note: the model returned ${formatName(actual)}, not ${asked ? formatName(asked) : 'the requested format'} — `
85
+ + `saving as ${corrected} so the extension matches the actual bytes.`));
86
+ return corrected;
87
+ }
34
88
  /** Create the parent directory of an -o path, so `-o src/audio/ding.mp3` works
35
89
  * in a tree that has no `src/audio/` yet instead of dying on a raw ENOENT.
36
90
  *
@@ -114,10 +168,13 @@ Examples:
114
168
  else {
115
169
  const sizeKb = Math.round(result.size_bytes / 1024);
116
170
  console.log(`${muted(`Generated with ${result.provider}/${result.model} (${sizeKb}KB)`)}`);
117
- console.log(success(`Saved to ${savedPath}`));
118
171
  if (result.seed !== undefined) {
119
172
  console.log(muted(`Seed ${result.seed} — pass --seed ${result.seed} to keep the next image coherent`));
120
173
  }
174
+ // Saved-path last: a caller reading a truncated tail of this output (an
175
+ // agent batching several generates in one shell call) must still see
176
+ // WHERE the bytes landed - that is the one line the next command needs.
177
+ console.log(success(`Saved to ${savedPath}`));
121
178
  }
122
179
  }
123
180
  catch (err) {