@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 +72 -100
- package/bin/demo-video.mjs +9 -13
- package/bin/init.mjs +4 -16
- package/bin/pick.d.mts +0 -4
- package/bin/pick.mjs +0 -39
- package/package.json +1 -1
- package/skills/np-demo-video/SKILL.md +2 -3
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
|
-
|
|
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+.
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
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
|
-
|
|
|
27
|
+
| Where | Ask | For |
|
|
27
28
|
|---|---|---|
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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`.
|
package/bin/demo-video.mjs
CHANGED
|
@@ -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
|
|
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>"
|
|
128
|
-
//
|
|
129
|
-
//
|
|
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
|
-
|
|
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 (
|
|
140
|
-
|
|
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>"
|
|
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
|
|
26
|
-
#
|
|
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 == '
|
|
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
|
|
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
|
@@ -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
|
|
147
|
-
|
|
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
|