@navipartner/demo-video 0.6.0 → 0.7.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/README.md +75 -100
- package/bin/demo-video.mjs +101 -30
- package/bin/init.d.mts +2 -0
- package/bin/init.mjs +21 -22
- package/bin/pick.d.mts +0 -4
- package/bin/pick.mjs +0 -39
- package/dist/{adapter-CDOXdUZI.d.ts → adapter-bOi1ZBRa.d.ts} +36 -7
- package/dist/{chunk-D7UYGNSF.js → chunk-RMBVQL2J.js} +199 -48
- package/dist/index.d.ts +119 -20
- package/dist/index.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/index.js +527 -180
- package/package.json +1 -1
- package/skills/np-demo-video/SKILL.md +213 -230
- package/skills/np-demo-video/look.md +169 -79
- package/skills/np-demo-video/reference.md +81 -30
package/README.md
CHANGED
|
@@ -1,132 +1,107 @@
|
|
|
1
1
|
# @navipartner/demo-video
|
|
2
2
|
|
|
3
|
-
Marketing demo videos of React apps
|
|
3
|
+
Marketing demo videos of React apps, in the NaviPartner look: brisk camera
|
|
4
|
+
pushes and big serif cards that can show a snapshot of the result beside
|
|
5
|
+
the words. A story is a showcase of an app's main journey (45–90 s) or a
|
|
6
|
+
clip of one feature (15–30 s). The engine films an app on its own
|
|
7
|
+
Playwright e2e mocks, frame by frame at 60 fps, so pans stay smooth. It
|
|
8
|
+
works in pnpm repos on GitHub Actions, with `@playwright/test` 1.59 or later.
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- Customer brands researched per video, never saved.
|
|
9
|
-
- Smooth 60 fps, frame by frame.
|
|
10
|
-
- pnpm repos, GitHub Actions, `@playwright/test` 1.59+.
|
|
10
|
+
NaviPartner's brand is the only one kept. A video in a customer's branding
|
|
11
|
+
is made in Linear, where @Linear researches their brand for that one video
|
|
12
|
+
and saves nothing.
|
|
11
13
|
|
|
12
14
|
## Setup
|
|
13
15
|
|
|
16
|
+
From the repo root:
|
|
17
|
+
|
|
14
18
|
npx --ignore-scripts @navipartner/demo-video init
|
|
15
19
|
pnpm install
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
`init` adds the skill, the `demo video` label and the workflow, and finds
|
|
22
|
+
the apps: folders with a `playwright.config`. A repo with one app is set up
|
|
23
|
+
in one go; with several, it prints the `init --app <folder>` line to run
|
|
24
|
+
for each.
|
|
25
|
+
It's safe to re-run, and `--dry-run` previews. Then ask Claude for a demo
|
|
26
|
+
video of the app: it writes the adapter and the primary story with you.
|
|
23
27
|
|
|
24
28
|
## Get a video
|
|
25
29
|
|
|
26
|
-
|
|
|
30
|
+
| Where | Ask | For |
|
|
27
31
|
|---|---|---|
|
|
28
|
-
| @Linear "video of checkout from np-pos
|
|
29
|
-
| @Linear "
|
|
30
|
-
|
|
|
31
|
-
| `/demo-video checkout`
|
|
32
|
-
|
|
|
33
|
-
| `/demo-video help` | members | menu of stories |
|
|
34
|
-
| `demo video` label on a PR | whoever labels | every changed story, each push |
|
|
35
|
-
| Actions → Demo video → Run workflow | repo writers | any story, any branch |
|
|
36
|
-
| `pnpm exec demo-video render` | developers | any story, locally |
|
|
37
|
-
|
|
38
|
-
- Requests only pick existing stories.
|
|
39
|
-
- New stories come via PRs.
|
|
40
|
-
- PR comments: first line is the request.
|
|
41
|
-
- Unknown names get the menu.
|
|
42
|
-
- Customer branding only through @Linear.
|
|
32
|
+
| Linear | @Linear "video of checkout from np-pos" | anyone who wants a video of an existing story |
|
|
33
|
+
| Linear | @Linear "… for Northwind (northwind.example)" | the same, in a customer's researched brand |
|
|
34
|
+
| A PR | the `demo video` label | reviewing the stories the PR adds or changes |
|
|
35
|
+
| A PR | `/demo-video checkout` (`help` for the menu) | filming any story from the PR's branch |
|
|
36
|
+
| Your machine | `pnpm exec demo-video render` | writing and tuning a story |
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
Requests only pick existing stories; new stories come through a developer's
|
|
39
|
+
PR. In a PR comment, the first line is the request, a translation is found
|
|
40
|
+
by its language (`checkout in danish`), and a name that doesn't match gets
|
|
41
|
+
the menu.
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
- Name the repo, story and customer.
|
|
48
|
-
- Follows the skill's `linear.md`.
|
|
49
|
-
- NaviPartner itself: 60 fps video.
|
|
50
|
-
- A customer: a pitch draft.
|
|
51
|
-
- MP4 and contact sheet attached.
|
|
52
|
-
- Nothing committed, nothing kept.
|
|
53
|
-
- Linear AI credits, no API key.
|
|
54
|
-
- Slow sandbox: ~15× video length.
|
|
43
|
+
### In Linear
|
|
55
44
|
|
|
56
|
-
|
|
45
|
+
Ask @Linear in plain words, naming the repo, the story and the customer. In
|
|
46
|
+
a coding session it follows the skill's `linear.md`, attaches the MP4 and
|
|
47
|
+
its contact sheet to the issue, and commits nothing. It runs on Linear's AI
|
|
48
|
+
credits; its sandbox is slow, about 15× the video's length at 60 fps.
|
|
57
49
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
For a customer, the video is a **pitch draft**. @Linear researches their
|
|
51
|
+
brand from their own website, the engine checks it and films at 30 fps, and
|
|
52
|
+
the brand is deleted afterwards. Fonts are Google Fonts stand-ins. A pitch
|
|
53
|
+
draft is for that customer only: check it before sending it, and any other
|
|
54
|
+
use needs their written permission.
|
|
63
55
|
|
|
64
56
|
## Stories and brands
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
- The skill has fields and the look.
|
|
58
|
+
A story is `demo/stories/<name>.json`: the scenes of one journey, built only
|
|
59
|
+
from what the e2e mocks return. One story per app is its primary story. A
|
|
60
|
+
translation is a story of its own, such as `checkout-da.json`, with the same
|
|
61
|
+
flow and its cards in that language.
|
|
62
|
+
|
|
63
|
+
Repos keep no brands. `navipartner` ships with the engine, `check` refuses a
|
|
64
|
+
`demo/brands/` folder, and stories never name a brand. The adapter's
|
|
65
|
+
`brandNeeds` lists what a brand gives the app (a background image, a
|
|
66
|
+
secondary colour), which is exactly what a pitch draft researches. The skill
|
|
67
|
+
(`skills/np-demo-video/`) has every field and the look.
|
|
77
68
|
|
|
78
69
|
## Commands
|
|
79
70
|
|
|
80
|
-
Run in the app's folder: `pnpm exec demo-video <command>`.
|
|
71
|
+
Run them in the app's folder: `pnpm exec demo-video <command>`.
|
|
81
72
|
|
|
82
73
|
| Command | Does |
|
|
83
74
|
|---|---|
|
|
84
|
-
| `init` | sets up a repo or app |
|
|
85
|
-
| `list` | stories, primary first |
|
|
86
|
-
| `check` | dry run in seconds
|
|
87
|
-
| `render` | MP4, contact sheet
|
|
88
|
-
| `record
|
|
89
|
-
| `brand-needs` | what a brand must give |
|
|
90
|
-
| `pitch <proposal.json>` | pitch draft from a researched brand |
|
|
91
|
-
| `pick` | what a request means (workflows) |
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| `--brand`, `--brands <dir>` | used by `pitch` only |
|
|
100
|
-
|
|
101
|
-
Output lands in `demo/out/`. Failures keep Playwright's output in `demo/out/failed/`.
|
|
102
|
-
|
|
103
|
-
## Develop
|
|
75
|
+
| `init` | sets up a repo or an app |
|
|
76
|
+
| `list` | the stories, primary first |
|
|
77
|
+
| `check` | a dry run in seconds that names what would film badly |
|
|
78
|
+
| `render` | the MP4, a contact sheet and ten stills, in `demo/out/` (it checks first when cards show snapshots) |
|
|
79
|
+
| `record`, `encode`, `stills` | `render` in parts |
|
|
80
|
+
| `brand-needs` | what a brand must give this app |
|
|
81
|
+
| `pitch <proposal.json>` | a pitch draft from a researched brand |
|
|
82
|
+
| `pick` | what a request means (used by the workflows) |
|
|
83
|
+
|
|
84
|
+
`--story <name>` picks a story (default: the primary one), `--draft` films
|
|
85
|
+
at 30 fps in about half the time, `--app <dir>` names the app folder and
|
|
86
|
+
`--json` gives machine-readable output. When something fails, Playwright's
|
|
87
|
+
output is kept in `demo/out/failed/`.
|
|
88
|
+
|
|
89
|
+
## Develop and release
|
|
104
90
|
|
|
105
91
|
pnpm install
|
|
106
92
|
pnpm test && pnpm typecheck && pnpm lint && pnpm build
|
|
107
93
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
114
|
-
|
|
115
|
-
- CI publishes to npm (`latest`).
|
|
116
|
-
- Trusted publishing, no npm token.
|
|
117
|
-
- `v0` tag moves along.
|
|
118
|
-
- Manual `release.yml` run: `next` prerelease.
|
|
119
|
-
|
|
120
|
-
## Public package, private repo
|
|
121
|
-
|
|
122
|
-
- `check-public.mjs` checks every tarball.
|
|
123
|
-
- Allowed files only, no source maps.
|
|
124
|
-
- No secrets, emails or local paths.
|
|
125
|
-
- Names aren't caught: keep customers out.
|
|
94
|
+
CI runs the tests, actionlint and a smoke render of the fixture app. To
|
|
95
|
+
release, bump `version` in `package.json` on main, then
|
|
96
|
+
`git tag vX.Y.Z && git push origin vX.Y.Z`. CI publishes to npm as `latest`
|
|
97
|
+
through trusted publishing (no npm token) and moves the `v0` tag. Running
|
|
98
|
+
`release.yml` by hand publishes a `next` prerelease.
|
|
126
99
|
|
|
127
|
-
|
|
100
|
+
The package is public; this repo is not. `scripts/check-public.mjs` checks
|
|
101
|
+
every tarball for files outside the allowlist, source maps, secrets, emails
|
|
102
|
+
and local paths. It can't catch names, so keep customers and people out of
|
|
103
|
+
everything that ships.
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
- Breaking that means `v1`.
|
|
105
|
+
Projects pin the CLI in their lockfile, but their workflows follow `v0` at
|
|
106
|
+
once. So `render.yml` and `request.yml` must work with every CLI from 0.4;
|
|
107
|
+
anything that can't is `v1`.
|
package/bin/demo-video.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import path from "node:path";
|
|
|
8
8
|
|
|
9
9
|
import { listStories, parseArgs } from "./args.mjs";
|
|
10
10
|
import { encodeRecording, extractStillsFrom } from "./encode.mjs";
|
|
11
|
-
import { initApp, initProject } from "./init.mjs";
|
|
12
|
-
import { appMenu, menuMarkdown, pick
|
|
11
|
+
import { findApps, initApp, initProject } from "./init.mjs";
|
|
12
|
+
import { appMenu, menuMarkdown, pick } from "./pick.mjs";
|
|
13
13
|
import { buildPitchBrand } from "./pitch.mjs";
|
|
14
14
|
import { writeRunner } from "./runner.mjs";
|
|
15
15
|
|
|
@@ -56,15 +56,85 @@ const runPlaywright = (args, mode) => {
|
|
|
56
56
|
DEMO_OUT: args.outDir,
|
|
57
57
|
DEMO_WORK: args.workDir,
|
|
58
58
|
DEMO_DRAFT: args.draft ? "1" : "",
|
|
59
|
+
DEMO_STILLS: args.stillsDir ?? "",
|
|
59
60
|
},
|
|
60
61
|
},
|
|
61
62
|
);
|
|
62
63
|
return result.status ?? 1;
|
|
63
64
|
};
|
|
64
65
|
|
|
66
|
+
const readReport = (args) => {
|
|
67
|
+
const reportPath = path.join(args.workDir, "check.json");
|
|
68
|
+
return fs.existsSync(reportPath)
|
|
69
|
+
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
70
|
+
: {
|
|
71
|
+
ok: false,
|
|
72
|
+
error:
|
|
73
|
+
"the dry run produced no report (Playwright did not run the demo test — see its output above)",
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A card that shows a still of its chapter's result (`show`) needs the
|
|
78
|
+
// still before filming: a check captures it, as the element last looks in
|
|
79
|
+
// that chapter. Returns the check's run, whose folder holds the stills
|
|
80
|
+
// until the take is filmed, or null when no card shows one.
|
|
81
|
+
const captureStills = (args) => {
|
|
82
|
+
const story = JSON.parse(fs.readFileSync(args.storyboardPath, "utf8"));
|
|
83
|
+
const shows =
|
|
84
|
+
story.title?.show !== undefined ||
|
|
85
|
+
(story.steps ?? []).some((s) => s?.do === "card" && s.show !== undefined);
|
|
86
|
+
if (!shows) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const workDir = path.join(path.dirname(args.workDir), "check");
|
|
90
|
+
const runnerDir = path.join(workDir, "runner");
|
|
91
|
+
const check = {
|
|
92
|
+
...args,
|
|
93
|
+
command: "check",
|
|
94
|
+
workDir,
|
|
95
|
+
runnerDir,
|
|
96
|
+
configPath: path.join(runnerDir, "playwright.config.mjs"),
|
|
97
|
+
stillsDir: path.join(workDir, "stills"),
|
|
98
|
+
};
|
|
99
|
+
const status = runPlaywright(check, "check");
|
|
100
|
+
const report = readReport(check);
|
|
101
|
+
if (!report.ok || status !== 0) {
|
|
102
|
+
finish(check, false);
|
|
103
|
+
throw new Error(
|
|
104
|
+
`The story doesn't pass check, so its card stills can't be captured: ${report.error}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return check;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Films the take, with the card stills when the story has any.
|
|
111
|
+
const film = (args) => {
|
|
112
|
+
const check = captureStills(args);
|
|
113
|
+
const status = runPlaywright(
|
|
114
|
+
{ ...args, stillsDir: check?.stillsDir },
|
|
115
|
+
"record",
|
|
116
|
+
);
|
|
117
|
+
if (check) {
|
|
118
|
+
finish(check, true);
|
|
119
|
+
}
|
|
120
|
+
return status;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// How long each kind runs (look.md, src/timing.ts KIND_SECONDS).
|
|
124
|
+
const KIND_SECONDS = { showcase: [45, 90], feature: [15, 30] };
|
|
125
|
+
|
|
65
126
|
const printReport = (report) => {
|
|
66
127
|
if (report.ok) {
|
|
67
128
|
console.log(`✓ story passes (${report.steps} steps)`);
|
|
129
|
+
const range = KIND_SECONDS[report.kind];
|
|
130
|
+
if (report.seconds !== undefined && range) {
|
|
131
|
+
const [low, high] = range;
|
|
132
|
+
const fits =
|
|
133
|
+
report.seconds < low ? "short" : report.seconds > high ? "long" : null;
|
|
134
|
+
console.log(
|
|
135
|
+
` about ${report.seconds} s; a ${report.kind === "feature" ? "feature clip" : "showcase"} runs ${low}–${high} s${fits ? `, so this is ${fits}` : ""}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
68
138
|
return;
|
|
69
139
|
}
|
|
70
140
|
const where = report.step
|
|
@@ -100,6 +170,14 @@ const runInit = (argv) => {
|
|
|
100
170
|
return 2;
|
|
101
171
|
}
|
|
102
172
|
const root = process.cwd();
|
|
173
|
+
if (app && !fs.existsSync(path.join(root, app, "package.json"))) {
|
|
174
|
+
// Name the folders that are apps, so the fix is a copy away.
|
|
175
|
+
const apps = findApps(root);
|
|
176
|
+
console.error(
|
|
177
|
+
`${app} isn't an app: it has no package.json.${apps.length ? `\nApps here: ${apps.join(", ")}` : ""}`,
|
|
178
|
+
);
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
103
181
|
const result = app
|
|
104
182
|
? initApp(path.resolve(root, app), { dryRun })
|
|
105
183
|
: initProject(root, { dryRun });
|
|
@@ -124,35 +202,31 @@ const runInit = (argv) => {
|
|
|
124
202
|
return 0;
|
|
125
203
|
};
|
|
126
204
|
|
|
127
|
-
// demo-video pick [--request "<text>"
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// flags are exact names (the Run workflow form).
|
|
205
|
+
// demo-video pick [--request "<text>"] [--json] <app dir>...: which existing
|
|
206
|
+
// story a request means (see pick.mjs): a /demo-video comment, or a request
|
|
207
|
+
// to @Linear. No request: the primary story.
|
|
131
208
|
const runPick = (argv) => {
|
|
132
|
-
|
|
209
|
+
let request = "";
|
|
133
210
|
const dirs = [];
|
|
134
211
|
let json = false;
|
|
135
212
|
for (let i = 0; i < argv.length; i++) {
|
|
136
213
|
const arg = argv[i];
|
|
137
214
|
if (arg === "--json") {
|
|
138
215
|
json = true;
|
|
139
|
-
} else if (
|
|
140
|
-
|
|
216
|
+
} else if (arg === "--request") {
|
|
217
|
+
request = argv[++i] ?? "";
|
|
141
218
|
} else {
|
|
142
219
|
dirs.push(arg);
|
|
143
220
|
}
|
|
144
221
|
}
|
|
145
222
|
if (dirs.length === 0) {
|
|
146
223
|
console.error(
|
|
147
|
-
'Usage: demo-video pick [--request "<text>"
|
|
224
|
+
'Usage: demo-video pick [--request "<text>"] [--json] <app dir>...',
|
|
148
225
|
);
|
|
149
226
|
return 2;
|
|
150
227
|
}
|
|
151
228
|
const apps = dirs.map((d) => appMenu(path.resolve(d)));
|
|
152
|
-
const result =
|
|
153
|
-
flags.request !== undefined
|
|
154
|
-
? pick(flags.request, apps)
|
|
155
|
-
: pickExact(apps, flags);
|
|
229
|
+
const result = pick(request, apps);
|
|
156
230
|
const out = result.menu
|
|
157
231
|
? {
|
|
158
232
|
menu: true,
|
|
@@ -211,21 +285,25 @@ const runPitch = async (argv) => {
|
|
|
211
285
|
brandsDir,
|
|
212
286
|
"--json",
|
|
213
287
|
];
|
|
288
|
+
// The check also captures the card stills, in the customer's brand.
|
|
214
289
|
const check = parseArgs(["check", ...common], process.cwd());
|
|
290
|
+
check.stillsDir = path.join(check.workDir, "stills");
|
|
215
291
|
const checked = runPlaywright(check, "check");
|
|
216
|
-
const
|
|
217
|
-
const report = fs.existsSync(reportPath)
|
|
218
|
-
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
219
|
-
: { ok: false, error: "the dry run produced no report" };
|
|
220
|
-
finish(check, report.ok && checked === 0);
|
|
292
|
+
const report = readReport(check);
|
|
221
293
|
if (!report.ok || checked !== 0) {
|
|
294
|
+
finish(check, false);
|
|
222
295
|
throw new Error(
|
|
223
296
|
`The researched brand doesn't film with this story: ${report.error}`,
|
|
224
297
|
);
|
|
225
298
|
}
|
|
226
299
|
// A pitch is a draft for one customer: 30 fps, about half the time.
|
|
227
300
|
const render = parseArgs(["render", ...common, "--draft"], process.cwd());
|
|
228
|
-
|
|
301
|
+
const filmed = runPlaywright(
|
|
302
|
+
{ ...render, stillsDir: check.stillsDir },
|
|
303
|
+
"record",
|
|
304
|
+
);
|
|
305
|
+
finish(check, true);
|
|
306
|
+
if (filmed !== 0) {
|
|
229
307
|
finish(render, false);
|
|
230
308
|
throw new Error("Filming the pitch draft failed (see above)");
|
|
231
309
|
}
|
|
@@ -283,14 +361,7 @@ const run = () => {
|
|
|
283
361
|
case "check": {
|
|
284
362
|
const status = runPlaywright(args, "check");
|
|
285
363
|
// The report comes back through the work folder and goes with it.
|
|
286
|
-
const
|
|
287
|
-
const report = fs.existsSync(reportPath)
|
|
288
|
-
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
289
|
-
: {
|
|
290
|
-
ok: false,
|
|
291
|
-
error:
|
|
292
|
-
"the dry run produced no report (Playwright did not run the demo test — see its output above)",
|
|
293
|
-
};
|
|
364
|
+
const report = readReport(args);
|
|
294
365
|
const ok = report.ok && status === 0;
|
|
295
366
|
finish(args, ok);
|
|
296
367
|
if (args.json) {
|
|
@@ -302,14 +373,14 @@ const run = () => {
|
|
|
302
373
|
}
|
|
303
374
|
case "record": {
|
|
304
375
|
// The recording stays in the work folder for `encode`.
|
|
305
|
-
const status =
|
|
376
|
+
const status = film(args);
|
|
306
377
|
if (status !== 0) {
|
|
307
378
|
finish(args, false);
|
|
308
379
|
}
|
|
309
380
|
return status;
|
|
310
381
|
}
|
|
311
382
|
case "render": {
|
|
312
|
-
const status =
|
|
383
|
+
const status = film(args);
|
|
313
384
|
if (status !== 0) {
|
|
314
385
|
finish(args, false);
|
|
315
386
|
return status;
|
package/bin/init.d.mts
CHANGED
|
@@ -5,6 +5,8 @@ export interface InitResult {
|
|
|
5
5
|
}
|
|
6
6
|
export const IGNORE_LINE: string;
|
|
7
7
|
export const LABEL: string;
|
|
8
|
+
/** How init is run before the package is installed. */
|
|
9
|
+
export const INIT: string;
|
|
8
10
|
export const CALLER_WORKFLOW: string;
|
|
9
11
|
export function initRepo(
|
|
10
12
|
root: string,
|
package/bin/init.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// workflow. Safe to re-run; with dryRun it reports what it would change.
|
|
10
10
|
|
|
11
11
|
const PKG = "@navipartner/demo-video";
|
|
12
|
+
/** How init is run before the package is installed (the README's way). */
|
|
13
|
+
export const INIT = `npx --ignore-scripts ${PKG} init`;
|
|
12
14
|
export const IGNORE_LINE = "**/demo/out/";
|
|
13
15
|
export const LABEL = "demo video";
|
|
14
16
|
|
|
@@ -22,23 +24,14 @@ const engineVersion = () =>
|
|
|
22
24
|
|
|
23
25
|
export const CALLER_WORKFLOW = `name: Demo video
|
|
24
26
|
# The "${LABEL}" label films the stories a PR adds or changes. A comment
|
|
25
|
-
# "/demo-video [<story>]" on a PR
|
|
26
|
-
#
|
|
27
|
+
# "/demo-video [<story>]" on a PR films an existing story in NaviPartner's
|
|
28
|
+
# brand. A customer's branding: ask
|
|
27
29
|
# @Linear (the skill's linear.md); repos keep no brands.
|
|
28
30
|
on:
|
|
29
31
|
pull_request:
|
|
30
32
|
types: [labeled, synchronize]
|
|
31
33
|
issue_comment:
|
|
32
34
|
types: [created]
|
|
33
|
-
# Actions → Demo video → Run workflow: film a story.
|
|
34
|
-
workflow_dispatch:
|
|
35
|
-
inputs:
|
|
36
|
-
app:
|
|
37
|
-
description: "App folder, e.g. apps/retail-self-service (empty: the only app)"
|
|
38
|
-
default: ""
|
|
39
|
-
story:
|
|
40
|
-
description: "Story name (empty: the app's primary story)"
|
|
41
|
-
default: ""
|
|
42
35
|
permissions: {}
|
|
43
36
|
jobs:
|
|
44
37
|
render:
|
|
@@ -48,15 +41,12 @@ jobs:
|
|
|
48
41
|
pull-requests: write
|
|
49
42
|
uses: navipartner/demo-video/.github/workflows/render.yml@v0
|
|
50
43
|
request:
|
|
51
|
-
if: github.event_name == '
|
|
44
|
+
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/demo-video')
|
|
52
45
|
permissions:
|
|
53
46
|
contents: read
|
|
54
47
|
pull-requests: write
|
|
55
48
|
issues: write
|
|
56
49
|
uses: navipartner/demo-video/.github/workflows/request.yml@v0
|
|
57
|
-
with:
|
|
58
|
-
app: \${{ inputs.app }}
|
|
59
|
-
story: \${{ inputs.story }}
|
|
60
50
|
`;
|
|
61
51
|
|
|
62
52
|
const read = (file) =>
|
|
@@ -257,7 +247,7 @@ export const initRepo = (root, { dryRun = false, label = true } = {}) => {
|
|
|
257
247
|
.find(Boolean);
|
|
258
248
|
if (existing && !existing.includes("request.yml@")) {
|
|
259
249
|
todo.push(
|
|
260
|
-
'.github/workflows/demo-video.yml predates the "request" job (/demo-video comments
|
|
250
|
+
'.github/workflows/demo-video.yml predates the "request" job (/demo-video comments): delete it and re-run init to get the current one.',
|
|
261
251
|
);
|
|
262
252
|
}
|
|
263
253
|
} else {
|
|
@@ -378,7 +368,7 @@ export const initApp = (appDir, { dryRun = false } = {}) => {
|
|
|
378
368
|
const todo = [];
|
|
379
369
|
const pkgFile = path.join(appDir, "package.json");
|
|
380
370
|
if (!fs.existsSync(pkgFile)) {
|
|
381
|
-
throw new Error(
|
|
371
|
+
throw new Error(`${appDir} has no package.json, so it isn't an app.`);
|
|
382
372
|
}
|
|
383
373
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
384
374
|
const version = engineVersion();
|
|
@@ -408,14 +398,19 @@ export const initApp = (appDir, { dryRun = false } = {}) => {
|
|
|
408
398
|
);
|
|
409
399
|
}
|
|
410
400
|
|
|
401
|
+
const configFile = ["ts", "mts", "js", "mjs", "cts", "cjs"]
|
|
402
|
+
.map((ext) => path.join(appDir, `playwright.config.${ext}`))
|
|
403
|
+
.find((f) => fs.existsSync(f));
|
|
404
|
+
if (!configFile) {
|
|
405
|
+
todo.push(
|
|
406
|
+
"Add a playwright.config to this folder: videos run on the app's e2e setup.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
411
409
|
const demoDir = path.join(appDir, "demo");
|
|
412
410
|
const adapter = path.join(demoDir, "demo.config.ts");
|
|
413
411
|
if (fs.existsSync(adapter)) {
|
|
414
412
|
changes.push(["ok", "demo/demo.config.ts", "exists"]);
|
|
415
413
|
} else {
|
|
416
|
-
const configFile = ["ts", "mts", "js", "mjs"]
|
|
417
|
-
.map((ext) => path.join(appDir, `playwright.config.${ext}`))
|
|
418
|
-
.find((f) => fs.existsSync(f));
|
|
419
414
|
const webServer = configFile ? webServerOf(read(configFile)) : {};
|
|
420
415
|
const skeleton = adapterSkeleton({
|
|
421
416
|
app: (pkg.name ?? path.basename(appDir)).replace(/^@[^/]+\//, ""),
|
|
@@ -504,12 +499,16 @@ export const initProject = (root, options = {}) => {
|
|
|
504
499
|
}
|
|
505
500
|
const todo = [...repo.todo];
|
|
506
501
|
if (apps.length > 1) {
|
|
502
|
+
// Exactly what to run: one line per app, copied as is.
|
|
507
503
|
todo.push(
|
|
508
|
-
|
|
504
|
+
[
|
|
505
|
+
`${apps.length} apps found and none set up yet. Run this for each app you want videos of:`,
|
|
506
|
+
...apps.map((app) => ` ${INIT} --app ${app}`),
|
|
507
|
+
].join("\n"),
|
|
509
508
|
);
|
|
510
509
|
} else {
|
|
511
510
|
todo.push(
|
|
512
|
-
|
|
511
|
+
`No app found (a folder with package.json and playwright.config). Run ${INIT} --app <folder> once it has both.`,
|
|
513
512
|
);
|
|
514
513
|
}
|
|
515
514
|
return { apps, changes: repo.changes, todo };
|
package/bin/pick.d.mts
CHANGED
|
@@ -15,7 +15,3 @@ export type Pick =
|
|
|
15
15
|
export function appMenu(appDir: string): AppMenu;
|
|
16
16
|
export function pick(request: string, apps: AppMenu[]): Pick;
|
|
17
17
|
export function menuMarkdown(apps: AppMenu[], reason: string): string;
|
|
18
|
-
export function pickExact(
|
|
19
|
-
apps: AppMenu[],
|
|
20
|
-
names: { app?: string; story?: string },
|
|
21
|
-
): Pick;
|
package/bin/pick.mjs
CHANGED
|
@@ -171,42 +171,3 @@ export const menuMarkdown = (apps, reason) => {
|
|
|
171
171
|
}
|
|
172
172
|
return lines.join("\n");
|
|
173
173
|
};
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The form's version (Actions → Run workflow): app and story by name, no
|
|
177
|
-
* guessing. Empty story = the primary story, empty app = the only app. A
|
|
178
|
-
* name that doesn't exist gets the menu, so a typo never films something
|
|
179
|
-
* else. `apps` in a menu result narrows it.
|
|
180
|
-
*/
|
|
181
|
-
export const pickExact = (apps, { app = "", story = "" }) => {
|
|
182
|
-
const slashes = (p) => p.split("\\").join("/");
|
|
183
|
-
const wanted = slashes(app).replace(/\/+$/, "");
|
|
184
|
-
const candidates = wanted
|
|
185
|
-
? apps.filter((a) => {
|
|
186
|
-
const dir = slashes(a.dir);
|
|
187
|
-
return (
|
|
188
|
-
a.name === wanted || dir === wanted || dir.endsWith(`/${wanted}`)
|
|
189
|
-
);
|
|
190
|
-
})
|
|
191
|
-
: apps;
|
|
192
|
-
if (candidates.length === 0) {
|
|
193
|
-
return { menu: true, reason: `No app \`${wanted}\`. The apps:` };
|
|
194
|
-
}
|
|
195
|
-
if (candidates.length > 1) {
|
|
196
|
-
return { menu: true, reason: "Which app? Fill in App with one of:" };
|
|
197
|
-
}
|
|
198
|
-
const chosen = candidates[0];
|
|
199
|
-
const found = story
|
|
200
|
-
? chosen.stories.find((s) => s.name === story)
|
|
201
|
-
: chosen.stories.find((s) => s.primary);
|
|
202
|
-
if (!found) {
|
|
203
|
-
return {
|
|
204
|
-
menu: true,
|
|
205
|
-
reason: story
|
|
206
|
-
? `No story \`${story}\` in ${chosen.name}. Pick one:`
|
|
207
|
-
: `${chosen.name} has no primary story. Pick one:`,
|
|
208
|
-
apps: [chosen],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return { app: chosen.dir, story: found.name };
|
|
212
|
-
};
|