@coppsary/motionly 1.1.0 → 1.1.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 +11 -10
- package/bin/ffmpeg-export.js +263 -0
- package/bin/motionly.js +21 -13
- package/dist/assets/{main-CrcFUwRU.js → main-Bhr-QmYm.js} +2 -2
- package/dist/assets/main-Bhr-QmYm.js.map +1 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/templates/motionly-skill/SKILL.md +1 -1
- package/templates/project/AGENTS.md +2 -2
- package/templates/project/README.md +10 -2
- package/dist/assets/main-CrcFUwRU.js.map +0 -1
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ Open Motionly Assistant beside Assets, enter your own provider key, and describe
|
|
|
210
210
|
|
|
211
211
|
**Model quality matters.** The AI model you choose directly affects composition, timing, and correct preset use. Smaller models may generate valid syntax but weaker visual decisions. Prefer a current, high-capability model with strong code-generation and instruction-following performance.
|
|
212
212
|
|
|
213
|
-
For coding agents working inside a project, install the skill with `npx motionly skills add` (or `npx motionly init`, which asks which agent). This writes the `SKILL.md` contract plus a full `references/` library into your agent's folder — Codex (`.agents/`), Claude Code (`.claude/`), Gemini CLI (`.gemini/`), opencode (`.opencode/`), or Kiro (`.kiro/`):
|
|
213
|
+
For coding agents working inside a project, install the skill with `npx @coppsary/motionly skills add` (or `npx @coppsary/motionly init`, which asks which agent). This writes the `SKILL.md` contract plus a full `references/` library into your agent's folder — Codex (`.agents/`), Claude Code (`.claude/`), Gemini CLI (`.gemini/`), opencode (`.opencode/`), or Kiro (`.kiro/`):
|
|
214
214
|
|
|
215
215
|
| Path | Purpose |
|
|
216
216
|
|---|---|
|
|
@@ -223,6 +223,7 @@ Point the agent at `AGENTS.md` and the installed `SKILL.md`, then let it load `r
|
|
|
223
223
|
Use this short prompt with an LLM or agent working inside the repository:
|
|
224
224
|
|
|
225
225
|
```text
|
|
226
|
+
/motionly
|
|
226
227
|
Read AGENTS.md and .agents/skills/write-motionly/SKILL.md first.
|
|
227
228
|
Inspect my assets, storyboard the animation, then create a valid .motion project.
|
|
228
229
|
Use only supported Motionly syntax and presets. Keep one focal subject per shot,
|
|
@@ -325,7 +326,7 @@ Motionly ships on npm — no clone and no global install. It needs Node.js `20.1
|
|
|
325
326
|
### Create a project
|
|
326
327
|
|
|
327
328
|
```bash
|
|
328
|
-
npx motionly init demo
|
|
329
|
+
npx @coppsary/motionly init demo
|
|
329
330
|
```
|
|
330
331
|
|
|
331
332
|
`init` scaffolds the project and asks **which agent you're using**, then installs the Motionly agent skill for just that one (or choose "All supported agents"). Supported agents: **Codex, Claude Code, Gemini CLI, opencode, and Kiro**. The install is not a single file: it writes the `SKILL.md` contract plus a full `references/` library (`motion-dsl`, `svg`, `animation`, `timeline`, and more) with an `llms.txt` discovery index.
|
|
@@ -333,9 +334,9 @@ npx motionly init demo
|
|
|
333
334
|
Skip the prompt with flags:
|
|
334
335
|
|
|
335
336
|
```bash
|
|
336
|
-
npx motionly init demo --provider opencode # install for one agent, no prompt
|
|
337
|
-
npx motionly init demo --all # every supported agent
|
|
338
|
-
npx motionly init demo --skip-skills # no agent skills
|
|
337
|
+
npx @coppsary/motionly init demo --provider opencode # install for one agent, no prompt
|
|
338
|
+
npx @coppsary/motionly init demo --all # every supported agent
|
|
339
|
+
npx @coppsary/motionly init demo --skip-skills # no agent skills
|
|
339
340
|
```
|
|
340
341
|
|
|
341
342
|
Provider names are `codex`, `claude`, `gemini`, `opencode`, and `kiro`. Use `--scope project` (default) or `--scope global` to install for every project on your machine.
|
|
@@ -354,9 +355,9 @@ demo/
|
|
|
354
355
|
### Add skills to an existing project
|
|
355
356
|
|
|
356
357
|
```bash
|
|
357
|
-
npx motionly skills add # pick scope and agents
|
|
358
|
-
npx motionly skills add --all --scope project
|
|
359
|
-
npx motionly skills add --provider codex --scope global
|
|
358
|
+
npx @coppsary/motionly skills add # pick scope and agents
|
|
359
|
+
npx @coppsary/motionly skills add --all --scope project
|
|
360
|
+
npx @coppsary/motionly skills add --provider codex --scope global
|
|
360
361
|
```
|
|
361
362
|
|
|
362
363
|
Re-running is safe: existing skill files are kept, never overwritten.
|
|
@@ -365,10 +366,10 @@ Re-running is safe: existing skill files are kept, never overwritten.
|
|
|
365
366
|
|
|
366
367
|
```bash
|
|
367
368
|
cd demo
|
|
368
|
-
npx motionly dev
|
|
369
|
+
npx @coppsary/motionly dev
|
|
369
370
|
```
|
|
370
371
|
|
|
371
|
-
Motionly opens `http://localhost:4173/editor`, loads `project.motion`, serves media from `assets/`, and saves editor changes back to the project. Add `--port <n>` to change the port or `--no-open` to skip launching the browser. For the no-setup browser editor, run `npx motionly`.
|
|
372
|
+
Motionly opens `http://localhost:4173/editor`, loads `project.motion`, serves media from `assets/`, and saves editor changes back to the project. Add `--port <n>` to change the port or `--no-open` to skip launching the browser. For the no-setup browser editor, run `npx @coppsary/motionly`.
|
|
372
373
|
|
|
373
374
|
## Development from source
|
|
374
375
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
5
|
+
import { mkdtemp, rm, stat } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { Transform } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
|
|
11
|
+
const jobs = new Map();
|
|
12
|
+
const MAX_FRAME_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
const MAX_AUDIO_BYTES = 1024 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/** Handle an export request in the standalone npm CLI server. */
|
|
16
|
+
export async function handleFfmpegExportRequest(request, response) {
|
|
17
|
+
const url = new URL(request.url ?? '/', 'http://motionly.local');
|
|
18
|
+
if (!url.pathname.startsWith('/api/exports')) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await handleExportRequest(request, response, url);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (response.headersSent) {
|
|
24
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
25
|
+
} else {
|
|
26
|
+
response.statusCode = 500;
|
|
27
|
+
response.setHeader('content-type', 'text/plain;charset=utf-8');
|
|
28
|
+
response.end(error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Connect-compatible middleware used by the Vite development and preview servers. */
|
|
35
|
+
export function createFfmpegExportMiddleware() {
|
|
36
|
+
return (request, response, next) => {
|
|
37
|
+
void handleFfmpegExportRequest(request, response).then((handled) => {
|
|
38
|
+
if (!handled) next();
|
|
39
|
+
}, next);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handleExportRequest(request, response, url) {
|
|
44
|
+
if (request.method === 'POST' && url.pathname === '/api/exports') {
|
|
45
|
+
await createJob(request, response);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const match = /^\/api\/exports\/([a-z0-9-]+)(?:\/(frames\/(\d+)|audio|finish))?$/.exec(
|
|
49
|
+
url.pathname
|
|
50
|
+
);
|
|
51
|
+
if (!match) return respond(response, 404, 'Unknown export endpoint');
|
|
52
|
+
const id = match[1];
|
|
53
|
+
const action = match[2];
|
|
54
|
+
if (!id) return respond(response, 404, 'Unknown export job');
|
|
55
|
+
const job = jobs.get(id);
|
|
56
|
+
if (!job) return respond(response, 404, 'Export job not found');
|
|
57
|
+
|
|
58
|
+
if (request.method === 'DELETE' && !action) {
|
|
59
|
+
jobs.delete(id);
|
|
60
|
+
await rm(job.directory, { recursive: true, force: true });
|
|
61
|
+
respond(response, 204);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (request.method === 'PUT' && action?.startsWith('frames/')) {
|
|
66
|
+
const index = Number(match[3]);
|
|
67
|
+
if (!Number.isInteger(index) || index < 0 || index >= job.totalFrames) {
|
|
68
|
+
return respond(response, 409, `Invalid frame ${index}`);
|
|
69
|
+
}
|
|
70
|
+
if (job.receivedFrames.has(index)) return respond(response, 409, `Duplicate frame ${index}`);
|
|
71
|
+
if (request.headers['content-type'] !== 'image/jpeg') {
|
|
72
|
+
return respond(response, 415, 'Export frames must be JPEG images');
|
|
73
|
+
}
|
|
74
|
+
await writeRequest(
|
|
75
|
+
request,
|
|
76
|
+
join(job.directory, `frame-${String(index).padStart(8, '0')}.jpg`),
|
|
77
|
+
MAX_FRAME_BYTES
|
|
78
|
+
);
|
|
79
|
+
job.receivedFrames.add(index);
|
|
80
|
+
respond(response, 204);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (request.method === 'PUT' && action === 'audio') {
|
|
85
|
+
if (!job.hasAudio) return respond(response, 409, 'This export was not created with audio');
|
|
86
|
+
await writeRequest(request, join(job.directory, 'audio-input'), MAX_AUDIO_BYTES);
|
|
87
|
+
job.audioReceived = true;
|
|
88
|
+
respond(response, 204);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (request.method === 'POST' && action === 'finish') {
|
|
93
|
+
if (job.receivedFrames.size !== job.totalFrames) {
|
|
94
|
+
return respond(
|
|
95
|
+
response,
|
|
96
|
+
409,
|
|
97
|
+
`Missing ${job.totalFrames - job.receivedFrames.size} export frames`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (job.hasAudio && !job.audioReceived) {
|
|
101
|
+
return respond(response, 409, 'The attached audio file was not uploaded');
|
|
102
|
+
}
|
|
103
|
+
await finishJob(id, job, response);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
respond(response, 405, 'Method not allowed');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function createJob(request, response) {
|
|
111
|
+
const input = JSON.parse((await readRequest(request, 64 * 1024)).toString('utf8'));
|
|
112
|
+
positiveInteger(input.width, 'width', 8192);
|
|
113
|
+
positiveInteger(input.height, 'height', 8192);
|
|
114
|
+
const fps = positiveNumber(input.fps, 'fps', 240);
|
|
115
|
+
const duration = positiveNumber(input.duration, 'duration', 24 * 60 * 60);
|
|
116
|
+
const totalFrames = positiveInteger(input.totalFrames, 'totalFrames', 24 * 60 * 60 * 240);
|
|
117
|
+
const audioStart = nonNegativeNumber(input.audioStart ?? 0, 'audioStart', duration);
|
|
118
|
+
const expectedFrames = Math.max(1, Math.ceil(duration * fps));
|
|
119
|
+
if (totalFrames !== expectedFrames) throw new Error(`Expected ${expectedFrames} frames`);
|
|
120
|
+
const id = randomUUID();
|
|
121
|
+
const directory = await mkdtemp(join(tmpdir(), 'motionly-export-'));
|
|
122
|
+
jobs.set(id, {
|
|
123
|
+
directory,
|
|
124
|
+
fps,
|
|
125
|
+
duration,
|
|
126
|
+
totalFrames,
|
|
127
|
+
receivedFrames: new Set(),
|
|
128
|
+
hasAudio: input.hasAudio === true,
|
|
129
|
+
audioStart,
|
|
130
|
+
audioReceived: false,
|
|
131
|
+
});
|
|
132
|
+
response.statusCode = 201;
|
|
133
|
+
response.setHeader('content-type', 'application/json');
|
|
134
|
+
response.end(JSON.stringify({ id }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function finishJob(id, job, response) {
|
|
138
|
+
const outputPath = join(job.directory, 'motionly.mp4');
|
|
139
|
+
const framePattern = join(job.directory, 'frame-%08d.jpg');
|
|
140
|
+
const args = [
|
|
141
|
+
'-hide_banner',
|
|
142
|
+
'-loglevel',
|
|
143
|
+
'error',
|
|
144
|
+
'-y',
|
|
145
|
+
'-framerate',
|
|
146
|
+
String(job.fps),
|
|
147
|
+
'-start_number',
|
|
148
|
+
'0',
|
|
149
|
+
'-i',
|
|
150
|
+
framePattern,
|
|
151
|
+
];
|
|
152
|
+
if (job.hasAudio) {
|
|
153
|
+
args.push('-itsoffset', String(job.audioStart), '-i', join(job.directory, 'audio-input'));
|
|
154
|
+
}
|
|
155
|
+
args.push('-map', '0:v:0');
|
|
156
|
+
if (job.hasAudio) args.push('-map', '1:a:0', '-c:a', 'aac', '-b:a', '192k');
|
|
157
|
+
args.push(
|
|
158
|
+
'-c:v',
|
|
159
|
+
'libx264',
|
|
160
|
+
'-preset',
|
|
161
|
+
'medium',
|
|
162
|
+
'-crf',
|
|
163
|
+
'18',
|
|
164
|
+
'-vf',
|
|
165
|
+
'pad=ceil(iw/2)*2:ceil(ih/2)*2:color=black,format=yuv420p',
|
|
166
|
+
'-frames:v',
|
|
167
|
+
String(job.totalFrames),
|
|
168
|
+
'-t',
|
|
169
|
+
String(job.duration),
|
|
170
|
+
'-movflags',
|
|
171
|
+
'+faststart',
|
|
172
|
+
outputPath
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
jobs.delete(id);
|
|
176
|
+
try {
|
|
177
|
+
await runFfmpeg(args);
|
|
178
|
+
const output = await stat(outputPath);
|
|
179
|
+
response.statusCode = 200;
|
|
180
|
+
response.setHeader('content-type', 'video/mp4');
|
|
181
|
+
response.setHeader('content-length', String(output.size));
|
|
182
|
+
await pipeline(createReadStream(outputPath), response);
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(job.directory, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function runFfmpeg(args) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const child = spawn('ffmpeg', args, { windowsHide: true });
|
|
191
|
+
let errorOutput = '';
|
|
192
|
+
child.stderr.setEncoding('utf8');
|
|
193
|
+
child.stderr.on('data', (chunk) => {
|
|
194
|
+
errorOutput = `${errorOutput}${chunk}`.slice(-16_384);
|
|
195
|
+
});
|
|
196
|
+
child.once('error', (error) => {
|
|
197
|
+
reject(
|
|
198
|
+
error.code === 'ENOENT'
|
|
199
|
+
? new Error('ffmpeg is not installed or is not available on PATH')
|
|
200
|
+
: error
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
child.once('close', (code) => {
|
|
204
|
+
if (code === 0) resolve();
|
|
205
|
+
else reject(new Error(errorOutput.trim() || `ffmpeg exited with code ${String(code)}`));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function writeRequest(request, path, maxBytes) {
|
|
211
|
+
let size = 0;
|
|
212
|
+
const limiter = new Transform({
|
|
213
|
+
transform(chunk, _encoding, callback) {
|
|
214
|
+
size += chunk.byteLength;
|
|
215
|
+
callback(size > maxBytes ? new Error('Export upload is too large') : null, chunk);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
await pipeline(request, limiter, createWriteStream(path));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
await rm(path, { force: true });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function readRequest(request, maxBytes) {
|
|
227
|
+
const chunks = [];
|
|
228
|
+
let size = 0;
|
|
229
|
+
for await (const chunk of request) {
|
|
230
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
231
|
+
size += buffer.byteLength;
|
|
232
|
+
if (size > maxBytes) throw new Error('Export upload is too large');
|
|
233
|
+
chunks.push(buffer);
|
|
234
|
+
}
|
|
235
|
+
return Buffer.concat(chunks);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function positiveInteger(value, name, max) {
|
|
239
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0 || value > max) {
|
|
240
|
+
throw new Error(`Invalid export ${name}`);
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function positiveNumber(value, name, max) {
|
|
246
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > max) {
|
|
247
|
+
throw new Error(`Invalid export ${name}`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function nonNegativeNumber(value, name, max) {
|
|
253
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > max) {
|
|
254
|
+
throw new Error(`Invalid export ${name}`);
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function respond(response, status, message) {
|
|
260
|
+
response.statusCode = status;
|
|
261
|
+
if (message) response.setHeader('content-type', 'text/plain;charset=utf-8');
|
|
262
|
+
response.end(message);
|
|
263
|
+
}
|
package/bin/motionly.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Zero-dependency Motionly CLI and local editor server.
|
|
3
3
|
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
|
+
import { handleFfmpegExportRequest } from './ffmpeg-export.js';
|
|
5
6
|
import { createInterface } from 'node:readline/promises';
|
|
6
7
|
import { spawn } from 'node:child_process';
|
|
7
8
|
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
@@ -151,7 +152,11 @@ async function copyReferenceLibrary(source, destination) {
|
|
|
151
152
|
const to = join(destination, entry.name);
|
|
152
153
|
if (entry.isDirectory()) {
|
|
153
154
|
copied += await copyReferenceLibrary(from, to);
|
|
154
|
-
} else if (
|
|
155
|
+
} else if (
|
|
156
|
+
entry.name === 'SKILL.md' ||
|
|
157
|
+
entry.name === 'llms.txt' ||
|
|
158
|
+
entry.name === 'AGENTS.md'
|
|
159
|
+
) {
|
|
155
160
|
try {
|
|
156
161
|
await writeFile(to, await readFile(from), { flag: 'wx' });
|
|
157
162
|
copied += 1;
|
|
@@ -252,7 +257,8 @@ async function promptForAgent(scope) {
|
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
async function initProject(name, argv = []) {
|
|
255
|
-
if (!name || name.startsWith('-'))
|
|
260
|
+
if (!name || name.startsWith('-'))
|
|
261
|
+
throw new Error('Usage: npx @coppsary/motionly init <project-folder>');
|
|
256
262
|
const target = resolve(name);
|
|
257
263
|
if (await exists(target)) {
|
|
258
264
|
if ((await readdir(target)).length) throw new Error(`Folder is not empty: ${target}`);
|
|
@@ -286,7 +292,7 @@ async function initProject(name, argv = []) {
|
|
|
286
292
|
if (providers.length) await installSkills(skillBase(scope, target), providers);
|
|
287
293
|
}
|
|
288
294
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
289
|
-
console.log(`\n To reopen later: cd ${name} && npx motionly dev`);
|
|
295
|
+
console.log(`\n To reopen later: cd ${name} && npx @coppsary/motionly dev`);
|
|
290
296
|
await serveEditor(argv, target);
|
|
291
297
|
}
|
|
292
298
|
}
|
|
@@ -335,6 +341,8 @@ async function serveEditor(argv, projectFolder = null) {
|
|
|
335
341
|
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
336
342
|
const pathname = decodeURIComponent(url.pathname);
|
|
337
343
|
|
|
344
|
+
if (await handleFfmpegExportRequest(request, response)) return;
|
|
345
|
+
|
|
338
346
|
if (projectPath && pathname === '/api/motion-project') {
|
|
339
347
|
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
340
348
|
const source = await readFile(projectPath);
|
|
@@ -408,7 +416,7 @@ async function serveEditor(argv, projectFolder = null) {
|
|
|
408
416
|
server.on('error', (error) => {
|
|
409
417
|
if (error.code === 'EADDRINUSE') {
|
|
410
418
|
console.error(
|
|
411
|
-
`Port ${port} is in use. Try: npx motionly ${projectRoot ? 'dev ' : ''}--port ${port + 1}`
|
|
419
|
+
`Port ${port} is in use. Try: npx @coppsary/motionly ${projectRoot ? 'dev ' : ''}--port ${port + 1}`
|
|
412
420
|
);
|
|
413
421
|
process.exitCode = 1;
|
|
414
422
|
return;
|
|
@@ -420,14 +428,14 @@ async function serveEditor(argv, projectFolder = null) {
|
|
|
420
428
|
function printHelp() {
|
|
421
429
|
console.log(`Motionly
|
|
422
430
|
|
|
423
|
-
npx motionly init <project-folder> Create a project; asks which agent to set up
|
|
424
|
-
npx motionly init <folder> --provider codex Create a project; install for one agent (no prompt)
|
|
425
|
-
npx motionly init <folder> --all Create a project; install for every agent
|
|
426
|
-
npx motionly init <folder> --skip-skills Create a project without agent skills
|
|
427
|
-
npx motionly skills add Install agent skills into an existing project
|
|
428
|
-
npx motionly skills add --all
|
|
429
|
-
npx motionly skills add --provider <codex|claude|gemini|opencode|kiro>
|
|
430
|
-
npx motionly dev [project-folder] Reopen and edit a local project
|
|
431
|
+
npx @coppsary/motionly init <project-folder> Create a project; asks which agent to set up
|
|
432
|
+
npx @coppsary/motionly init <folder> --provider codex Create a project; install for one agent (no prompt)
|
|
433
|
+
npx @coppsary/motionly init <folder> --all Create a project; install for every agent
|
|
434
|
+
npx @coppsary/motionly init <folder> --skip-skills Create a project without agent skills
|
|
435
|
+
npx @coppsary/motionly skills add Install agent skills into an existing project
|
|
436
|
+
npx @coppsary/motionly skills add --all
|
|
437
|
+
npx @coppsary/motionly skills add --provider <codex|claude|gemini|opencode|kiro>
|
|
438
|
+
npx @coppsary/motionly dev [project-folder] Reopen and edit a local project
|
|
431
439
|
|
|
432
440
|
Options: --scope <project|global>, --port <number>, --no-open`);
|
|
433
441
|
}
|
|
@@ -438,7 +446,7 @@ async function main() {
|
|
|
438
446
|
if (command === 'skills') {
|
|
439
447
|
if (subcommand !== 'add')
|
|
440
448
|
throw new Error(
|
|
441
|
-
'Usage: npx motionly skills add [--provider <name> | --all] [--scope project|global]'
|
|
449
|
+
'Usage: npx @coppsary/motionly skills add [--provider <name> | --all] [--scope project|global]'
|
|
442
450
|
);
|
|
443
451
|
const options = await resolveSkillOptions(argv.slice(2));
|
|
444
452
|
await installSkills(skillBase(options.scope, process.cwd()), options.providers);
|