@navipartner/demo-video 0.6.0 → 0.7.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/README.md CHANGED
@@ -1,132 +1,104 @@
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: calm camera
4
+ pushes and serif statement cards. The engine films an app on its own
5
+ Playwright e2e mocks, frame by frame at 60 fps, so pans stay smooth. It
6
+ works in pnpm repos on GitHub Actions, with `@playwright/test` 1.59 or later.
4
7
 
5
- - Filmed on the app's own e2e mocks.
6
- - NaviPartner look: camera pushes, serif cards.
7
- - NaviPartner's brand, the only one kept.
8
- - Customer brands researched per video, never saved.
9
- - Smooth 60 fps, frame by frame.
10
- - pnpm repos, GitHub Actions, `@playwright/test` 1.59+.
8
+ NaviPartner's brand is the only one kept. A video in a customer's branding
9
+ is made in Linear, where @Linear researches their brand for that one video
10
+ and saves nothing.
11
11
 
12
12
  ## Setup
13
13
 
14
+ From the repo root:
15
+
14
16
  npx --ignore-scripts @navipartner/demo-video init
15
17
  pnpm install
16
18
 
17
- - Adds the skill, label and workflow.
18
- - Finds apps with a `playwright.config`.
19
- - One app: set up directly.
20
- - Several apps: `init --app <folder>` each.
21
- - Safe to re-run; `--dry-run` previews.
22
- - Then ask Claude for a video.
19
+ `init` adds the skill, the `demo video` label and the workflow, and finds
20
+ the apps: folders with a `playwright.config`. A repo with one app is set up
21
+ in one go; with several, run `init --app <folder>` for each one you want.
22
+ It's safe to re-run, and `--dry-run` previews. Then ask Claude for a demo
23
+ video of the app: it writes the adapter and the primary story with you.
23
24
 
24
25
  ## Get a video
25
26
 
26
- | Ask | Who | You get |
27
+ | Where | Ask | For |
27
28
  |---|---|---|
28
- | @Linear "video of checkout from np-pos for Northwind (northwind.example)" | Linear users | that story in Northwind's researched brand, attached |
29
- | @Linear "video of checkout from np-pos" | Linear users | that story, NaviPartner brand, attached |
30
- | `/demo-video` on a PR | members | primary story, from the PR branch |
31
- | `/demo-video checkout` | members | that story |
32
- | `/demo-video checkout in danish` | members | its translation |
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.
29
+ | Linear | @Linear "video of checkout from np-pos" | anyone who wants a video of an existing story |
30
+ | Linear | @Linear "… for Northwind (northwind.example)" | the same, in a customer's researched brand |
31
+ | A PR | the `demo video` label | reviewing the stories the PR adds or changes |
32
+ | A PR | `/demo-video checkout` (`help` for the menu) | filming any story from the PR's branch |
33
+ | Your machine | `pnpm exec demo-video render` | writing and tuning a story |
43
34
 
44
- ### In Linear
35
+ Requests only pick existing stories; new stories come through a developer's
36
+ PR. In a PR comment, the first line is the request, a translation is found
37
+ by its language (`checkout in danish`), and a name that doesn't match gets
38
+ the menu.
45
39
 
46
- - Ask @Linear in plain words.
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.
40
+ ### In Linear
55
41
 
56
- ### Pitch drafts
42
+ Ask @Linear in plain words, naming the repo, the story and the customer. In
43
+ a coding session it follows the skill's `linear.md`, attaches the MP4 and
44
+ its contact sheet to the issue, and commits nothing. It runs on Linear's AI
45
+ credits; its sandbox is slow, about 15× the video's length at 60 fps.
57
46
 
58
- - Researched from the customer's own website.
59
- - Checked, then filmed at 30 fps.
60
- - Google Fonts stand-ins only.
61
- - That customer only; check before sending.
62
- - Other use needs their written permission.
47
+ For a customer, the video is a **pitch draft**. @Linear researches their
48
+ brand from their own website, the engine checks it and films at 30 fps, and
49
+ the brand is deleted afterwards. Fonts are Google Fonts stand-ins. A pitch
50
+ draft is for that customer only: check it before sending it, and any other
51
+ use needs their written permission.
63
52
 
64
53
  ## Stories and brands
65
54
 
66
- - **Story:** `demo/stories/<name>.json`, scenes as JSON.
67
- - Built only from the mocks.
68
- - One story per app is primary.
69
- - **Translation:** its own story, e.g. `checkout-da.json`.
70
- - Same flow, cards in that language.
71
- - **Brands:** only `navipartner`, shipped with the engine.
72
- - No `demo/brands/` in repos: `check` refuses it.
73
- - Stories never name a brand.
74
- - **Brand needs:** the adapter lists what a brand gives.
75
- - Pitch research fills exactly those.
76
- - The skill has fields and the look.
55
+ A story is `demo/stories/<name>.json`: the scenes of one journey, built only
56
+ from what the e2e mocks return. One story per app is its primary story. A
57
+ translation is a story of its own, such as `checkout-da.json`, with the same
58
+ flow and its cards in that language.
59
+
60
+ Repos keep no brands. `navipartner` ships with the engine, `check` refuses a
61
+ `demo/brands/` folder, and stories never name a brand. The adapter's
62
+ `brandNeeds` lists what a brand gives the app (a background image, a
63
+ secondary colour), which is exactly what a pitch draft researches. The skill
64
+ (`skills/np-demo-video/`) has every field and the look.
77
65
 
78
66
  ## Commands
79
67
 
80
- Run in the app's folder: `pnpm exec demo-video <command>`.
68
+ Run them in the app's folder: `pnpm exec demo-video <command>`.
81
69
 
82
70
  | Command | Does |
83
71
  |---|---|
84
- | `init` | sets up a repo or app |
85
- | `list` | stories, primary first |
86
- | `check` | dry run in seconds, names problems |
87
- | `render` | MP4, contact sheet, ten stills |
88
- | `record` / `encode` / `stills` | render in parts |
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
- | Flag | Means |
94
- |---|---|
95
- | `--story <name>` | default: primary story |
96
- | `--draft` | 30 fps, twice as fast |
97
- | `--app <dir>` | app folder, default `.` |
98
- | `--json` | machine-readable output |
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
72
+ | `init` | sets up a repo or an app |
73
+ | `list` | the stories, primary first |
74
+ | `check` | a dry run in seconds that names what would film badly |
75
+ | `render` | the MP4, a contact sheet and ten stills, in `demo/out/` |
76
+ | `record`, `encode`, `stills` | `render` in parts |
77
+ | `brand-needs` | what a brand must give this app |
78
+ | `pitch <proposal.json>` | a pitch draft from a researched brand |
79
+ | `pick` | what a request means (used by the workflows) |
80
+
81
+ `--story <name>` picks a story (default: the primary one), `--draft` films
82
+ at 30 fps in about half the time, `--app <dir>` names the app folder and
83
+ `--json` gives machine-readable output. When something fails, Playwright's
84
+ output is kept in `demo/out/failed/`.
85
+
86
+ ## Develop and release
104
87
 
105
88
  pnpm install
106
89
  pnpm test && pnpm typecheck && pnpm lint && pnpm build
107
90
 
108
- - CI runs tests, actionlint, smoke render.
109
-
110
- ## Release
111
-
112
- 1. Bump `version` on main.
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.
91
+ CI runs the tests, actionlint and a smoke render of the fixture app. To
92
+ release, bump `version` in `package.json` on main, then
93
+ `git tag vX.Y.Z && git push origin vX.Y.Z`. CI publishes to npm as `latest`
94
+ through trusted publishing (no npm token) and moves the `v0` tag. Running
95
+ `release.yml` by hand publishes a `next` prerelease.
126
96
 
127
- ## Compatibility
97
+ The package is public; this repo is not. `scripts/check-public.mjs` checks
98
+ every tarball for files outside the allowlist, source maps, secrets, emails
99
+ and local paths. It can't catch names, so keep customers and people out of
100
+ everything that ships.
128
101
 
129
- - Projects pin the CLI version.
130
- - Their workflows follow `v0` immediately.
131
- - Workflows must support CLI 0.4+.
132
- - Breaking that means `v1`.
102
+ Projects pin the CLI in their lockfile, but their workflows follow `v0` at
103
+ once. So `render.yml` and `request.yml` must work with every CLI from 0.4;
104
+ anything that can't is `v1`.
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { listStories, parseArgs } from "./args.mjs";
10
10
  import { encodeRecording, extractStillsFrom } from "./encode.mjs";
11
11
  import { initApp, initProject } from "./init.mjs";
12
- import { appMenu, menuMarkdown, pick, pickExact } from "./pick.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
 
@@ -124,35 +124,31 @@ const runInit = (argv) => {
124
124
  return 0;
125
125
  };
126
126
 
127
- // demo-video pick [--request "<text>" | --app <a> --story <s> --brand <b>]
128
- // [--json] <app dir>...: which existing story and brand a request means
129
- // (see pick.mjs). --request reads a sentence (a /demo-video comment); the
130
- // flags are exact names (the Run workflow form).
127
+ // demo-video pick [--request "<text>"] [--json] <app dir>...: which existing
128
+ // story a request means (see pick.mjs): a /demo-video comment, or a request
129
+ // to @Linear. No request: the primary story.
131
130
  const runPick = (argv) => {
132
- const flags = {};
131
+ let request = "";
133
132
  const dirs = [];
134
133
  let json = false;
135
134
  for (let i = 0; i < argv.length; i++) {
136
135
  const arg = argv[i];
137
136
  if (arg === "--json") {
138
137
  json = true;
139
- } else if (["--request", "--app", "--story"].includes(arg)) {
140
- flags[arg.slice(2)] = argv[++i] ?? "";
138
+ } else if (arg === "--request") {
139
+ request = argv[++i] ?? "";
141
140
  } else {
142
141
  dirs.push(arg);
143
142
  }
144
143
  }
145
144
  if (dirs.length === 0) {
146
145
  console.error(
147
- 'Usage: demo-video pick [--request "<text>" | --app <a> --story <s>] [--json] <app dir>...',
146
+ 'Usage: demo-video pick [--request "<text>"] [--json] <app dir>...',
148
147
  );
149
148
  return 2;
150
149
  }
151
150
  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);
151
+ const result = pick(request, apps);
156
152
  const out = result.menu
157
153
  ? {
158
154
  menu: true,
package/bin/init.mjs CHANGED
@@ -22,23 +22,14 @@ const engineVersion = () =>
22
22
 
23
23
  export const CALLER_WORKFLOW = `name: Demo video
24
24
  # The "${LABEL}" label films the stories a PR adds or changes. A comment
25
- # "/demo-video [<story>]" on a PR, or Actions → Demo video → Run workflow,
26
- # films an existing story in NaviPartner's brand. A customer's branding: ask
25
+ # "/demo-video [<story>]" on a PR films an existing story in NaviPartner's
26
+ # brand. A customer's branding: ask
27
27
  # @Linear (the skill's linear.md); repos keep no brands.
28
28
  on:
29
29
  pull_request:
30
30
  types: [labeled, synchronize]
31
31
  issue_comment:
32
32
  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
33
  permissions: {}
43
34
  jobs:
44
35
  render:
@@ -48,15 +39,12 @@ jobs:
48
39
  pull-requests: write
49
40
  uses: navipartner/demo-video/.github/workflows/render.yml@v0
50
41
  request:
51
- if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/demo-video'))
42
+ if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/demo-video')
52
43
  permissions:
53
44
  contents: read
54
45
  pull-requests: write
55
46
  issues: write
56
47
  uses: navipartner/demo-video/.github/workflows/request.yml@v0
57
- with:
58
- app: \${{ inputs.app }}
59
- story: \${{ inputs.story }}
60
48
  `;
61
49
 
62
50
  const read = (file) =>
@@ -257,7 +245,7 @@ export const initRepo = (root, { dryRun = false, label = true } = {}) => {
257
245
  .find(Boolean);
258
246
  if (existing && !existing.includes("request.yml@")) {
259
247
  todo.push(
260
- '.github/workflows/demo-video.yml predates the "request" job (/demo-video comments, the Run workflow form): delete it and re-run init to get the current one.',
248
+ '.github/workflows/demo-video.yml predates the "request" job (/demo-video comments): delete it and re-run init to get the current one.',
261
249
  );
262
250
  }
263
251
  } else {
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
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navipartner/demo-video",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Marketing demo videos of a React app, filmed by Playwright against the app's own e2e mocks.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -143,9 +143,8 @@ end NaviPartner logo · "Self-checkout by NaviPartner"
143
143
  ## 4. Write the story
144
144
 
145
145
  A developer writes stories locally (VS Code, with this skill) and a PR
146
- confirms them. A `/demo-video` PR comment, the Run workflow form and a
147
- request to @Linear only pick an existing story and a brand: they never
148
- write one. A story is
146
+ confirms them. A `/demo-video` PR comment and a request to @Linear only
147
+ pick an existing story: they never write one. A story is
149
148
  `<app>/demo/stories/<name>.json`, named for what it shows (`checkout`,
150
149
  `settings`), and holds no brand: it films in NaviPartner's brand, or in a
151
150
  customer's that @Linear researches for one video. A translation is a story