Skip to content

Commit 6d39b82

Browse files
authored
Merge pull request #11188 from remotion-dev/codex/studio-install-skills
`@remotion/studio`: Add skill installation to settings
2 parents 30d195a + 896dc73 commit 6d39b82

10 files changed

Lines changed: 350 additions & 12 deletions

File tree

packages/cli/src/skills.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const skillsCommand = (
5151
subcommand === 'add'
5252
? [
5353
'--loglevel=error',
54-
'skills@1.5.20',
54+
'skills@1.5.26',
5555
'add',
5656
'remotion-dev/skills',
5757
...restArgs,

packages/cli/src/test/skills.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ appendFileSync(process.env.REMOTION_SKILLS_TEST_OUTPUT, JSON.stringify(process.a
100100

101101
expect(addArguments).toEqual([
102102
'--loglevel=error',
103-
'skills@1.5.20',
103+
'skills@1.5.26',
104104
'add',
105105
'remotion-dev/skills',
106106
'--yes',

packages/create-video/src/install-skills.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const installSkills = async (projectRoot: string) => {
77
try {
88
await execa(
99
command,
10-
['-y', '--loglevel=error', 'skills@1.5.20', 'add', 'remotion-dev/skills'],
10+
['-y', '--loglevel=error', 'skills@1.5.26', 'add', 'remotion-dev/skills'],
1111
{
1212
cwd: projectRoot,
1313
stdio: 'inherit',

packages/studio-server/src/preview-server/api-routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import {duplicateJsxNodeHandler} from './routes/duplicate-jsx-node';
2727
import {findInFileHandler} from './routes/find-in-file';
2828
import {insertElementHandler} from './routes/insert-element';
2929
import {insertJsxElementHandler} from './routes/insert-jsx-element';
30+
import {
31+
installRemotionSkillHandler,
32+
removeRemotionSkillHandler,
33+
} from './routes/install-remotion-skill';
3034
import {invalidateBundleHandler} from './routes/invalidate-bundle';
3135
import {logStudioErrorHandler} from './routes/log-studio-error';
3236
import {moveKeyframesHandler} from './routes/move-keyframes';
@@ -127,6 +131,8 @@ export const allApiRoutes: {
127131
'/api/update-available': handleUpdate,
128132
'/api/release-notes': getReleaseNotesHandler,
129133
'/api/remotion-skills-info': remotionSkillsInfoHandler,
134+
'/api/install-remotion-skill': installRemotionSkillHandler,
135+
'/api/remove-remotion-skill': removeRemotionSkillHandler,
130136
'/api/project-info': projectInfoHandler,
131137
'/api/delete-static-file': deleteStaticFileHandler,
132138
'/api/rename-static-file': renameStaticFileHandler,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {spawn} from 'node:child_process';
2+
import {RenderInternals} from '@remotion/renderer';
3+
import type {
4+
GetRemotionSkillsInfoResponse,
5+
InstallRemotionSkillRequest,
6+
RemoveRemotionSkillRequest,
7+
} from '@remotion/studio-shared';
8+
import {getPackageManagerSpawnOptions} from '../../helpers/package-manager-spawn-options';
9+
import {remotionSkillNames} from '../../remotion-skill-names';
10+
import type {ApiHandler} from '../api-types';
11+
import {getPackageManager} from '../get-package-manager';
12+
import {getRemotionSkillsInfo} from './remotion-skills-info';
13+
14+
const changingSkillsInProjects = new Set<string>();
15+
16+
const changeRemotionSkill = async ({
17+
action,
18+
remotionRoot,
19+
input: {skill},
20+
logLevel,
21+
}: Parameters<
22+
ApiHandler<InstallRemotionSkillRequest, GetRemotionSkillsInfoResponse>
23+
>[0] & {
24+
readonly action: 'install' | 'remove';
25+
}): Promise<GetRemotionSkillsInfoResponse> => {
26+
if (!remotionSkillNames.some((name) => name === skill)) {
27+
throw new Error(`Unknown Remotion skill: ${JSON.stringify(skill)}`);
28+
}
29+
30+
if (changingSkillsInProjects.has(remotionRoot)) {
31+
throw new Error(
32+
'A skill is already being installed or removed. Please try again once it finishes.',
33+
);
34+
}
35+
36+
changingSkillsInProjects.add(remotionRoot);
37+
try {
38+
const installedSkill = getRemotionSkillsInfo({remotionRoot}).skills.find(
39+
({name}) => name === skill,
40+
);
41+
const removingGlobally =
42+
action === 'remove' &&
43+
installedSkill?.installedInProject === false &&
44+
installedSkill.installedGlobally;
45+
if (
46+
action === 'remove' &&
47+
!removingGlobally &&
48+
!installedSkill?.installedInProject
49+
) {
50+
throw new Error(`${skill} is not installed.`);
51+
}
52+
53+
const packageManager = getPackageManager({
54+
remotionRoot,
55+
packageManager: undefined,
56+
dirUp: 0,
57+
logLevel,
58+
});
59+
const useBunx =
60+
packageManager !== 'unknown' && packageManager.manager === 'bun';
61+
const executable = useBunx
62+
? 'bunx'
63+
: process.platform === 'win32'
64+
? 'npx.cmd'
65+
: 'npx';
66+
const commandArgs =
67+
action === 'install'
68+
? ['add', `remotion-dev/skills@${skill}`, '--yes']
69+
: ['remove', ...(removingGlobally ? ['--global'] : []), skill, '--yes'];
70+
const args = useBunx
71+
? ['--silent', 'skills@1.5.26', ...commandArgs]
72+
: ['--yes', '--loglevel=error', 'skills@1.5.26', ...commandArgs];
73+
RenderInternals.Log.info(
74+
{indent: false, logLevel},
75+
RenderInternals.chalk.gray(`╭─ ${executable} ${args.join(' ')}`),
76+
);
77+
const time = Date.now();
78+
try {
79+
await new Promise<void>((resolve, reject) => {
80+
const child = spawn(executable, args, {
81+
cwd: remotionRoot,
82+
env: {...process.env, DISABLE_TELEMETRY: '1'},
83+
stdio: ['ignore', 'pipe', 'pipe'],
84+
...getPackageManagerSpawnOptions(),
85+
});
86+
let output = '';
87+
const onData = (data: Buffer) => {
88+
output = (output + data.toString()).slice(-8000);
89+
data
90+
.toString()
91+
.trim()
92+
.split('\n')
93+
.forEach((line) =>
94+
RenderInternals.Log.info({indent: true, logLevel}, line),
95+
);
96+
};
97+
98+
child.stdout.on('data', onData);
99+
child.stderr.on('data', onData);
100+
child.on('error', reject);
101+
child.on('close', (code, signal) => {
102+
if (code === 0) {
103+
resolve();
104+
} else {
105+
reject(
106+
new Error(
107+
`Could not ${action} ${skill} (exit code ${code}, signal ${signal}). ${output.trim()}`,
108+
),
109+
);
110+
}
111+
});
112+
});
113+
114+
const info = getRemotionSkillsInfo({remotionRoot});
115+
const updatedSkill = info.skills.find(({name}) => name === skill);
116+
if (action === 'install' && !updatedSkill?.installedInProject) {
117+
throw new Error(
118+
`The installer finished, but ${skill} was not found in the project. Please try again.`,
119+
);
120+
}
121+
122+
if (
123+
action === 'remove' &&
124+
(removingGlobally
125+
? updatedSkill?.installedGlobally
126+
: updatedSkill?.installedInProject)
127+
) {
128+
throw new Error(
129+
`The remover finished, but ${skill} is still installed ${removingGlobally ? 'globally' : 'in the project'}. Please try again.`,
130+
);
131+
}
132+
133+
RenderInternals.Log.info(
134+
{indent: false, logLevel},
135+
RenderInternals.chalk.gray('╰─ '),
136+
`Done in ${Date.now() - time}ms`,
137+
);
138+
return info;
139+
} catch (error) {
140+
RenderInternals.Log.info(
141+
{indent: false, logLevel},
142+
RenderInternals.chalk.gray('╰─ '),
143+
RenderInternals.chalk.red(`Errored in ${Date.now() - time}ms`),
144+
);
145+
throw error;
146+
}
147+
} finally {
148+
changingSkillsInProjects.delete(remotionRoot);
149+
}
150+
};
151+
152+
export const installRemotionSkillHandler: ApiHandler<
153+
InstallRemotionSkillRequest,
154+
GetRemotionSkillsInfoResponse
155+
> = (input) => {
156+
return changeRemotionSkill({...input, action: 'install'});
157+
};
158+
159+
export const removeRemotionSkillHandler: ApiHandler<
160+
RemoveRemotionSkillRequest,
161+
GetRemotionSkillsInfoResponse
162+
> = (input) => {
163+
return changeRemotionSkill({...input, action: 'remove'});
164+
};

packages/studio-shared/src/api-requests.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,12 @@ export type GetReleaseNotesResponse = {
11441144
};
11451145

11461146
export type GetRemotionSkillsInfoRequest = {};
1147+
export type InstallRemotionSkillRequest = {
1148+
skill: string;
1149+
};
1150+
export type RemoveRemotionSkillRequest = {
1151+
skill: string;
1152+
};
11471153
export type GetRemotionSkillsInfoResponse = {
11481154
remotionUpgradeSkillAvailable: boolean;
11491155
remotionInteractivitySkillAvailable: boolean;
@@ -1426,6 +1432,14 @@ export type ApiRoutes = {
14261432
GetRemotionSkillsInfoRequest,
14271433
GetRemotionSkillsInfoResponse
14281434
>;
1435+
'/api/install-remotion-skill': ReqAndRes<
1436+
InstallRemotionSkillRequest,
1437+
GetRemotionSkillsInfoResponse
1438+
>;
1439+
'/api/remove-remotion-skill': ReqAndRes<
1440+
RemoveRemotionSkillRequest,
1441+
GetRemotionSkillsInfoResponse
1442+
>;
14291443
'/api/apply-codemod': ReqAndRes<ApplyCodemodRequest, ApplyCodemodResponse>;
14301444
'/api/project-info': ReqAndRes<ProjectInfoRequest, ProjectInfoResponse>;
14311445
'/api/delete-static-file': ReqAndRes<

packages/studio-shared/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export {
6767
GetDefaultEditorInfoResponse,
6868
GetRemotionSkillsInfoRequest,
6969
GetRemotionSkillsInfoResponse,
70+
InstallRemotionSkillRequest,
71+
RemoveRemotionSkillRequest,
7072
GetReleaseNotesRequest,
7173
GetReleaseNotesResponse,
7274
GoogleFontSourceEdit,

packages/studio/src/components/SettingsContext.tsx

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ import React, {
1616
import {getBrowserStudioOperations} from '../helpers/browser-studio-operations';
1717
import {StudioServerConnectionCtx} from '../helpers/client-id';
1818
import {callApi} from './call-api';
19+
import {showNotification} from './Notifications/NotificationCenter';
1920
import {UpdateStatusProvider} from './UpdateStatusContext';
2021

22+
type SkillAction = {
23+
readonly skill: string;
24+
readonly type: 'installing' | 'removing';
25+
};
26+
2127
type SettingsContextValue = {
2228
readonly codingAgentInfo: GetDefaultCodingAgentInfoResponse | null;
2329
readonly editorInfo: GetDefaultEditorInfoResponse | null;
@@ -28,6 +34,10 @@ type SettingsContextValue = {
2834
readonly studioRuntimeConfig: StudioRuntimeConfig | null;
2935
readonly revision: number;
3036
readonly setPublicLicenseKey: (publicLicenseKey: string | null) => void;
37+
readonly installSkill: (skill: string) => Promise<void>;
38+
readonly removeSkill: (skill: string) => Promise<void>;
39+
readonly skillAction: SkillAction | null;
40+
readonly skillActionError: string | null;
3141
};
3242

3343
const SettingsContext = createContext<SettingsContextValue | null>(null);
@@ -39,7 +49,10 @@ export const SettingsProvider: React.FC<{
3949
StudioServerConnectionCtx,
4050
);
4151
const [settings, setSettings] = useState<
42-
Omit<SettingsContextValue, 'setPublicLicenseKey'>
52+
Omit<
53+
SettingsContextValue,
54+
'setPublicLicenseKey' | 'installSkill' | 'removeSkill'
55+
>
4356
>({
4457
codingAgentInfo: null,
4558
editorInfo: null,
@@ -49,6 +62,8 @@ export const SettingsProvider: React.FC<{
4962
renderDefaults: window.remotion_renderDefaults ?? null,
5063
studioRuntimeConfig: window.remotion_studioConfig ?? null,
5164
revision: 0,
65+
skillAction: null,
66+
skillActionError: null,
5267
});
5368

5469
useEffect(() => {
@@ -147,9 +162,59 @@ export const SettingsProvider: React.FC<{
147162
};
148163
});
149164
}, []);
165+
const installSkill = useCallback(async (skill: string) => {
166+
setSettings((currentSettings) => ({
167+
...currentSettings,
168+
skillAction: {skill, type: 'installing'},
169+
skillActionError: null,
170+
}));
171+
try {
172+
const remotionSkillsInfo = await callApi('/api/install-remotion-skill', {
173+
skill,
174+
});
175+
setSettings((currentSettings) => ({
176+
...currentSettings,
177+
remotionSkillsInfo,
178+
skillAction: null,
179+
revision: currentSettings.revision + 1,
180+
}));
181+
showNotification(`Installed ${skill}.`, 5000);
182+
} catch (err) {
183+
setSettings((currentSettings) => ({
184+
...currentSettings,
185+
skillAction: null,
186+
skillActionError: (err as Error).message,
187+
}));
188+
}
189+
}, []);
190+
const removeSkill = useCallback(async (skill: string) => {
191+
setSettings((currentSettings) => ({
192+
...currentSettings,
193+
skillAction: {skill, type: 'removing'},
194+
skillActionError: null,
195+
}));
196+
try {
197+
const remotionSkillsInfo = await callApi('/api/remove-remotion-skill', {
198+
skill,
199+
});
200+
setSettings((currentSettings) => ({
201+
...currentSettings,
202+
remotionSkillsInfo,
203+
skillAction: null,
204+
revision: currentSettings.revision + 1,
205+
}));
206+
showNotification(`Removed ${skill}.`, 5000);
207+
} catch (err) {
208+
setSettings((currentSettings) => ({
209+
...currentSettings,
210+
skillAction: null,
211+
skillActionError: (err as Error).message,
212+
}));
213+
}
214+
}, []);
150215
const value = useMemo<SettingsContextValue>(() => {
151-
return {...settings, setPublicLicenseKey};
152-
}, [setPublicLicenseKey, settings]);
216+
return {...settings, setPublicLicenseKey, installSkill, removeSkill};
217+
}, [installSkill, removeSkill, setPublicLicenseKey, settings]);
153218

154219
return (
155220
<SettingsContext.Provider value={value}>

packages/studio/src/components/SettingsModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,10 @@ export const SettingsModal: React.FC<{
348348
</div>
349349
{tab === 'packages' ? (
350350
<div ref={setPackagesFooterContainer} />
351-
) : isBrowserStudio || tab === 'models' || tab === 'updates' ? null : (
351+
) : isBrowserStudio ||
352+
tab === 'models' ||
353+
tab === 'updates' ||
354+
tab === 'skills' ? null : (
352355
<SettingsModalFooter showLicenseFaq={tab === 'license'} />
353356
)}
354357
</>

0 commit comments

Comments
 (0)