@remixmate/cli 0.9.64 → 0.9.65
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/dist/cli.js +13 -0
- package/dist/handlers/index.js +2 -0
- package/dist/handlers/template-init.d.ts +10 -0
- package/dist/handlers/template-init.js +21 -0
- package/dist/manifest.json +52 -2
- package/dist/template/args.d.ts +25 -0
- package/dist/template/args.js +69 -0
- package/dist/template/check.d.ts +23 -0
- package/dist/template/check.js +25 -0
- package/dist/template/commands.d.ts +25 -0
- package/dist/template/commands.js +146 -0
- package/dist/template/constants.d.ts +25 -0
- package/dist/template/constants.js +32 -0
- package/dist/template/init.d.ts +22 -0
- package/dist/template/init.js +115 -0
- package/dist/template/list.d.ts +23 -0
- package/dist/template/list.js +46 -0
- package/dist/template/preview.d.ts +60 -0
- package/dist/template/preview.js +318 -0
- package/dist/template/proc.d.ts +26 -0
- package/dist/template/proc.js +43 -0
- package/dist/template/project.d.ts +37 -0
- package/dist/template/project.js +110 -0
- package/dist/template/publish.d.ts +43 -0
- package/dist/template/publish.js +182 -0
- package/dist/template/render-client.d.ts +39 -0
- package/dist/template/render-client.js +79 -0
- package/package.json +1 -1
- package/skills/template-authoring/SKILL.md +69 -0
- package/skills/template-authoring/skill.json +32 -0
- package/skills/template-authoring/version.json +7 -0
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ import { runExec } from './exec.js';
|
|
|
21
21
|
import { authStatusLine, runDoctor } from './doctor.js';
|
|
22
22
|
import { padDisplay } from './text.js';
|
|
23
23
|
import { EXIT, SkillError } from './errors.js';
|
|
24
|
+
import { TEMPLATE_VERB, runTemplateCommand } from './template/commands.js';
|
|
25
|
+
import { diagnostic } from './diagnostics.js';
|
|
24
26
|
/** Reserved auth verbs handled before skill dispatch. */
|
|
25
27
|
const AUTH_VERBS = new Set(['login', 'logout', 'whoami']);
|
|
26
28
|
/**
|
|
@@ -64,6 +66,11 @@ function printUsage() {
|
|
|
64
66
|
process.stdout.write(' remixmate project use <prj> [--global] Bind output to a project (this dir, or globally)\n');
|
|
65
67
|
process.stdout.write(' remixmate project show Show the current project binding\n');
|
|
66
68
|
process.stdout.write(' remixmate host probe Diagnose host detection and take grouping\n');
|
|
69
|
+
process.stdout.write(' remixmate template init <dir> Create a video-template project (no login)\n');
|
|
70
|
+
process.stdout.write(' remixmate template check [name] Validate templates locally (no login)\n');
|
|
71
|
+
process.stdout.write(' remixmate template preview <name> Render a draft preview mp4 remotely\n');
|
|
72
|
+
process.stdout.write(' remixmate template publish <name> Publish as your private template\n');
|
|
73
|
+
process.stdout.write(' remixmate template list List your private templates\n');
|
|
67
74
|
process.stdout.write(' remixmate <name> [--flag value ...] Invoke a skill\n');
|
|
68
75
|
process.stdout.write(' remixmate <name> --help Show skill-specific help\n');
|
|
69
76
|
process.stdout.write('\nAvailable skills:\n');
|
|
@@ -155,6 +162,12 @@ async function main() {
|
|
|
155
162
|
if (argv[0] === HOST_VERB) {
|
|
156
163
|
process.exit(await runHostCommand(argv.slice(1)));
|
|
157
164
|
}
|
|
165
|
+
if (argv[0] === TEMPLATE_VERB) {
|
|
166
|
+
const started = performance.now();
|
|
167
|
+
const code = await runTemplateCommand(argv.slice(1));
|
|
168
|
+
diagnostic(code, performance.now() - started);
|
|
169
|
+
process.exit(code);
|
|
170
|
+
}
|
|
158
171
|
if (argv[0] === 'exec') {
|
|
159
172
|
// Raw argv (not parsed) — everything after `--` belongs to the child command.
|
|
160
173
|
const code = await runExec(argv.slice(1));
|
package/dist/handlers/index.js
CHANGED
|
@@ -8,9 +8,11 @@ import { genImage } from './gen-image.js';
|
|
|
8
8
|
import { genVoice } from './gen-voice.js';
|
|
9
9
|
import { genVideo } from './gen-video.js';
|
|
10
10
|
import { genDigitalHuman } from './gen-digital-human.js';
|
|
11
|
+
import { templateInit } from './template-init.js';
|
|
11
12
|
export const HANDLERS = {
|
|
12
13
|
'gen-image': genImage,
|
|
13
14
|
'gen-voice': genVoice,
|
|
14
15
|
'gen-video': genVideo,
|
|
15
16
|
'gen-digital-human': genDigitalHuman,
|
|
17
|
+
'template-init': templateInit,
|
|
16
18
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* template-authoring skill → `remixmate template init`.
|
|
3
|
+
*
|
|
4
|
+
* Agent hosts call skills as `remixmate template-authoring --dir X [--force]`;
|
|
5
|
+
* this handler is a thin adapter so both entry points share one implementation.
|
|
6
|
+
* Purely local (auth: none): fetches the starter via npm, no ab-api calls.
|
|
7
|
+
*/
|
|
8
|
+
import type { HandlerContext, HandlerInput } from './index.js';
|
|
9
|
+
export declare function templateInit(input: HandlerInput, _ctx: HandlerContext): Promise<void>;
|
|
10
|
+
export default templateInit;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* template-authoring skill → `remixmate template init`.
|
|
3
|
+
*
|
|
4
|
+
* Agent hosts call skills as `remixmate template-authoring --dir X [--force]`;
|
|
5
|
+
* this handler is a thin adapter so both entry points share one implementation.
|
|
6
|
+
* Purely local (auth: none): fetches the starter via npm, no ab-api calls.
|
|
7
|
+
*/
|
|
8
|
+
import { EXIT, SkillError } from '../errors.js';
|
|
9
|
+
import { runInit } from '../template/init.js';
|
|
10
|
+
export async function templateInit(input, _ctx) {
|
|
11
|
+
const dir = typeof input.dir === 'string' ? input.dir.trim() : '';
|
|
12
|
+
if (!dir)
|
|
13
|
+
throw new SkillError('❌ 缺少 --dir:模板工程的目标目录', EXIT.USAGE);
|
|
14
|
+
await runInit({
|
|
15
|
+
dir,
|
|
16
|
+
force: input.force === true || input.force === 'true',
|
|
17
|
+
json: input.json_output === true || input.json === true,
|
|
18
|
+
skipInstall: input.skip_install === true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export default templateInit;
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"version": "0.9.65",
|
|
4
|
+
"generatedAt": "2026-10-05T06:33:09.117Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -932,6 +932,56 @@
|
|
|
932
932
|
"hidden": []
|
|
933
933
|
}
|
|
934
934
|
},
|
|
935
|
+
{
|
|
936
|
+
"id": "template-authoring",
|
|
937
|
+
"toolName": "template_authoring",
|
|
938
|
+
"tier": "tool",
|
|
939
|
+
"category": "meta",
|
|
940
|
+
"title": "Video Template Authoring",
|
|
941
|
+
"summary": "Start authoring a custom video template locally: scaffolds a starter project (Remotion Studio preview, local validation, remote draft preview, publishing as the user's private template) into the given directory. After this, follow the project's AGENTS.md and use `remixmate template check|preview|publish`. Local-only; requires Node >= 18 and npm.",
|
|
942
|
+
"triggers": [
|
|
943
|
+
"做视频模板",
|
|
944
|
+
"自定义视频模板",
|
|
945
|
+
"创建模板 / 新建模板 / 自己写一个视频模板",
|
|
946
|
+
"预览或发布我本地开发的模板",
|
|
947
|
+
"template authoring",
|
|
948
|
+
"create video template / build a custom video template",
|
|
949
|
+
"publish my own Remotion template"
|
|
950
|
+
],
|
|
951
|
+
"entry": {
|
|
952
|
+
"type": "builtin",
|
|
953
|
+
"handler": "template-init"
|
|
954
|
+
},
|
|
955
|
+
"runtime": "node",
|
|
956
|
+
"envVars": [
|
|
957
|
+
"REMIXMATE_STARTER_TARBALL"
|
|
958
|
+
],
|
|
959
|
+
"parameters": {
|
|
960
|
+
"type": "object",
|
|
961
|
+
"properties": {
|
|
962
|
+
"dir": {
|
|
963
|
+
"type": "string",
|
|
964
|
+
"description": "Target directory for the new template project (must be empty unless force is set)"
|
|
965
|
+
},
|
|
966
|
+
"force": {
|
|
967
|
+
"type": "boolean",
|
|
968
|
+
"description": "Allow scaffolding into a non-empty directory (existing files with the same name are overwritten)"
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
"required": [
|
|
972
|
+
"dir"
|
|
973
|
+
]
|
|
974
|
+
},
|
|
975
|
+
"ui": {
|
|
976
|
+
"primary": [
|
|
977
|
+
"dir"
|
|
978
|
+
],
|
|
979
|
+
"advanced": [
|
|
980
|
+
"force"
|
|
981
|
+
],
|
|
982
|
+
"hidden": []
|
|
983
|
+
}
|
|
984
|
+
},
|
|
935
985
|
{
|
|
936
986
|
"id": "template-registry",
|
|
937
987
|
"toolName": "template_registry",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict argv parser for `remixmate template <sub>`.
|
|
3
|
+
*
|
|
4
|
+
* Unlike src/argv.ts (which is permissive because skill flags are forwarded to
|
|
5
|
+
* Python), these subcommands own their whole surface, so an unknown flag or a
|
|
6
|
+
* stray positional is a usage error (exit 2) rather than something silently
|
|
7
|
+
* ignored — `publish --nmae X` must not publish under the default name.
|
|
8
|
+
*/
|
|
9
|
+
import { SkillError } from '../errors.js';
|
|
10
|
+
export type FlagKind = 'bool' | 'string';
|
|
11
|
+
export type FlagSpec = Record<string, FlagKind>;
|
|
12
|
+
export interface ParsedTemplateArgs {
|
|
13
|
+
positionals: string[];
|
|
14
|
+
/** Keyed by the flag name without dashes, kebab-case preserved (`props-json`). */
|
|
15
|
+
flags: Record<string, string | boolean>;
|
|
16
|
+
}
|
|
17
|
+
/** Flags every subcommand accepts. */
|
|
18
|
+
export declare const COMMON_FLAGS: FlagSpec;
|
|
19
|
+
export declare function usageError(message: string): SkillError;
|
|
20
|
+
export declare function parseTemplateArgs(argv: string[], spec: FlagSpec, opts: {
|
|
21
|
+
maxPositionals: number;
|
|
22
|
+
}): ParsedTemplateArgs;
|
|
23
|
+
export declare function flagString(flags: ParsedTemplateArgs['flags'], name: string): string | undefined;
|
|
24
|
+
/** Parse a positive number flag; usage error on anything else. */
|
|
25
|
+
export declare function flagNumber(flags: ParsedTemplateArgs['flags'], name: string): number | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict argv parser for `remixmate template <sub>`.
|
|
3
|
+
*
|
|
4
|
+
* Unlike src/argv.ts (which is permissive because skill flags are forwarded to
|
|
5
|
+
* Python), these subcommands own their whole surface, so an unknown flag or a
|
|
6
|
+
* stray positional is a usage error (exit 2) rather than something silently
|
|
7
|
+
* ignored — `publish --nmae X` must not publish under the default name.
|
|
8
|
+
*/
|
|
9
|
+
import { EXIT, SkillError } from '../errors.js';
|
|
10
|
+
/** Flags every subcommand accepts. */
|
|
11
|
+
export const COMMON_FLAGS = {
|
|
12
|
+
json: 'bool',
|
|
13
|
+
help: 'bool',
|
|
14
|
+
token: 'string',
|
|
15
|
+
'api-base-url': 'string',
|
|
16
|
+
};
|
|
17
|
+
export function usageError(message) {
|
|
18
|
+
return new SkillError(`❌ ${message}`, EXIT.USAGE);
|
|
19
|
+
}
|
|
20
|
+
export function parseTemplateArgs(argv, spec, opts) {
|
|
21
|
+
const all = { ...COMMON_FLAGS, ...spec };
|
|
22
|
+
const positionals = [];
|
|
23
|
+
const flags = {};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const token = argv[i];
|
|
26
|
+
if (token === '-h') {
|
|
27
|
+
flags.help = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!token.startsWith('--')) {
|
|
31
|
+
positionals.push(token);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const eq = token.indexOf('=');
|
|
35
|
+
const name = token.slice(2, eq > 0 ? eq : undefined);
|
|
36
|
+
const kind = all[name];
|
|
37
|
+
if (!kind)
|
|
38
|
+
throw usageError(`未知参数: --${name}`);
|
|
39
|
+
if (kind === 'bool') {
|
|
40
|
+
if (eq > 0)
|
|
41
|
+
throw usageError(`--${name} 不接受取值`);
|
|
42
|
+
flags[name] = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const value = eq > 0 ? token.slice(eq + 1) : argv[++i];
|
|
46
|
+
if (value === undefined || (eq < 0 && value.startsWith('--'))) {
|
|
47
|
+
throw usageError(`--${name} 需要一个取值`);
|
|
48
|
+
}
|
|
49
|
+
flags[name] = value;
|
|
50
|
+
}
|
|
51
|
+
if (!flags.help && positionals.length > opts.maxPositionals) {
|
|
52
|
+
throw usageError(`多余的参数: ${positionals.slice(opts.maxPositionals).join(' ')}`);
|
|
53
|
+
}
|
|
54
|
+
return { positionals, flags };
|
|
55
|
+
}
|
|
56
|
+
export function flagString(flags, name) {
|
|
57
|
+
const v = flags[name];
|
|
58
|
+
return typeof v === 'string' ? v : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Parse a positive number flag; usage error on anything else. */
|
|
61
|
+
export function flagNumber(flags, name) {
|
|
62
|
+
const raw = flagString(flags, name);
|
|
63
|
+
if (raw === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
const n = Number(raw);
|
|
66
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
67
|
+
throw usageError(`--${name} 必须是正数,收到 '${raw}'`);
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template check [name] [--json] [--no-write]`
|
|
3
|
+
*
|
|
4
|
+
* Pure orchestration: the validation itself lives in the starter
|
|
5
|
+
* (`.remixmate/check/index.mjs`, K3), synced from the same schema / security
|
|
6
|
+
* policy sources the platform uses. The cli never carries its own copy — that
|
|
7
|
+
* would be the drift requirement 5.2 exists to prevent.
|
|
8
|
+
*/
|
|
9
|
+
import { type ProcOptions } from './proc.js';
|
|
10
|
+
export interface CheckOptions {
|
|
11
|
+
name?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
noWrite?: boolean;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Where the check's stdout goes. Standalone `check` streams it straight
|
|
17
|
+
* through; preview/publish in --json mode route it to stderr so their own
|
|
18
|
+
* JSON stays the last stdout line.
|
|
19
|
+
*/
|
|
20
|
+
stdio?: ProcOptions['stdio'];
|
|
21
|
+
}
|
|
22
|
+
/** Run the starter's check script; resolves to its exit code (0 ok / 1 issues / 2 usage). */
|
|
23
|
+
export declare function runCheck(opts?: CheckOptions): Promise<number>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template check [name] [--json] [--no-write]`
|
|
3
|
+
*
|
|
4
|
+
* Pure orchestration: the validation itself lives in the starter
|
|
5
|
+
* (`.remixmate/check/index.mjs`, K3), synced from the same schema / security
|
|
6
|
+
* policy sources the platform uses. The cli never carries its own copy — that
|
|
7
|
+
* would be the drift requirement 5.2 exists to prevent.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { CHECK_ENTRY } from './constants.js';
|
|
11
|
+
import { requireProjectRoot } from './project.js';
|
|
12
|
+
import { runProcess } from './proc.js';
|
|
13
|
+
/** Run the starter's check script; resolves to its exit code (0 ok / 1 issues / 2 usage). */
|
|
14
|
+
export async function runCheck(opts = {}) {
|
|
15
|
+
const root = requireProjectRoot(opts.cwd);
|
|
16
|
+
const args = [path.join(root, CHECK_ENTRY)];
|
|
17
|
+
if (opts.name)
|
|
18
|
+
args.push(opts.name);
|
|
19
|
+
if (opts.json)
|
|
20
|
+
args.push('--json');
|
|
21
|
+
if (opts.noWrite)
|
|
22
|
+
args.push('--no-write');
|
|
23
|
+
const res = await runProcess(process.execPath, args, { cwd: root, stdio: opts.stdio ?? 'inherit' });
|
|
24
|
+
return res.code;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template <init|check|preview|publish|list>` dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Exit codes follow the cli contract (errors.ts): 2 usage, 4 credentials,
|
|
5
|
+
* 5 backend unreachable; `check` propagates the starter script's own code.
|
|
6
|
+
* With --json a failure also ends stdout with `{"ok":false,...}` so hosts
|
|
7
|
+
* parsing the last line always get an object.
|
|
8
|
+
*/
|
|
9
|
+
import { type FlagSpec, type ParsedTemplateArgs } from './args.js';
|
|
10
|
+
export declare const TEMPLATE_VERB = "template";
|
|
11
|
+
export declare const TEMPLATE_SUBCOMMANDS: readonly ["init", "check", "preview", "publish", "list"];
|
|
12
|
+
export type TemplateSubcommand = (typeof TEMPLATE_SUBCOMMANDS)[number];
|
|
13
|
+
interface SubSpec {
|
|
14
|
+
flags: FlagSpec;
|
|
15
|
+
maxPositionals: number;
|
|
16
|
+
usage: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const SUB_SPECS: Record<TemplateSubcommand, SubSpec>;
|
|
19
|
+
export declare function templateUsage(): string;
|
|
20
|
+
export declare function parseTemplateCommand(argv: string[]): {
|
|
21
|
+
sub: TemplateSubcommand | 'help';
|
|
22
|
+
args: ParsedTemplateArgs;
|
|
23
|
+
};
|
|
24
|
+
export declare function runTemplateCommand(argv: string[]): Promise<number>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template <init|check|preview|publish|list>` dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Exit codes follow the cli contract (errors.ts): 2 usage, 4 credentials,
|
|
5
|
+
* 5 backend unreachable; `check` propagates the starter script's own code.
|
|
6
|
+
* With --json a failure also ends stdout with `{"ok":false,...}` so hosts
|
|
7
|
+
* parsing the last line always get an object.
|
|
8
|
+
*/
|
|
9
|
+
import { cleanDiagnosticText } from '../diagnostics.js';
|
|
10
|
+
import { EXIT, SkillError } from '../errors.js';
|
|
11
|
+
import { flagNumber, flagString, parseTemplateArgs, usageError } from './args.js';
|
|
12
|
+
import { runCheck } from './check.js';
|
|
13
|
+
import { STARTER_PACKAGE, STARTER_VERSION } from './constants.js';
|
|
14
|
+
import { runInit } from './init.js';
|
|
15
|
+
import { runList } from './list.js';
|
|
16
|
+
import { runPreview } from './preview.js';
|
|
17
|
+
import { runPublish } from './publish.js';
|
|
18
|
+
export const TEMPLATE_VERB = 'template';
|
|
19
|
+
export const TEMPLATE_SUBCOMMANDS = ['init', 'check', 'preview', 'publish', 'list'];
|
|
20
|
+
export const SUB_SPECS = {
|
|
21
|
+
init: {
|
|
22
|
+
flags: { force: 'bool', 'skip-install': 'bool' },
|
|
23
|
+
maxPositionals: 1,
|
|
24
|
+
usage: 'remixmate template init <dir> [--force] [--skip-install] [--json]\n' +
|
|
25
|
+
` 从 ${STARTER_PACKAGE}@${STARTER_VERSION} 创建模板工程并安装依赖(无需登录)。\n` +
|
|
26
|
+
' 目标目录非空时拒绝,除非 --force。--skip-install 只解压不执行 npm install。\n',
|
|
27
|
+
},
|
|
28
|
+
check: {
|
|
29
|
+
flags: { 'no-write': 'bool' },
|
|
30
|
+
maxPositionals: 1,
|
|
31
|
+
usage: 'remixmate template check [name] [--json] [--no-write]\n' +
|
|
32
|
+
' 在模板工程内运行本地校验(类型、schema、安全策略、props 契约、core 导出)。\n' +
|
|
33
|
+
' 不传 name 校验 templates/ 下全部模板;默认刷新 coreVersion/coreExports,--no-write 只检查。\n',
|
|
34
|
+
},
|
|
35
|
+
preview: {
|
|
36
|
+
flags: { 'props-json': 'string', 'duration-sec': 'string', 'composition-id': 'string', 'timeout-sec': 'string' },
|
|
37
|
+
maxPositionals: 1,
|
|
38
|
+
usage: 'remixmate template preview <name> [--props-json JSON] [--duration-sec N] [--composition-id ID] [--timeout-sec N] [--json]\n' +
|
|
39
|
+
' 先 check,再把模板提交到远程渲染器出预览 mp4(需登录)。\n' +
|
|
40
|
+
' 样例数据取自模板的 dsl-example.json,可用 --props-json 覆盖(传数组 = 每个元素一页)。\n' +
|
|
41
|
+
' 预览上限:长边 1280、短边 720、总时长 60 秒。\n',
|
|
42
|
+
},
|
|
43
|
+
publish: {
|
|
44
|
+
flags: { name: 'string', 'dry-run': 'bool' },
|
|
45
|
+
maxPositionals: 1,
|
|
46
|
+
usage: 'remixmate template publish <name> [--name <displayName>] [--dry-run] [--json]\n' +
|
|
47
|
+
' 先 check 与 core 预检,再发布为你的私有模板(需登录)。\n' +
|
|
48
|
+
' 首次发布生成 templateKey 并写入 templates/<name>/.remixmate.json,之后重复发布即更新。\n',
|
|
49
|
+
},
|
|
50
|
+
list: {
|
|
51
|
+
flags: {},
|
|
52
|
+
maxPositionals: 0,
|
|
53
|
+
usage: 'remixmate template list [--json]\n 列出你发布的私有模板(需登录)。\n',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export function templateUsage() {
|
|
57
|
+
return ('Usage: remixmate template <command> [options]\n\n' +
|
|
58
|
+
'Commands:\n' +
|
|
59
|
+
' init <dir> 创建模板工程(无需登录)\n' +
|
|
60
|
+
' check [name] 本地校验(无需登录)\n' +
|
|
61
|
+
' preview <name> 远程渲染预览 mp4\n' +
|
|
62
|
+
' publish <name> 发布为私有模板\n' +
|
|
63
|
+
' list 列出我的私有模板\n\n' +
|
|
64
|
+
'Common options: --json --token <PrivToken> --api-base-url <url>\n' +
|
|
65
|
+
'环境变量:AB_RENDER_URL(渲染服务地址)、REMIXMATE_STARTER_TARBALL(init 使用本地 starter 包)\n' +
|
|
66
|
+
'运行 remixmate template <command> --help 查看子命令说明。\n');
|
|
67
|
+
}
|
|
68
|
+
export function parseTemplateCommand(argv) {
|
|
69
|
+
const [sub, ...rest] = argv;
|
|
70
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
71
|
+
return { sub: 'help', args: { positionals: [], flags: {} } };
|
|
72
|
+
}
|
|
73
|
+
if (!TEMPLATE_SUBCOMMANDS.includes(sub)) {
|
|
74
|
+
throw usageError(`未知子命令: template ${sub}(可选:${TEMPLATE_SUBCOMMANDS.join(' | ')})`);
|
|
75
|
+
}
|
|
76
|
+
const spec = SUB_SPECS[sub];
|
|
77
|
+
return { sub: sub, args: parseTemplateArgs(rest, spec.flags, { maxPositionals: spec.maxPositionals }) };
|
|
78
|
+
}
|
|
79
|
+
async function dispatch(sub, args) {
|
|
80
|
+
const { flags, positionals } = args;
|
|
81
|
+
const json = flags.json === true;
|
|
82
|
+
const token = flagString(flags, 'token');
|
|
83
|
+
const apiBaseUrl = flagString(flags, 'api-base-url');
|
|
84
|
+
switch (sub) {
|
|
85
|
+
case 'init':
|
|
86
|
+
if (!positionals[0])
|
|
87
|
+
throw usageError('缺少目标目录:remixmate template init <dir>');
|
|
88
|
+
await runInit({ dir: positionals[0], force: flags.force === true, json, skipInstall: flags['skip-install'] === true });
|
|
89
|
+
return EXIT.OK;
|
|
90
|
+
case 'check':
|
|
91
|
+
return runCheck({ name: positionals[0], json, noWrite: flags['no-write'] === true });
|
|
92
|
+
case 'preview':
|
|
93
|
+
await runPreview({
|
|
94
|
+
name: positionals[0],
|
|
95
|
+
propsJson: flagString(flags, 'props-json'),
|
|
96
|
+
durationSec: flagNumber(flags, 'duration-sec'),
|
|
97
|
+
compositionId: flagString(flags, 'composition-id'),
|
|
98
|
+
timeoutSec: flagNumber(flags, 'timeout-sec'),
|
|
99
|
+
json,
|
|
100
|
+
token,
|
|
101
|
+
apiBaseUrl,
|
|
102
|
+
});
|
|
103
|
+
return EXIT.OK;
|
|
104
|
+
case 'publish':
|
|
105
|
+
await runPublish({
|
|
106
|
+
name: positionals[0],
|
|
107
|
+
displayName: flagString(flags, 'name'),
|
|
108
|
+
dryRun: flags['dry-run'] === true,
|
|
109
|
+
json,
|
|
110
|
+
token,
|
|
111
|
+
apiBaseUrl,
|
|
112
|
+
});
|
|
113
|
+
return EXIT.OK;
|
|
114
|
+
case 'list':
|
|
115
|
+
await runList({ json, token, apiBaseUrl });
|
|
116
|
+
return EXIT.OK;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function runTemplateCommand(argv) {
|
|
120
|
+
const json = argv.includes('--json');
|
|
121
|
+
let sub;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = parseTemplateCommand(argv);
|
|
124
|
+
sub = parsed.sub;
|
|
125
|
+
if (sub === 'help') {
|
|
126
|
+
process.stdout.write(templateUsage());
|
|
127
|
+
return EXIT.OK;
|
|
128
|
+
}
|
|
129
|
+
if (parsed.args.flags.help) {
|
|
130
|
+
process.stdout.write('Usage: ' + SUB_SPECS[sub].usage);
|
|
131
|
+
return EXIT.OK;
|
|
132
|
+
}
|
|
133
|
+
return await dispatch(sub, parsed.args);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const code = err instanceof SkillError ? err.exitCode : EXIT.ERROR;
|
|
137
|
+
const message = err instanceof SkillError ? err.message : `❌ unexpected error: ${err.stack ?? String(err)}`;
|
|
138
|
+
process.stderr.write(cleanDiagnosticText(message, 8192) + '\n');
|
|
139
|
+
if (code === EXIT.USAGE && sub && sub !== 'help')
|
|
140
|
+
process.stderr.write('Usage: ' + SUB_SPECS[sub].usage);
|
|
141
|
+
if (json) {
|
|
142
|
+
process.stdout.write(JSON.stringify({ ok: false, exitCode: code, error: cleanDiagnosticText(message.replace(/^❌\s*/, ''), 2048) }) + '\n');
|
|
143
|
+
}
|
|
144
|
+
return code;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract constants for `remixmate template` (design §11 K1 / K5 / K6).
|
|
3
|
+
*
|
|
4
|
+
* STARTER_VERSION pins the exact starter release this cli was tested against.
|
|
5
|
+
* Bumping it is a deliberate cli release, never a range — `init` must produce
|
|
6
|
+
* the same project on every machine for a given cli version.
|
|
7
|
+
*/
|
|
8
|
+
export declare const STARTER_PACKAGE = "@remixmate/template-starter";
|
|
9
|
+
export declare const STARTER_VERSION = "0.1.0";
|
|
10
|
+
/** Local tarball override (testing a starter before it is published). */
|
|
11
|
+
export declare const STARTER_TARBALL_ENV = "REMIXMATE_STARTER_TARBALL";
|
|
12
|
+
/** Marker file that identifies a starter project root (K3). */
|
|
13
|
+
export declare const CHECK_ENTRY = ".remixmate/check/index.mjs";
|
|
14
|
+
/** Per-template identity file written by publish (K4). */
|
|
15
|
+
export declare const IDENTITY_FILE = ".remixmate.json";
|
|
16
|
+
export declare const DEFAULT_RENDER_BASE_URL = "https://api-render.remixmate.ai";
|
|
17
|
+
/** Draft preview limits enforced by ab-render /renderDraft (K5). */
|
|
18
|
+
export declare const PREVIEW_MAX_LONG_EDGE = 1280;
|
|
19
|
+
export declare const PREVIEW_MAX_SHORT_EDGE = 720;
|
|
20
|
+
export declare const PREVIEW_MAX_SECONDS = 60;
|
|
21
|
+
/**
|
|
22
|
+
* Same list as template-library `publish-private.mjs` TAR_EXCLUDES. `dsl-example.json`
|
|
23
|
+
* is deliberately kept: it is the reference DSL the registry ships with the template.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TAR_EXCLUDES: readonly string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract constants for `remixmate template` (design §11 K1 / K5 / K6).
|
|
3
|
+
*
|
|
4
|
+
* STARTER_VERSION pins the exact starter release this cli was tested against.
|
|
5
|
+
* Bumping it is a deliberate cli release, never a range — `init` must produce
|
|
6
|
+
* the same project on every machine for a given cli version.
|
|
7
|
+
*/
|
|
8
|
+
export const STARTER_PACKAGE = '@remixmate/template-starter';
|
|
9
|
+
export const STARTER_VERSION = '0.1.0';
|
|
10
|
+
/** Local tarball override (testing a starter before it is published). */
|
|
11
|
+
export const STARTER_TARBALL_ENV = 'REMIXMATE_STARTER_TARBALL';
|
|
12
|
+
/** Marker file that identifies a starter project root (K3). */
|
|
13
|
+
export const CHECK_ENTRY = '.remixmate/check/index.mjs';
|
|
14
|
+
/** Per-template identity file written by publish (K4). */
|
|
15
|
+
export const IDENTITY_FILE = '.remixmate.json';
|
|
16
|
+
export const DEFAULT_RENDER_BASE_URL = 'https://api-render.remixmate.ai';
|
|
17
|
+
/** Draft preview limits enforced by ab-render /renderDraft (K5). */
|
|
18
|
+
export const PREVIEW_MAX_LONG_EDGE = 1280;
|
|
19
|
+
export const PREVIEW_MAX_SHORT_EDGE = 720;
|
|
20
|
+
export const PREVIEW_MAX_SECONDS = 60;
|
|
21
|
+
/**
|
|
22
|
+
* Same list as template-library `publish-private.mjs` TAR_EXCLUDES. `dsl-example.json`
|
|
23
|
+
* is deliberately kept: it is the reference DSL the registry ships with the template.
|
|
24
|
+
*/
|
|
25
|
+
export const TAR_EXCLUDES = [
|
|
26
|
+
'__pycache__',
|
|
27
|
+
'node_modules',
|
|
28
|
+
'dist',
|
|
29
|
+
'.DS_Store',
|
|
30
|
+
'*.test.*',
|
|
31
|
+
'*.spec.*',
|
|
32
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template init <dir> [--force]` — scaffold a starter project.
|
|
3
|
+
*
|
|
4
|
+
* Only Node + npm are required (requirement 3.1): the starter is fetched with
|
|
5
|
+
* `npm pack` (works with whatever registry/proxy npm is configured for), unpacked
|
|
6
|
+
* with the system `tar` (bsdtar ships with Windows 10+), then `npm install`ed.
|
|
7
|
+
* No login is needed (requirement 3.5).
|
|
8
|
+
*/
|
|
9
|
+
export interface InitOptions {
|
|
10
|
+
dir: string;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
/** Skip `npm install` (offline tests, or users who want to pick their own installer). */
|
|
14
|
+
skipInstall?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface InitResult {
|
|
17
|
+
ok: true;
|
|
18
|
+
dir: string;
|
|
19
|
+
starter: string;
|
|
20
|
+
installed: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function runInit(opts: InitOptions): Promise<InitResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate template init <dir> [--force]` — scaffold a starter project.
|
|
3
|
+
*
|
|
4
|
+
* Only Node + npm are required (requirement 3.1): the starter is fetched with
|
|
5
|
+
* `npm pack` (works with whatever registry/proxy npm is configured for), unpacked
|
|
6
|
+
* with the system `tar` (bsdtar ships with Windows 10+), then `npm install`ed.
|
|
7
|
+
* No login is needed (requirement 3.5).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { EXIT, SkillError } from '../errors.js';
|
|
13
|
+
import { emitProgress } from '../progress.js';
|
|
14
|
+
import { STARTER_PACKAGE, STARTER_TARBALL_ENV, STARTER_VERSION } from './constants.js';
|
|
15
|
+
import { Output } from './project.js';
|
|
16
|
+
import { runNpm, runProcess } from './proc.js';
|
|
17
|
+
function isNonEmptyDir(dir) {
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
return false;
|
|
20
|
+
if (!statSync(dir).isDirectory()) {
|
|
21
|
+
throw new SkillError(`❌ ${dir} 已存在且不是目录`, EXIT.USAGE);
|
|
22
|
+
}
|
|
23
|
+
return readdirSync(dir).length > 0;
|
|
24
|
+
}
|
|
25
|
+
/** Obtain the starter tarball: local override, else `npm pack` into `tmp`. */
|
|
26
|
+
async function obtainTarball(tmp, out) {
|
|
27
|
+
const override = (process.env[STARTER_TARBALL_ENV] ?? '').trim();
|
|
28
|
+
if (override) {
|
|
29
|
+
const file = path.resolve(override);
|
|
30
|
+
if (!existsSync(file)) {
|
|
31
|
+
throw new SkillError(`❌ ${STARTER_TARBALL_ENV} 指向的文件不存在: ${file}`, EXIT.USAGE);
|
|
32
|
+
}
|
|
33
|
+
out.info(`使用本地 starter 包: ${file}`);
|
|
34
|
+
return { file, label: `local:${path.basename(file)}` };
|
|
35
|
+
}
|
|
36
|
+
const spec = `${STARTER_PACKAGE}@${STARTER_VERSION}`;
|
|
37
|
+
out.info(`下载 ${spec} …`);
|
|
38
|
+
const res = await runNpm(['pack', spec, '--json', '--silent'], { cwd: tmp, stdio: 'pipe' });
|
|
39
|
+
if (res.code !== 0) {
|
|
40
|
+
const tail = res.stderr.trim().split('\n').slice(-5).join('\n');
|
|
41
|
+
throw new SkillError(`❌ npm pack ${spec} 失败(退出码 ${res.code})。检查网络与 npm registry 配置。\n${tail}`, EXIT.BACKEND_UNREACHABLE);
|
|
42
|
+
}
|
|
43
|
+
let file;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(res.stdout);
|
|
46
|
+
if (parsed[0]?.filename)
|
|
47
|
+
file = path.join(tmp, parsed[0].filename);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Older npm without --json support: fall back to whatever .tgz landed in tmp.
|
|
51
|
+
}
|
|
52
|
+
file ??= readdirSync(tmp).filter((f) => f.endsWith('.tgz')).map((f) => path.join(tmp, f))[0];
|
|
53
|
+
if (!file || !existsSync(file))
|
|
54
|
+
throw new SkillError('❌ npm pack 未产出 tarball');
|
|
55
|
+
return { file, label: spec };
|
|
56
|
+
}
|
|
57
|
+
export async function runInit(opts) {
|
|
58
|
+
const out = new Output(Boolean(opts.json));
|
|
59
|
+
if (!opts.dir)
|
|
60
|
+
throw new SkillError('❌ 缺少目标目录:remixmate template init <dir>', EXIT.USAGE);
|
|
61
|
+
const dir = path.resolve(opts.dir);
|
|
62
|
+
if (isNonEmptyDir(dir) && !opts.force) {
|
|
63
|
+
throw new SkillError(`❌ 目标目录非空: ${dir}\n 换一个空目录,或确认可以覆盖后加 --force。`, EXIT.USAGE);
|
|
64
|
+
}
|
|
65
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'remixmate-starter-'));
|
|
66
|
+
try {
|
|
67
|
+
emitProgress({ phase: 'template-init:fetch', progress: 0.1 });
|
|
68
|
+
const { file, label } = await obtainTarball(tmp, out);
|
|
69
|
+
emitProgress({ phase: 'template-init:extract', progress: 0.4 });
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
// npm tarballs are rooted at `package/` — strip it so files land directly in <dir>.
|
|
72
|
+
const tar = await runProcess('tar', ['-xzf', file, '-C', dir, '--strip-components=1'], { stdio: 'pipe' });
|
|
73
|
+
if (tar.code !== 0) {
|
|
74
|
+
throw new SkillError(`❌ 解压 starter 失败(tar 退出码 ${tar.code}): ${tar.stderr.trim().slice(0, 300)}`);
|
|
75
|
+
}
|
|
76
|
+
let installed = false;
|
|
77
|
+
if (!opts.skipInstall) {
|
|
78
|
+
emitProgress({ phase: 'template-init:install', progress: 0.6 });
|
|
79
|
+
out.info('安装依赖(npm install)…');
|
|
80
|
+
const inst = await runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'stderr' });
|
|
81
|
+
if (inst.code !== 0) {
|
|
82
|
+
throw new SkillError(`❌ npm install 失败(退出码 ${inst.code})。工程文件已解压到 ${dir},修复后在该目录重跑 npm install。`);
|
|
83
|
+
}
|
|
84
|
+
installed = true;
|
|
85
|
+
}
|
|
86
|
+
emitProgress({ phase: 'template-init:done', progress: 1 });
|
|
87
|
+
printNextSteps(out, dir, installed);
|
|
88
|
+
const result = { ok: true, dir, starter: label, installed };
|
|
89
|
+
out.result({ ...result });
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function printNextSteps(out, dir, installed) {
|
|
97
|
+
const rel = path.relative(process.cwd(), dir) || '.';
|
|
98
|
+
out.info('');
|
|
99
|
+
out.info(`✅ 模板工程已创建: ${dir}`);
|
|
100
|
+
out.info('');
|
|
101
|
+
out.info('下一步:');
|
|
102
|
+
out.info(` cd ${rel}`);
|
|
103
|
+
if (!installed)
|
|
104
|
+
out.info(' npm install');
|
|
105
|
+
out.info(' npm run dev # 打开 Remotion Studio 本地预览');
|
|
106
|
+
out.info(' remixmate template check <name> # 本地校验(无需登录)');
|
|
107
|
+
out.info(' remixmate template preview <name> # 远程渲染预览 mp4(需登录)');
|
|
108
|
+
out.info(' remixmate template publish <name> # 发布为你的私有模板(需登录)');
|
|
109
|
+
out.info('');
|
|
110
|
+
out.info('在本地 agent 里使用:');
|
|
111
|
+
out.info(' Codex / WorkBuddy 在工程目录打开,agent 会读取 AGENTS.md');
|
|
112
|
+
out.info(' Claude Code 在工程目录打开,agent 会读取 CLAUDE.md(与 AGENTS.md 内容相同)');
|
|
113
|
+
out.info(' TraeWork 只认项目级 skill:在工程目录执行一次 remixmate install --project');
|
|
114
|
+
out.info(' 其它宿主若还没装 skill:remixmate install');
|
|
115
|
+
}
|