zdymak 0.3.0 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdymak",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Generate premium, spec-compliant App Store & Google Play preview videos (and store assets) from screenshots — one config-driven CLI across all your projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +1,19 @@
1
1
  /**
2
- * Capture mode (mode B) — for devs who'd rather grab frames from a running app than hand-manage a
3
- * screenshots folder. Project-agnostic on purpose: it does NOT build your app (that's your toolchain).
4
- * It snapshots or screen-records whatever is on the BOOTED simulator / connected device, strips the
5
- * alpha channel (Apple & Play reject transparency on screenshots), and writes store-ready PNGs you then
6
- * reference from your config's `scenes`.
2
+ * Capture mode (mode B) — grab store-ready PNGs from a running app instead of hand-managing a folder.
3
+ * Strips the alpha channel (Apple & Play reject transparency) and writes into your `capturesDir`, so the
4
+ * capture compose (`build`/`screenshots`) chain is one pipeline.
7
5
  *
8
- * zdymak capture --platform ios --name welcome # snap the booted iOS simulator
9
- * zdymak capture --platform android --name welcome # snap the connected device/emulator
10
- * zdymak capture --platform ios --record --out shots/rec # screen-record (stop with Ctrl-C)
6
+ * FULL WORKFLOW (start app drive by a handle → snap each screen):
7
+ * zdymak capture --platform ios --bundle com.x.app --arg -marketingScreen \
8
+ * --states welcome,today,study,answer --suffix -light \
9
+ * --build --project App.xcodeproj --scheme App --out marketing/ios/captures
10
+ * → boots a sim, (optionally builds+installs), then for each state relaunches the app with
11
+ * `<arg> <state>` and screenshots it. The app just needs a launch-arg "handle" that routes to a
12
+ * seeded screen (e.g. reads `-marketingScreen <id>` from UserDefaults). Then `zdymak build` composes.
11
13
  *
12
- * The two-mode contract: capture writes PNGs; the video engine consumes PNGs. So capture output drops
13
- * straight into `screenshotsDir`, and mode A (bring-your-own-screenshots) and mode B share one pipeline.
14
+ * Simpler modes:
15
+ * zdymak capture --platform ios|android --name welcome # single snapshot of the booted device
16
+ * zdymak capture --platform ios --record --out shots/rec # screen-record (stop with Ctrl-C)
14
17
  */
15
18
  import { spawn, spawnSync } from 'node:child_process';
16
19
  import fs from 'node:fs';
@@ -29,10 +32,41 @@ async function stripAlpha(file) {
29
32
  }
30
33
 
31
34
  function sh(cmd, args) {
32
- const r = spawnSync(cmd, args, { encoding: 'utf8' });
35
+ const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
33
36
  if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed: ${(r.stderr || r.stdout || '').trim()}`);
34
37
  return r.stdout;
35
38
  }
39
+ const out2 = (cmd, args) => (spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).stdout || '');
40
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
41
+ const udidRe = /\(([0-9A-Fa-f-]{36})\)/;
42
+
43
+ /** Boot (or reuse/create) an iOS simulator → UDID. Prefers --udid, then an already-booted sim, then a
44
+ * device matching --device (default iPhone 16 Pro Max), creating one if needed. */
45
+ function bootIosSim(flags) {
46
+ if (flags.udid) {
47
+ spawnSync('xcrun', ['simctl', 'boot', flags.udid], { stdio: 'ignore' });
48
+ spawnSync('xcrun', ['simctl', 'bootstatus', flags.udid, '-b'], { stdio: 'ignore' });
49
+ return flags.udid;
50
+ }
51
+ if (!flags.device) {
52
+ const booted = out2('xcrun', ['simctl', 'list', 'devices', 'booted']).match(udidRe);
53
+ if (booted) return booted[1];
54
+ }
55
+ const name = flags.device || 'iPhone 16 Pro Max';
56
+ let udid = out2('xcrun', ['simctl', 'list', 'devices', 'available'])
57
+ .split('\n').find((l) => l.includes(name) && udidRe.test(l))?.match(udidRe)?.[1];
58
+ if (!udid) {
59
+ const devtype = out2('xcrun', ['simctl', 'list', 'devicetypes']).split('\n')
60
+ .find((l) => l.includes(name))?.match(/(com\.apple[^\s)]+)/)?.[1];
61
+ const runtime = out2('xcrun', ['simctl', 'list', 'runtimes', 'ios']).split('\n').reverse()
62
+ .find((l) => /com\.apple[^\s)]+/.test(l))?.match(/(com\.apple[^\s)]+)/)?.[1];
63
+ if (!devtype || !runtime) throw new Error(`Could not resolve a simulator for "${name}".`);
64
+ udid = sh('xcrun', ['simctl', 'create', 'zdymak-capture', devtype, runtime]).trim();
65
+ }
66
+ spawnSync('xcrun', ['simctl', 'boot', udid], { stdio: 'ignore' });
67
+ spawnSync('xcrun', ['simctl', 'bootstatus', udid, '-b'], { stdio: 'ignore' });
68
+ return udid;
69
+ }
36
70
 
37
71
  async function captureIos(flags) {
38
72
  if (spawnSync('xcrun', ['simctl', 'help'], { stdio: 'ignore' }).status !== 0) {
@@ -50,6 +84,49 @@ async function captureIos(flags) {
50
84
  return;
51
85
  }
52
86
 
87
+ // FULL WORKFLOW: drive the app through screens by a launch-arg HANDLE, capturing each.
88
+ // zdymak capture --platform ios --bundle com.x.app --arg -screen --states a,b,c --suffix -light
89
+ // [--build --project X.xcodeproj --scheme S] [--device "iPhone 16 Pro Max"] [--settle 3]
90
+ if (flags.states) {
91
+ if (!flags.bundle || !flags.arg) {
92
+ throw new Error('state capture needs --bundle <id> and --arg <launch-handle> (e.g. -marketingScreen).');
93
+ }
94
+ const states = flags.states.split(',').map((s) => s.trim()).filter(Boolean);
95
+ const suffix = flags.suffix || '';
96
+ const settle = Number(flags.settle || 3);
97
+ const udid = bootIosSim(flags);
98
+
99
+ if (flags.build !== undefined) {
100
+ if (!flags.project || !flags.scheme) throw new Error('--build needs --project <.xcodeproj> and --scheme <name>.');
101
+ const dd = path.join(outDir, '.dd');
102
+ console.log(`▶︎ Building ${flags.scheme} for the simulator (this is the slow step)…`);
103
+ sh('xcodebuild', ['build', '-project', flags.project, '-scheme', flags.scheme, '-configuration', 'Debug',
104
+ '-destination', `id=${udid}`, '-derivedDataPath', dd, '-allowProvisioningUpdates']);
105
+ const app = out2('bash', ['-lc', `ls -dt "${dd}"/Build/Products/Debug-iphonesimulator/*.app 2>/dev/null | head -1`]).trim();
106
+ if (!app) throw new Error(`No .app under ${dd}/Build/Products/Debug-iphonesimulator`);
107
+ console.log(`▶︎ Installing ${path.basename(app)}…`);
108
+ sh('xcrun', ['simctl', 'install', udid, app]);
109
+ }
110
+
111
+ // Pin the canonical 9:41 marketing status bar (best-effort).
112
+ spawnSync('xcrun', ['simctl', 'status_bar', udid, 'override', '--time', '9:41',
113
+ '--batteryState', 'charging', '--batteryLevel', '100', '--cellularBars', '4', '--wifiBars', '3'], { stdio: 'ignore' });
114
+
115
+ console.log(`▶︎ Driving ${states.length} screens via "${flags.arg} <id>" on ${flags.bundle}…`);
116
+ for (const st of states) {
117
+ spawnSync('xcrun', ['simctl', 'terminate', udid, flags.bundle], { stdio: 'ignore' });
118
+ sh('xcrun', ['simctl', 'launch', udid, flags.bundle, flags.arg, st]);
119
+ await sleep(settle * 1000);
120
+ const out = path.join(outDir, `${st}${suffix}.png`);
121
+ sh('xcrun', ['simctl', 'io', udid, 'screenshot', out]);
122
+ await stripAlpha(out);
123
+ console.log(` ✓ ${st}${suffix}.png`);
124
+ }
125
+ console.log(`Done → ${outDir}`);
126
+ return;
127
+ }
128
+
129
+ // Single snapshot of whatever is on the booted sim.
53
130
  const out = path.join(outDir, `${flags.name || 'shot'}.png`);
54
131
  sh('xcrun', ['simctl', 'io', 'booted', 'screenshot', out]);
55
132
  await stripAlpha(out);
package/src/cli.mjs CHANGED
@@ -125,7 +125,10 @@ Usage:
125
125
  zdymak video [--config <path>] [--target <ids>] [--out <dir>]
126
126
  zdymak screenshots [--config <path>] [--out <dir>]
127
127
  zdymak specs
128
- zdymak capture --platform ios|android --name <screen> [--record] [--out <dir>]
128
+ zdymak capture --platform ios --bundle <id> --arg <handle> --states <a,b,c> [--suffix -light]
129
+ [--build --project <.xcodeproj> --scheme <name>] [--device <sim>] [--out <dir>]
130
+ # full workflow: start the app, drive each screen by a launch handle, snap store-ready PNGs
131
+ zdymak capture --platform ios|android --name <screen> # single snapshot of the booted device
129
132
  zdymak help
130
133
 
131
134
  Defaults: --config ${DEFAULT_CONFIG}. Needs ffmpeg on PATH (or $FFMPEG).