playrig 0.0.0-stage → 0.1.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.
Files changed (118) hide show
  1. package/AGENTS.md +106 -0
  2. package/LICENSE +37 -0
  3. package/README.md +77 -2
  4. package/bin/playrig.js +4 -0
  5. package/editions/ae/README.md +144 -0
  6. package/editions/ae/client/aeb.js +9 -0
  7. package/editions/ae/examples/hello.jsx +3 -0
  8. package/editions/ae/examples/inspect.jsx +7 -0
  9. package/editions/ae/examples/snap.jsx +15 -0
  10. package/editions/ae/lib/actions.json +159 -0
  11. package/editions/ae/lib/bridge-api.jsxinc +474 -0
  12. package/editions/ae/lib/dev.jsxinc +21 -0
  13. package/editions/ae/lib/icons/README.md +4 -0
  14. package/editions/ae/lib/icons/dot-error.svg +3 -0
  15. package/editions/ae/lib/icons/dot-idle.svg +3 -0
  16. package/editions/ae/lib/icons/dot-running.svg +3 -0
  17. package/editions/ae/lib/icons/make-png.js +25 -0
  18. package/editions/ae/lib/icons/png/dot-error.png +0 -0
  19. package/editions/ae/lib/icons/png/dot-error@2x.png +0 -0
  20. package/editions/ae/lib/icons/png/dot-idle.png +0 -0
  21. package/editions/ae/lib/icons/png/dot-idle@2x.png +0 -0
  22. package/editions/ae/lib/icons/png/dot-running.png +0 -0
  23. package/editions/ae/lib/icons/png/dot-running@2x.png +0 -0
  24. package/editions/ae/lib/icons/png/power-off.png +0 -0
  25. package/editions/ae/lib/icons/png/power-off@2x.png +0 -0
  26. package/editions/ae/lib/icons/png/power-on.png +0 -0
  27. package/editions/ae/lib/icons/png/power-on@2x.png +0 -0
  28. package/editions/ae/lib/icons/png/settings.png +0 -0
  29. package/editions/ae/lib/icons/png/settings@2x.png +0 -0
  30. package/editions/ae/lib/icons/png/status-error.png +0 -0
  31. package/editions/ae/lib/icons/png/status-error@2x.png +0 -0
  32. package/editions/ae/lib/icons/png/status-ok.png +0 -0
  33. package/editions/ae/lib/icons/png/status-ok@2x.png +0 -0
  34. package/editions/ae/lib/icons/png/status-rejected.png +0 -0
  35. package/editions/ae/lib/icons/png/status-rejected@2x.png +0 -0
  36. package/editions/ae/lib/icons/png/status-timeout.png +0 -0
  37. package/editions/ae/lib/icons/png/status-timeout@2x.png +0 -0
  38. package/editions/ae/lib/icons/png/status-unknown.png +0 -0
  39. package/editions/ae/lib/icons/png/status-unknown@2x.png +0 -0
  40. package/editions/ae/lib/icons/power-off.svg +4 -0
  41. package/editions/ae/lib/icons/power-on.svg +4 -0
  42. package/editions/ae/lib/icons/settings.svg +4 -0
  43. package/editions/ae/lib/icons/status-error.svg +4 -0
  44. package/editions/ae/lib/icons/status-ok.svg +4 -0
  45. package/editions/ae/lib/icons/status-rejected.svg +4 -0
  46. package/editions/ae/lib/icons/status-timeout.svg +4 -0
  47. package/editions/ae/lib/icons/status-unknown.svg +5 -0
  48. package/editions/ae/lib/panel-core.jsxinc +249 -0
  49. package/editions/ae/lib/update-key.pem +11 -0
  50. package/editions/ae/lib/updater.jsxinc +148 -0
  51. package/editions/ae/library-starter/INDEX.md +5 -0
  52. package/editions/ae/library-starter/README.md +161 -0
  53. package/editions/ae/library-starter/_common.jsx +187 -0
  54. package/editions/ae/library-starter/categories.json +9 -0
  55. package/editions/ae/library-starter/recipes/.gitkeep +0 -0
  56. package/editions/ae/package.json +7 -0
  57. package/editions/ae/panel/Playrig.jsx +39 -0
  58. package/editions/ae/panel/core.jsx +1026 -0
  59. package/editions/ae/skills/create-ae-recipe/SKILL.md +85 -0
  60. package/editions/ae/skills/create-ae-recipe/references/ae-patterns.md +88 -0
  61. package/editions/ae/skills/create-ae-recipe/references/video-analysis.md +72 -0
  62. package/editions/ae/skills/create-ae-recipe/scripts/media +5 -0
  63. package/editions/ae/skills/create-ae-recipe/scripts/media.py +253 -0
  64. package/editions/ae/skills/create-ae-recipe/scripts/setup.sh +17 -0
  65. package/editions/ae/skills/create-ae-video/SKILL.md +88 -0
  66. package/editions/ae/skills/create-ae-video/references/templates.md +90 -0
  67. package/editions/ae/skills/create-ae-video/scripts/preflight.js +99 -0
  68. package/editions/ae/skills/use-ae-recipes/SKILL.md +99 -0
  69. package/editions/ae/skills/use-ae-recipes/scripts/contact-sheet.jsx +10 -0
  70. package/editions/ae/skills/use-ae-recipes/scripts/edit-text.jsx +12 -0
  71. package/editions/ae/skills/use-ae-recipes/scripts/remove-comp.jsx +18 -0
  72. package/editions/pr/README.md +60 -0
  73. package/editions/pr/SCORE_PROCESS.md +41 -0
  74. package/editions/pr/client/aeb.js +9 -0
  75. package/editions/pr/examples/frame.js +6 -0
  76. package/editions/pr/examples/hello.js +4 -0
  77. package/editions/pr/organize.default.json +245 -0
  78. package/editions/pr/package.json +7 -0
  79. package/editions/pr/plugin/actions.js +79 -0
  80. package/editions/pr/plugin/dev.js +19 -0
  81. package/editions/pr/plugin/icons/README.md +4 -0
  82. package/editions/pr/plugin/icons/file-code.svg +6 -0
  83. package/editions/pr/plugin/icons/folder.svg +3 -0
  84. package/editions/pr/plugin/icons/power-off.svg +4 -0
  85. package/editions/pr/plugin/icons/power-on.svg +4 -0
  86. package/editions/pr/plugin/icons/settings.svg +4 -0
  87. package/editions/pr/plugin/icons/status-error.svg +4 -0
  88. package/editions/pr/plugin/icons/status-ok.svg +4 -0
  89. package/editions/pr/plugin/icons/status-rejected.svg +4 -0
  90. package/editions/pr/plugin/icons/status-timeout.svg +4 -0
  91. package/editions/pr/plugin/icons/status-unknown.svg +5 -0
  92. package/editions/pr/plugin/incremental.js +212 -0
  93. package/editions/pr/plugin/index.html +195 -0
  94. package/editions/pr/plugin/index.js +710 -0
  95. package/editions/pr/plugin/manifest.json +45 -0
  96. package/editions/pr/plugin/organize.js +223 -0
  97. package/editions/pr/skills/score-video/SKILL.md +69 -0
  98. package/editions/pr/skills/score-video/scripts/mix.py +112 -0
  99. package/editions/pr/skills/score-video/scripts/place.js +63 -0
  100. package/lib/ae.js +754 -0
  101. package/lib/common.js +132 -0
  102. package/lib/install.js +148 -0
  103. package/lib/main.js +107 -0
  104. package/lib/payload.js +60 -0
  105. package/lib/pr.js +239 -0
  106. package/lib/update.js +118 -0
  107. package/package.json +35 -4
  108. package/skills/create-video/SKILL.md +72 -0
  109. package/skills/create-video/references/lessons.md +31 -0
  110. package/skills/create-video/references/project-conventions.md +46 -0
  111. package/skills/create-video/scripts/ae/list-project.jsx +8 -0
  112. package/skills/create-video/scripts/ae/move-chips.jsx +14 -0
  113. package/skills/create-video/scripts/ae/organize-project.jsx +25 -0
  114. package/skills/create-video/scripts/ae/quadrant-chips.jsx +143 -0
  115. package/skills/create-video/scripts/ae/render-comps.jsx +16 -0
  116. package/skills/create-video/scripts/premiere/export-sequences.js +22 -0
  117. package/skills/create-video/scripts/premiere/make-sequences.js +24 -0
  118. package/skills/create-video/scripts/premiere/swap-media.js +43 -0
package/lib/common.js ADDED
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+ // Shared by the After Effects and Premiere Pro clients: argument parsing, inbox roots, result files, edition lookup.
3
+ // Node >= 18, no dependencies.
4
+
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
10
+ const BOOL_FLAGS = new Set(['no-undo', 'json', 'no-snap', 'save-preview', 'full', 'yes', 'replace', 'all', 'dry-run', 'no-open', 'check', 'global', 'force', 'help', 'version']);
11
+
12
+ // The two editions. `ext` is the job file extension the panel watches for.
13
+ const APPS = {
14
+ ae: { id: 'ae', name: 'After Effects', product: 'Playrig for After Effects', ext: '.jsx', legacyDir: 'AE Bridge', legacyEnv: 'AE_BRIDGE_ROOT' },
15
+ pr: { id: 'pr', name: 'Premiere Pro', product: 'Playrig for Premiere Pro', ext: '.js', legacyDir: 'Premiere Bridge', legacyEnv: 'PREMIERE_BRIDGE_ROOT' },
16
+ };
17
+
18
+ const USAGE = `Playrig: let a coding agent control After Effects and Premiere Pro.
19
+
20
+ Usage:
21
+ playrig <ae|pr> run <job> [--id <id>] [--action <id>] [--desc "<what it does>"] [--snap "<comp or sequence>@t1,t2,..."]...
22
+ [--inspect "<comp>:<depth>" | "<sequence>"] [--timeout 120] [--no-undo] [--json] [--root <dir>]
23
+ (After Effects jobs are .jsx, Premiere Pro jobs are .js)
24
+ playrig <ae|pr> snap "<comp or sequence>@t1,t2,..." [more specs] export frames without running a job script
25
+ playrig <ae|pr> inspect "<comp>:<depth>" | "<sequence>" report the structure of a comp or sequence
26
+ playrig status <id> [--app ae|pr] result (or running/queued state) of a job
27
+ playrig tail [--app ae|pr] [--all | --project <name>] [--limit 10] latest jobs of the project of the most recent job
28
+ playrig <ae|pr> scratch start | status | clean --yes safe experimenting: remove only what was added since start
29
+ playrig ae lib list|show|run|index|check|new ... the motion-recipe library (After Effects)
30
+ playrig install <ae|pr|skills|all> [--dry-run] install a panel / the agent skills
31
+ playrig update [ae|pr|cli] [--check] check for (and install) newer versions
32
+ playrig where show where the editions, library and inboxes are
33
+
34
+ Short alias: prg (same as playrig).
35
+ Every job has one action: declare it with --action or a first-lines header in the job file:
36
+ // @action layer.add
37
+ // Added three null layers for the camera rig
38
+
39
+ Job inbox: --root, else $PLAYRIG_ROOT/<ae|pr>, else ~/Documents/Playrig/<ae|pr>
40
+ (an existing ~/Documents/AE Bridge or ~/Documents/Premiere Bridge is still used until the new folder exists).
41
+ Old commands (aeb ...) still work and print a deprecation notice.`;
42
+
43
+ function die(msg, code = 2) {
44
+ console.error(msg);
45
+ process.exit(code);
46
+ }
47
+
48
+ function parseArgs(argv) {
49
+ const pos = [];
50
+ const flags = { snap: [], param: [] };
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const a = argv[i];
53
+ if (!a.startsWith('--')) { pos.push(a); continue; }
54
+ const key = a.slice(2);
55
+ if (BOOL_FLAGS.has(key)) { flags[key] = true; continue; }
56
+ if (i + 1 >= argv.length) die(`Missing value for --${key}\n\n${USAGE}`);
57
+ const val = argv[++i];
58
+ if (key === 'snap') flags.snap.push(val);
59
+ else if (key === 'param') flags.param.push(val);
60
+ else flags[key] = val;
61
+ }
62
+ return { pos, flags };
63
+ }
64
+
65
+ const docs = () => path.join(os.homedir(), 'Documents');
66
+
67
+ // <Documents>/Playrig/<app>; the pre-rename folder is kept while it is the only one with an inbox, so upgrades keep working.
68
+ function defaultRoot(app) {
69
+ const fresh = path.join(docs(), 'Playrig', app.id);
70
+ const legacy = path.join(docs(), app.legacyDir);
71
+ if (!fs.existsSync(path.join(fresh, 'inbox')) && fs.existsSync(path.join(legacy, 'inbox'))) return legacy;
72
+ return fresh;
73
+ }
74
+
75
+ function rootDir(app, flags) {
76
+ if (flags.root) return path.resolve(flags.root);
77
+ if (process.env.PLAYRIG_ROOT) return path.resolve(process.env.PLAYRIG_ROOT, app.id);
78
+ if (process.env[app.legacyEnv]) return path.resolve(process.env[app.legacyEnv]);
79
+ return defaultRoot(app);
80
+ }
81
+
82
+ function writeAtomic(file, data) {
83
+ const tmp = file + '.tmp';
84
+ fs.writeFileSync(tmp, data);
85
+ fs.renameSync(tmp, file);
86
+ }
87
+
88
+ function defaultId(jobPath) {
89
+ const d = new Date();
90
+ const p = (n) => String(n).padStart(2, '0');
91
+ const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
92
+ const name = path.basename(jobPath, path.extname(jobPath)).replace(/[^A-Za-z0-9_-]+/g, '-');
93
+ return `${stamp}-${name}`.slice(0, 64);
94
+ }
95
+
96
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
97
+
98
+ function readResult(root, id) {
99
+ const f = path.join(root, 'out', id, 'result.json');
100
+ if (!fs.existsSync(f)) return null;
101
+ return JSON.parse(fs.readFileSync(f, 'utf8'));
102
+ }
103
+
104
+ // ------------------------------------------------------------------ editions
105
+ // An edition lives in <cli>/editions/<id> in the published npm package, or in the sibling package folder in the monorepo.
106
+ const CLI_DIR = path.join(__dirname, '..');
107
+ function editionDir(id) {
108
+ const marker = id === 'ae' ? 'panel' : 'plugin';
109
+ for (const d of [path.join(CLI_DIR, 'editions', id), path.join(CLI_DIR, '..', id)]) {
110
+ if (fs.existsSync(path.join(d, marker))) return d;
111
+ }
112
+ return null;
113
+ }
114
+ function skillsDirs() {
115
+ const out = [];
116
+ for (const d of [path.join(CLI_DIR, 'skills'), ...['ae', 'pr'].map((id) => { const e = editionDir(id); return e && path.join(e, 'skills'); })]) {
117
+ if (d && fs.existsSync(d)) out.push(d);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ // Recipe library: --lib, $PLAYRIG_LIBRARY (or $AE_LIBRARY), the user's ~/Documents/Playrig/library, then a library inside the
123
+ // edition folder (only the source repo has one: no recipes are shipped). Falls back to the user folder so messages can name it.
124
+ const userLibrary = () => path.join(docs(), 'Playrig', 'library');
125
+ function libraryDir(flags = {}) {
126
+ const hasRecipes = (d) => d && fs.existsSync(path.join(d, 'recipes'));
127
+ const ed = editionDir('ae');
128
+ const candidates = [flags.lib, process.env.PLAYRIG_LIBRARY, process.env.AE_LIBRARY, userLibrary(), ed && path.join(ed, 'library')].filter(Boolean).map((d) => path.resolve(d));
129
+ return candidates.find(hasRecipes) || path.resolve(flags.lib || process.env.PLAYRIG_LIBRARY || process.env.AE_LIBRARY || userLibrary());
130
+ }
131
+
132
+ module.exports = { libraryDir, userLibrary, ID_RE, BOOL_FLAGS, APPS, USAGE, die, parseArgs, rootDir, defaultRoot, writeAtomic, defaultId, sleep, readResult, editionDir, skillsDirs, CLI_DIR };
package/lib/install.js ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+ // playrig install <ae|pr|skills|all> and playrig where.
3
+ // Each edition installs on its own: someone with only Premiere Pro never needs the After Effects panel, and the reverse.
4
+
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const cp = require('child_process');
9
+ const C = require('./common');
10
+ const P = require('./payload');
11
+ const { APPS, die } = C;
12
+
13
+ const AE_PANEL = 'Playrig.jsx';
14
+ const AE_LIB = 'Playrig lib';
15
+ const PR_PACKAGE = 'Playrig (PR)';
16
+ const SKIP_COPY = /(^|[\\/])(\.venv|__pycache__|\.DS_Store)([\\/]|$)/;
17
+
18
+ // ------------------------------------------------------------------ where
19
+ function where(pos) {
20
+ const ae = C.editionDir('ae'), pr = C.editionDir('pr');
21
+ const items = {
22
+ ae: ae, pr: pr,
23
+ library: fs.existsSync(path.join(C.libraryDir(), 'recipes')) ? C.libraryDir() : null,
24
+ skills: C.skillsDirs().join(path.delimiter) || null,
25
+ 'inbox:ae': C.rootDir(APPS.ae, {}), 'inbox:pr': C.rootDir(APPS.pr, {}),
26
+ };
27
+ if (pos[0]) {
28
+ if (!(pos[0] in items)) die(`playrig where [${Object.keys(items).join('|')}]`);
29
+ if (!items[pos[0]]) die(`${pos[0]}: not found`, 1);
30
+ console.log(items[pos[0]]);
31
+ return;
32
+ }
33
+ for (const [k, v] of Object.entries(items)) console.log(`${k.padEnd(10)} ${v || '(not found)'}`);
34
+ }
35
+
36
+ // ------------------------------------------------------------------ After Effects
37
+ function aePanelFolders() {
38
+ const out = [];
39
+ const bases = process.platform === 'win32'
40
+ ? [path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Adobe')]
41
+ : process.platform === 'darwin' ? ['/Applications'] : [];
42
+ for (const base of bases) {
43
+ if (!fs.existsSync(base)) continue;
44
+ for (const d of fs.readdirSync(base)) {
45
+ if (!/^Adobe After Effects/.test(d)) continue;
46
+ const rel = process.platform === 'win32' ? path.join('Support Files', 'Scripts', 'ScriptUI Panels') : path.join('Scripts', 'ScriptUI Panels');
47
+ const dir = path.join(base, d, rel);
48
+ if (fs.existsSync(dir)) out.push(dir);
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function installAE(flags) {
55
+ const ed = C.editionDir('ae');
56
+ if (!ed) die('The After Effects edition was not found next to this CLI.', 1);
57
+ const targets = flags.to ? [path.resolve(flags.to)] : aePanelFolders();
58
+ if (!targets.length) {
59
+ console.log('After Effects: no installation found. Pass --to "<ScriptUI Panels folder>" to install into a specific folder.');
60
+ return false;
61
+ }
62
+ // The stub (Playrig.jsx) goes into ScriptUI Panels and loads the payload (Playrig lib/) beside it, or a newer verified one that
63
+ // the panel's updater installs in the user's data folder. The payload is staged in the temp folder so the sudo hint below can read it.
64
+ const stage = path.join(os.tmpdir(), 'playrig-ae-stage');
65
+ const { version } = P.aePayload(path.join(stage, AE_LIB));
66
+ fs.copyFileSync(path.join(ed, 'panel', AE_PANEL), path.join(stage, AE_PANEL));
67
+ let ok = true;
68
+ for (const dir of targets) {
69
+ const panelDest = path.join(dir, AE_PANEL), libDest = path.join(dir, AE_LIB);
70
+ console.log(`After Effects: ${flags['dry-run'] ? 'would install' : 'installing'} Playrig ${version} into ${dir}`);
71
+ if (flags['dry-run']) continue;
72
+ try {
73
+ fs.rmSync(libDest, { recursive: true, force: true });
74
+ fs.cpSync(path.join(stage, AE_LIB), libDest, { recursive: true });
75
+ fs.copyFileSync(path.join(stage, AE_PANEL), panelDest);
76
+ } catch (e) {
77
+ ok = false;
78
+ if (e.code === 'EACCES' || e.code === 'EPERM') {
79
+ console.log(` Not allowed to write there. Run it with admin rights instead:\n sudo rm -rf "${libDest}" && sudo cp -R "${path.join(stage, AE_LIB)}" "${libDest}" && sudo cp "${path.join(stage, AE_PANEL)}" "${dir}/"`);
80
+ } else console.log(` Failed: ${e.message}`);
81
+ }
82
+ const old = ['AE Bridge.jsx', 'AE Bridge lib', 'Playrig (AE).jsx', 'Playrig (AE) lib'].filter((n) => fs.existsSync(path.join(dir, n)));
83
+ if (old.length) console.log(` Note: an older panel is still there (${old.join(', ')}). Delete it so After Effects shows one panel.`);
84
+ }
85
+ if (ok && !flags['dry-run']) fs.rmSync(stage, { recursive: true, force: true });
86
+ if (!flags['dry-run'] && ok) console.log('Restart After Effects, open Window > Playrig, and press Start.\nTurn on Preferences > Scripting & Expressions > "Allow Scripts to Write Files and Access Network" once.\nAfter this, updates install from the panel (Settings > About > Check for Updates) without admin rights.');
87
+ return ok;
88
+ }
89
+
90
+ // ------------------------------------------------------------------ Premiere Pro
91
+ // Premiere panels are UXP plugins. We package the plugin as a .ccx (a zip) and hand it to the system, which opens the Creative Cloud
92
+ // installer. For development, load plugin/manifest.json in the Adobe UXP Developer Tool instead.
93
+ function installPR(flags) {
94
+ const ed = C.editionDir('pr');
95
+ if (!ed) die('The Premiere Pro edition was not found next to this CLI.', 1);
96
+ const outDir = path.join(os.homedir(), 'Documents', 'Playrig');
97
+ const ccx = path.join(outDir, `${PR_PACKAGE}.ccx`);
98
+ console.log(`Premiere Pro: ${flags['dry-run'] ? 'would build' : 'building'} ${ccx}`);
99
+ if (flags['dry-run']) return true;
100
+ try { P.prPackage(ccx); } catch (e) { console.log(` ${e.message}`); return false; }
101
+ console.log(` Built ${ccx}`);
102
+ if (!flags['no-open']) {
103
+ const opener = process.platform === 'darwin' ? ['open', [ccx]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', ccx]] : null;
104
+ if (opener) { cp.spawnSync(opener[0], opener[1], { stdio: 'ignore' }); console.log(' Opened it with the Creative Cloud installer. Confirm the install there.'); }
105
+ }
106
+ console.log([
107
+ 'If the installer refuses an unsigned plugin (or you are developing), load it instead:',
108
+ ' Adobe UXP Developer Tool > Add Plugin > ' + path.join(ed, 'plugin', 'manifest.json') + ' > Load',
109
+ 'Then in Premiere Pro: Window > Extensions > Playrig, and press Start. Premiere asks once for file access.',
110
+ ].join('\n'));
111
+ return true;
112
+ }
113
+
114
+ // ------------------------------------------------------------------ Skills (Claude Code)
115
+ function installSkills(flags) {
116
+ const dirs = C.skillsDirs();
117
+ if (!dirs.length) die('No skills found next to this CLI.', 1);
118
+ const dest = flags.global ? path.join(os.homedir(), '.claude', 'skills') : path.join(process.cwd(), '.claude', 'skills');
119
+ console.log(`Skills: ${flags['dry-run'] ? 'would install' : 'installing'} into ${dest}`);
120
+ for (const d of dirs) {
121
+ for (const name of fs.readdirSync(d)) {
122
+ const from = path.join(d, name);
123
+ if (!fs.statSync(from).isDirectory() || !fs.existsSync(path.join(from, 'SKILL.md'))) continue;
124
+ console.log(` ${name}`);
125
+ if (flags['dry-run']) continue;
126
+ fs.mkdirSync(dest, { recursive: true });
127
+ fs.rmSync(path.join(dest, name), { recursive: true, force: true });
128
+ fs.cpSync(from, path.join(dest, name), { recursive: true, filter: (f) => !SKIP_COPY.test(f) });
129
+ }
130
+ }
131
+ console.log('The agent guide (AGENTS.md) is not a skill: put it, or a CLAUDE.md that points to it, in your project folder.');
132
+ return true;
133
+ }
134
+
135
+ function install(pos, flags) {
136
+ const what = pos[0];
137
+ if (what === 'ae') return installAE(flags);
138
+ if (what === 'pr') return installPR(flags);
139
+ if (what === 'skills') return installSkills(flags);
140
+ if (what === 'all') {
141
+ const results = [C.editionDir('ae') && installAE(flags), C.editionDir('pr') && installPR(flags), installSkills(flags)];
142
+ if (results.includes(false)) process.exitCode = 1;
143
+ return;
144
+ }
145
+ die('Usage: playrig install <ae|pr|skills|all> [--dry-run] [--no-open] [--to <ScriptUI Panels folder>] [--global]', 2);
146
+ }
147
+
148
+ module.exports = { install, where };
package/lib/main.js ADDED
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+ // playrig <ae|pr> <command> ... one CLI for both editions. The per-app code is in ae.js and pr.js.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const C = require('./common');
7
+ const { APPS, USAGE, die, parseArgs } = C;
8
+
9
+ const APP_IDS = Object.keys(APPS);
10
+ const VERSION = require('../package.json').version;
11
+
12
+ function loadApp(id) { return require(id === 'ae' ? './ae' : './pr'); }
13
+
14
+ function newestResult(root) {
15
+ const out = path.join(root, 'out');
16
+ if (!fs.existsSync(out)) return 0;
17
+ let t = 0;
18
+ for (const d of fs.readdirSync(out)) {
19
+ try { t = Math.max(t, fs.statSync(path.join(out, d, 'result.json')).mtimeMs); } catch (e) { /* no result yet */ }
20
+ }
21
+ return t;
22
+ }
23
+
24
+ // Which edition does a job id belong to? Looks in both inboxes (ids are timestamped, so a clash is unlikely).
25
+ function locate(id, flags) {
26
+ return APP_IDS.filter((a) => {
27
+ const app = APPS[a], root = C.rootDir(app, flags);
28
+ return fs.existsSync(path.join(root, 'out', id, 'result.json')) || fs.existsSync(path.join(root, 'running', id + app.ext)) || fs.existsSync(path.join(root, 'inbox', id + app.ext));
29
+ });
30
+ }
31
+
32
+ function appFromFlag(flags) {
33
+ if (flags.app === undefined) return null;
34
+ if (!APPS[flags.app]) die(`--app must be ae or pr (got "${flags.app}")`);
35
+ return flags.app;
36
+ }
37
+
38
+ async function main(argv) {
39
+ const [first, ...rest] = argv;
40
+ if (!first || first === 'help' || first === '--help' || first === '-h') { console.log(USAGE); return; }
41
+ if (first === '--version' || first === '-v' || first === 'version') { console.log(`playrig ${VERSION}`); return; }
42
+
43
+ // playrig ae|pr <command> ...
44
+ if (APPS[first]) {
45
+ const [sub, ...args] = rest;
46
+ if (!sub) die(USAGE);
47
+ const { pos, flags } = parseArgs(args);
48
+ return loadApp(first).main(sub, pos, flags);
49
+ }
50
+
51
+ const { pos, flags } = parseArgs(rest);
52
+
53
+ if (first === 'install') return require('./install').install(pos, flags);
54
+ if (first === 'where') return require('./install').where(pos);
55
+ if (first === 'update') return require('./update').update(pos, flags);
56
+
57
+ // Commands without an app: status and tail find it, run infers it from the file extension.
58
+ if (first === 'status') {
59
+ const id = pos[0];
60
+ if (!id || !C.ID_RE.test(id)) die(`Missing or bad <id>\n\n${USAGE}`);
61
+ let app = appFromFlag(flags);
62
+ if (!app) {
63
+ const hits = flags.root ? [] : locate(id, flags);
64
+ if (flags.root) die('With --root, say which app: playrig ae status <id> --root ... (or --app ae|pr)');
65
+ if (hits.length > 1) die(`Job "${id}" exists for both After Effects and Premiere Pro. Add --app ae or --app pr.`);
66
+ app = hits[0] || null;
67
+ if (!app) die(`Unknown job "${id}" (looked in both inboxes). Use --app ae|pr or --root if your inbox is elsewhere.`, 1);
68
+ }
69
+ return loadApp(app).main('status', pos, flags);
70
+ }
71
+
72
+ if (first === 'tail') {
73
+ const app = appFromFlag(flags);
74
+ if (app) return loadApp(app).main('tail', pos, flags);
75
+ if (flags.root) die('With --root, say which app: --app ae|pr');
76
+ const times = APP_IDS.map((a) => ({ a, t: newestResult(C.rootDir(APPS[a], flags)) })).filter((x) => x.t > 0).sort((x, y) => y.t - x.t);
77
+ if (!times.length) { console.log('No results yet in either inbox.'); return; }
78
+ // --all lists every project of every edition that has results; otherwise the edition with the latest job.
79
+ const shown = flags.all ? times : times.slice(0, 1);
80
+ for (const x of shown) { console.log(`== ${APPS[x.a].product}`); await loadApp(x.a).main('tail', pos, flags); }
81
+ return;
82
+ }
83
+
84
+ if (first === 'run') {
85
+ let app = appFromFlag(flags);
86
+ if (!app && pos[0]) {
87
+ const ext = path.extname(pos[0]).toLowerCase();
88
+ app = ext === '.jsx' || ext === '.jsxbin' ? 'ae' : ext === '.js' || ext === '.mjs' ? 'pr' : null;
89
+ }
90
+ if (!app) die('Say which app: playrig ae run <job.jsx> or playrig pr run <job.js>');
91
+ return loadApp(app).main('run', pos, flags);
92
+ }
93
+
94
+ return die(`Unknown command "${first}"\n\n${USAGE}`);
95
+ }
96
+
97
+ // Entry for the deprecated `aeb` clients that still sit at <edition>/client/aeb.js: print one line, then forward.
98
+ function legacy(app, argv) {
99
+ console.error(`aeb is deprecated: use \`playrig ${app} ${argv[0] || '<command>'}\` (forwarding)`);
100
+ return run([app, ...argv]);
101
+ }
102
+
103
+ function run(argv) {
104
+ return main(argv).catch((e) => die(`playrig: ${e.message}`));
105
+ }
106
+
107
+ module.exports = { main, run, legacy };
package/lib/payload.js ADDED
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ // Builds what gets installed or released for each edition. Shared by `playrig install`, `playrig update` and scripts/release.js.
3
+ // AE payload: a flat folder (core.jsx, the lib files, icons/png, version.json). The small stub panel in After Effects' ScriptUI
4
+ // Panels folder loads it, either the copy installed beside the stub or a newer verified copy in the user's data folder.
5
+ // PR package: the UXP plugin as a .ccx (a zip), without the developer-only dev.js.
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+ const cp = require('child_process');
10
+ const C = require('./common');
11
+
12
+ // Bump when the stub's contract with core.jsx changes; a payload that needs a newer stub is not installed by an older one.
13
+ const STUB_API = 1;
14
+ const SKIP = /(^|[\\/])(\.venv|__pycache__|\.DS_Store)([\\/]|$)/;
15
+
16
+ // LICENSE sits at the repo root in the monorepo and in the CLI package when published.
17
+ const licenseFile = () => [path.join(C.CLI_DIR, '..', '..', 'LICENSE'), path.join(C.CLI_DIR, 'LICENSE')].find((f) => fs.existsSync(f));
18
+ const readJSON = (f) => JSON.parse(fs.readFileSync(f, 'utf8'));
19
+ const editionVersion = (id) => readJSON(path.join(C.editionDir(id), 'package.json')).version;
20
+
21
+ function aePayload(dest, versionOverride) {
22
+ const ed = C.editionDir('ae');
23
+ if (!ed) throw new Error('The After Effects edition was not found next to this CLI.');
24
+ const version = versionOverride || editionVersion('ae');
25
+ fs.rmSync(dest, { recursive: true, force: true });
26
+ fs.mkdirSync(dest, { recursive: true });
27
+ fs.copyFileSync(path.join(ed, 'panel', 'core.jsx'), path.join(dest, 'core.jsx'));
28
+ const keep = (src) => !SKIP.test(src) && !/dev\.jsxinc$/.test(src) && !/make-png\.js$/.test(src) && !/icons[\\/][^\\/]+\.(svg|md)$/.test(src);
29
+ fs.cpSync(path.join(ed, 'lib'), dest, { recursive: true, filter: keep });
30
+ if (licenseFile()) fs.copyFileSync(licenseFile(), path.join(dest, 'LICENSE.txt'));
31
+ fs.writeFileSync(path.join(dest, 'version.json'), JSON.stringify({ version, stubApi: STUB_API }, null, 2) + '\n');
32
+ return { version, dir: dest };
33
+ }
34
+
35
+ // Zips the *contents* of a folder into `outFile`.
36
+ function zipFolder(dir, outFile) {
37
+ fs.rmSync(outFile, { force: true });
38
+ const r = process.platform === 'win32'
39
+ ? cp.spawnSync('powershell', ['-NoProfile', '-Command', `Compress-Archive -Path "${dir}\\*" -DestinationPath "${outFile}.zip" -Force; Move-Item -Force "${outFile}.zip" "${outFile}"`], { encoding: 'utf8' })
40
+ : cp.spawnSync('zip', ['-qr', outFile, '.'], { cwd: dir, encoding: 'utf8' });
41
+ if (r.status !== 0) throw new Error(`could not create ${outFile} (${(r.stderr || r.error || '').toString().trim() || 'zip not available'})`);
42
+ }
43
+
44
+ function prPackage(outFile) {
45
+ const ed = C.editionDir('pr');
46
+ if (!ed) throw new Error('The Premiere Pro edition was not found next to this CLI.');
47
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'playrig-pr-'));
48
+ try {
49
+ fs.cpSync(path.join(ed, 'plugin'), stage, { recursive: true, filter: (f) => !SKIP.test(f) && !/[\\/]dev\.js$/.test(f) && !/icons[\\/]README\.md$/.test(f) });
50
+ // dev.js is developer-only (see packages/pr/DEV_ONLY.md): leave it and its <script> line out of the package.
51
+ if (licenseFile()) fs.copyFileSync(licenseFile(), path.join(stage, 'LICENSE.txt'));
52
+ const html = path.join(stage, 'index.html');
53
+ fs.writeFileSync(html, fs.readFileSync(html, 'utf8').split('\n').filter((l) => !/src="dev\.js"/.test(l)).join('\n'));
54
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
55
+ zipFolder(stage, outFile);
56
+ } finally { fs.rmSync(stage, { recursive: true, force: true }); }
57
+ return { version: readJSON(path.join(ed, 'plugin', 'manifest.json')).version, file: outFile };
58
+ }
59
+
60
+ module.exports = { STUB_API, aePayload, prPackage, zipFolder, editionVersion, readJSON };