返回 html-video
project.ts
根目录 / packages / cli / src / commands / project.ts
1 /**
2 * Project-centric CLI commands per RFC-05.
3 */
4
5 import { readFile } from 'node:fs/promises';
6 import { existsSync } from 'node:fs';
7 import { resolve } from 'node:path';
8 import type { CliContext } from '../context.js';
9 import { fail, ok, progress } from '../output.js';
10
11 export async function projectCreate(
12 ctx: CliContext,
13 opts: { name: string; intent?: string; aspect?: string; commercial?: boolean },
14 ): Promise<void> {
15 if (!opts.name) fail('invalid-input', '--name required');
16 const project = await ctx.orchestrator.create({
17 name: opts.name,
18 ...(opts.intent !== undefined && { intent: opts.intent }),
19 preferences: {
20 ...(opts.aspect !== undefined && { aspect: opts.aspect }),
21 ...(opts.commercial !== undefined && { commercial: opts.commercial }),
22 },
23 });
24 ok({ project_id: project.id, name: project.name, status: project.status });
25 }
26
27 export async function projectList(ctx: CliContext): Promise<void> {
28 const projects = await ctx.orchestrator.list();
29 ok({
30 projects: projects.map((p) => ({
31 id: p.id,
32 name: p.name,
33 template_id: p.templateId,
34 asset_count: p.assets.length,
35 status: p.status,
36 updated_at: p.updatedAt,
37 })),
38 });
39 }
40
41 export async function projectShow(ctx: CliContext, id: string): Promise<void> {
42 const project = await ctx.orchestrator.load(id);
43 ok({ project });
44 }
45
46 export async function projectDelete(ctx: CliContext, id: string): Promise<void> {
47 await ctx.orchestrator.remove(id);
48 ok({ project_id: id, deleted: true });
49 }
50
51 export async function projectAddAsset(
52 ctx: CliContext,
53 id: string,
54 opts: {
55 file?: string;
56 inlineText?: string;
57 inlineDataFile?: string;
58 caption?: string;
59 },
60 ): Promise<void> {
61 let project;
62 if (opts.file) {
63 const f = resolve(opts.file);
64 if (!existsSync(f)) fail('asset-not-found', `File not found: ${f}`);
65 project = await ctx.orchestrator.addFileAsset(id, f, opts.caption);
66 } else if (opts.inlineText) {
67 project = await ctx.orchestrator.addInlineAsset(id, opts.inlineText, 'text', opts.caption);
68 } else if (opts.inlineDataFile) {
69 const f = resolve(opts.inlineDataFile);
70 if (!existsSync(f)) fail('asset-not-found', `Data file not found: ${f}`);
71 const content = await readFile(f, 'utf8');
72 project = await ctx.orchestrator.addInlineAsset(id, content, 'data', opts.caption);
73 } else {
74 fail('invalid-input', 'Provide one of --file, --inline-text, --inline-data-file');
75 }
76 ok({
77 project_id: project!.id,
78 asset_count: project!.assets.length,
79 last_added: project!.assets[project!.assets.length - 1],
80 });
81 }
82
83 export async function projectRemoveAsset(
84 ctx: CliContext,
85 id: string,
86 assetId: string,
87 ): Promise<void> {
88 const project = await ctx.orchestrator.removeAsset(id, assetId);
89 ok({ project_id: project.id, asset_count: project.assets.length });
90 }
91
92 export async function projectSetTemplate(
93 ctx: CliContext,
94 id: string,
95 templateId: string,
96 ): Promise<void> {
97 const project = await ctx.orchestrator.setTemplate(id, templateId);
98 ok({
99 project_id: project.id,
100 template_id: project.templateId,
101 variables: project.variables,
102 });
103 }
104
105 export async function projectSetVar(
106 ctx: CliContext,
107 id: string,
108 key: string,
109 valueJson: string,
110 ): Promise<void> {
111 let value: unknown;
112 try {
113 value = JSON.parse(valueJson);
114 } catch {
115 // not JSON → keep raw string
116 value = valueJson;
117 }
118 const project = await ctx.orchestrator.setVariable(id, key, value);
119 ok({ project_id: project.id, variables: project.variables });
120 }
121
122 export async function projectSetVars(
123 ctx: CliContext,
124 id: string,
125 varsFile: string,
126 ): Promise<void> {
127 const f = resolve(varsFile);
128 if (!existsSync(f)) fail('invalid-input', `vars file not found: ${f}`);
129 const vars = JSON.parse(await readFile(f, 'utf8')) as Record<string, unknown>;
130 const project = await ctx.orchestrator.setVariables(id, vars);
131 ok({ project_id: project.id, variables: project.variables });
132 }
133
134 export async function projectPreview(ctx: CliContext, id: string): Promise<void> {
135 const { project, htmlPath } = await ctx.orchestrator.renderPreviewHtml(id);
136 ok({
137 project_id: project.id,
138 html_path: htmlPath,
139 poster_path: project.lastPreviewPosterPath,
140 note: 'Open html_path in a browser to preview, or use `html-video studio` for full UI.',
141 });
142 }
143
144 export async function projectRender(
145 ctx: CliContext,
146 id: string,
147 opts: { output?: string; streamProgress?: boolean },
148 ): Promise<void> {
149 const { project, outputPath } = await ctx.orchestrator.exportMp4({
150 projectId: id,
151 ...(opts.output !== undefined && { outputPath: resolve(opts.output) }),
152 onProgress: opts.streamProgress ? (pct, stage) => progress(stage, pct) : undefined,
153 });
154 ok({ project_id: project.id, output_path: outputPath, status: project.status });
155 }
156
156 lines TYPESCRIPT