@clize/clize 0.9.1 → 0.10.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 (46) hide show
  1. package/README.md +19 -8
  2. package/dist/cli.js +386 -108
  3. package/dist/cli.js.map +1 -1
  4. package/dist/context.js +20 -2
  5. package/dist/context.js.map +1 -1
  6. package/dist/core/addresses.js +10 -3
  7. package/dist/core/addresses.js.map +1 -1
  8. package/dist/core/autopay.js +6 -0
  9. package/dist/core/autopay.js.map +1 -1
  10. package/dist/core/billing.js +9 -0
  11. package/dist/core/billing.js.map +1 -1
  12. package/dist/core/domains.js +84 -26
  13. package/dist/core/domains.js.map +1 -1
  14. package/dist/core/email.js +54 -11
  15. package/dist/core/email.js.map +1 -1
  16. package/dist/core/handle.js +19 -3
  17. package/dist/core/handle.js.map +1 -1
  18. package/dist/core/media.js +55 -10
  19. package/dist/core/media.js.map +1 -1
  20. package/dist/core/projects.js +217 -0
  21. package/dist/core/projects.js.map +1 -0
  22. package/dist/core/serve.js +170 -0
  23. package/dist/core/serve.js.map +1 -0
  24. package/dist/core/setup.js +89 -6
  25. package/dist/core/setup.js.map +1 -1
  26. package/dist/core/sites.js +3 -0
  27. package/dist/core/sites.js.map +1 -1
  28. package/dist/core/video.js +387 -0
  29. package/dist/core/video.js.map +1 -0
  30. package/dist/index.js +52 -11
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers/email/cloudflare-inbound.js +30 -12
  33. package/dist/providers/email/cloudflare-inbound.js.map +1 -1
  34. package/dist/providers/media/google.js +3 -0
  35. package/dist/providers/media/google.js.map +1 -1
  36. package/dist/providers/media/kunavo.js +11 -4
  37. package/dist/providers/media/kunavo.js.map +1 -1
  38. package/dist/providers/media/openai.js +1 -0
  39. package/dist/providers/media/openai.js.map +1 -1
  40. package/dist/remote.js +63 -12
  41. package/dist/remote.js.map +1 -1
  42. package/dist/state/file-store.js +75 -5
  43. package/dist/state/file-store.js.map +1 -1
  44. package/package.json +18 -5
  45. package/skills/clize/SKILL.md +21 -7
  46. package/skills/clize-site-build/SKILL.md +12 -11
package/dist/cli.js CHANGED
@@ -20,9 +20,12 @@ import * as addressesCore from "./core/addresses.js";
20
20
  import * as handleCore from "./core/handle.js";
21
21
  import * as sitesCore from "./core/sites.js";
22
22
  import * as setupCore from "./core/setup.js";
23
+ import * as projectsCore from "./core/projects.js";
23
24
  import * as billingCore from "./core/billing.js";
24
25
  import * as installCore from "./core/install.js";
25
26
  import * as mediaCore from "./core/media.js";
27
+ import * as videoCore from "./core/video.js";
28
+ import * as serveCore from "./core/serve.js";
26
29
  import { selfcheck } from "./selfcheck.js";
27
30
  import * as remoteClient from "./remote.js";
28
31
  import { VERSION } from "./version.js";
@@ -30,7 +33,8 @@ import { VERSION } from "./version.js";
30
33
  const remote = remoteClient.loadRemote();
31
34
  // 本地 ctx 惰性构造:remote 模式下完全不碰 CF / FileStore。
32
35
  let _ctx;
33
- const ctx = () => (_ctx ??= createLocalContext());
36
+ let projectOverride; // -p <slug>:覆盖当前目录检出(clize.json)
37
+ const ctx = () => (_ctx ??= createLocalContext({ projectSlug: projectOverride }));
34
38
  /** 统一输出:成功打印美化 JSON(字符串原样),失败 → stderr + 退出码 1。 */
35
39
  async function emit(value) {
36
40
  try {
@@ -92,7 +96,7 @@ async function pairingLogin(apiOpt) {
92
96
  ok: true,
93
97
  mode: "remote",
94
98
  api,
95
- message: "已登录;凭证已保存到 ~/.clize。跑 clize whoami / balance 试试。",
99
+ message: "已登录;凭证已保存到 ~/.clize。跑 clize status / balance 试试。",
96
100
  };
97
101
  }
98
102
  /** 把 --attach 的逗号分隔路径读成邮件附件(base64);读不到则报错。 */
@@ -136,6 +140,30 @@ function guessContentType(p) {
136
140
  };
137
141
  return map[path.extname(p).toLowerCase()] ?? "application/octet-stream";
138
142
  }
143
+ /** 参考图路径 → data URI(托管模式上传用:Worker 无用户文件系统,CLI 端读盘转好;已是 URL / data URI 原样透传)。 */
144
+ function refsToDataUris(paths) {
145
+ return paths.map((p) => {
146
+ if (p.startsWith("data:") || p.startsWith("http://") || p.startsWith("https://"))
147
+ return p;
148
+ let buf;
149
+ try {
150
+ buf = fs.readFileSync(p);
151
+ }
152
+ catch {
153
+ throw new Error(`读不到参考图:${p}`);
154
+ }
155
+ return `data:${guessContentType(p)};base64,${buf.toString("base64")}`;
156
+ });
157
+ }
158
+ /** 解析逗号分隔的 --ref 路径列表。 */
159
+ function splitRefs(spec) {
160
+ return spec
161
+ ? String(spec)
162
+ .split(",")
163
+ .map((s) => s.trim())
164
+ .filter(Boolean)
165
+ : [];
166
+ }
139
167
  /** 托管模式异步生成(视频 / 音乐):提交 →(默认)long-poll 到完成并落盘 /(--async)返 job id。 */
140
168
  async function remoteGenAsync(rc, modality, body, opts) {
141
169
  const submit = (await (modality === "video"
@@ -164,10 +192,38 @@ async function remoteGenAsync(rc, modality, body, opts) {
164
192
  if (st.state === "failed")
165
193
  throw new Error(`生成失败${st.error ? `:${st.error}` : ""}(作数据看待,别盲目重试)`);
166
194
  if (Date.now() - start >= timeoutMs)
167
- return { ...st, message: `已等 ${Number(opts.timeout) || 300}s 仍在生成 —— clize gen status ${id} 回头拉结果。` };
195
+ // timedOut=true + next:机器可读 —— 任务没失败也没丢,别重新提交,照 next 轮询。
196
+ return {
197
+ ...st,
198
+ timedOut: true,
199
+ next: `clize gen status ${id}`,
200
+ message: `已等 ${Number(opts.timeout) || 300}s 仍在生成 —— clize gen status ${id} 回头拉结果。`,
201
+ };
168
202
  }
169
203
  }
170
204
  const program = new Command();
205
+ /** 给命令挂 -p/--project(指定项目,覆盖当前目录的检出;全局 preAction hook 统一取值)。 */
206
+ const withProject = (cmd) => cmd.option("-p, --project <slug>", "指定项目(覆盖当前目录的 clize use 检出)");
207
+ /** actionCmd 是否在某命令树下(沿祖先链查;三级嵌套用 parent 单层判断会漏)。 */
208
+ const inCommandTree = (cmd, name) => {
209
+ for (let c = cmd; c; c = c.parent ?? null)
210
+ if (c.name() === name)
211
+ return true;
212
+ return false;
213
+ };
214
+ // 全局 -p:命令动作前从该命令的 --project 选项取值 → 本地进 ctx.projectSlug,remote 由 call() 统一注入 ?project=。
215
+ program.hook("preAction", (_thisCmd, actionCmd) => {
216
+ // build 树(及 design 兼容别名树)排除:--project 在 build site start 下是「设计项目名」
217
+ // (给方法后端做记录),不是资源项目 slug。build clip render 仍要 -p(挂了 withProject,
218
+ // 但 render 不在排除外…… render 在 build 树内会被排除!)——
219
+ // 例外:withProject 挂过的命令(render)以自己的 -p 为准,不排除。
220
+ const hasOwnP = actionCmd.options.some((o) => o.long === "--project" && o.short === "-p");
221
+ const excluded = !hasOwnP && (inCommandTree(actionCmd, "build") || inCommandTree(actionCmd, "design"));
222
+ projectOverride = excluded
223
+ ? undefined
224
+ : actionCmd.opts().project || undefined;
225
+ remoteClient.setProjectScope(projectOverride); // undefined → call() 回落 clize.json 检出
226
+ });
171
227
  program
172
228
  .name("clize")
173
229
  .description("Clize —— 给 agent 的真实世界能力层:域名 / 邮箱 / 部署 / 媒体")
@@ -182,6 +238,10 @@ program.hook("preAction", (_thisCmd, actionCmd) => {
182
238
  // 生成式媒体(gen *):本地只需自带 OPENAI / GEMINI key(由 core 自检),不要求 CF / clize 凭证。
183
239
  if (actionCmd.name() === "gen" || actionCmd.parent?.name() === "gen")
184
240
  return;
241
+ // 作品域(build *,含 design 兼容别名树):hosted 方法命令自带比通用闸更准的登录引导;
242
+ // build clip check 纯本地零凭证、render 同 gen 由 core 自检。
243
+ if (inCommandTree(actionCmd, "build") || inCommandTree(actionCmd, "design"))
244
+ return;
185
245
  if (remote || remoteClient.hasLocalCfCreds())
186
246
  return;
187
247
  console.error("❌ 未登录。\n" +
@@ -190,11 +250,10 @@ program.hook("preAction", (_thisCmd, actionCmd) => {
190
250
  process.exit(1);
191
251
  });
192
252
  // ───────── 总览 / 自检 / 审计 ─────────
193
- program
253
+ withProject(program
194
254
  .command("status")
195
255
  .description("工作空间总览:域名、邮箱 / 站点状态、本月开销;--assets 加到期巡检")
196
- .option("--assets", "含注册商处真实到期列表(资产健康)")
197
- .action((opts) => emit(remote
256
+ .option("--assets", "含注册商处真实到期列表(资产健康)")).action((opts) => emit(remote
198
257
  ? opts.assets
199
258
  ? remoteClient.api.statusAssets(remote)
200
259
  : remoteClient.api.status(remote)
@@ -227,10 +286,69 @@ program
227
286
  .action(() => emit(setupCore.clearRemoteLogin()));
228
287
  program
229
288
  .command("init")
230
- .description("在当前目录绑定一个 clize 身份(写 ./clize.json)")
289
+ .description("目录即项目:绑定身份 + 检出项目一步完成(写 ./clize.json;项目实体首次 claim/buy 时自动建)")
231
290
  .option("--handle <slug>", "免费 handle")
232
291
  .option("--domain <domain>", "自定义域名")
233
292
  .action((opts) => emit(setupCore.initProject({ handle: opts.handle, domain: opts.domain })));
293
+ // ───────── 项目(租户下的资源分组;域名为归属锚点)─────────
294
+ const projects = program
295
+ .command("projects")
296
+ .description("项目:列表 / 新建 / 挪资源 / 改名 / 删除(不带子命令 = 列表)");
297
+ projects
298
+ .command("list", { isDefault: true })
299
+ .description("列出你的所有项目(各带域名数)")
300
+ .action(() => emit(remote ? remoteClient.api.projectsList(remote) : projectsCore.listProjects(ctx())));
301
+ projects
302
+ .command("new <name>")
303
+ .description('新建项目(名字带空格用引号,如 "Shop A";slug 缺省由名字规整)')
304
+ .option("--slug <slug>", "项目标识(租户内唯一,CLI/URL 友好)")
305
+ .option("--use", "建完顺手把当前目录检出到它(写 ./clize.json)")
306
+ .action((name, opts) => emit((async () => {
307
+ const rec = (remote
308
+ ? await remoteClient.api.projectCreate(remote, name, opts.slug)
309
+ : await projectsCore.createProject(ctx(), { name, slug: opts.slug }));
310
+ // 输出瘦身:slug/name 足矣(id/tenantId/createdAt 是内部细节,不进 transcript)。
311
+ const lean = { slug: rec?.slug, name: rec?.name };
312
+ if (opts.use && rec?.slug)
313
+ return { ...lean, ...setupCore.useProject(rec.slug) };
314
+ return {
315
+ ...lean,
316
+ nextSteps: [
317
+ `检出到本目录(此后命令默认作用于它):clize use ${rec?.slug}`,
318
+ `或单次指定:任意命令加 -p ${rec?.slug}`,
319
+ ],
320
+ };
321
+ })()));
322
+ projects
323
+ .command("move <domain> <project>")
324
+ .description("把域名挪进某项目(它的邮箱地址跟着走;媒体资产不动)")
325
+ .action((d, p) => emit(remote ? remoteClient.api.projectMove(remote, d, p) : projectsCore.moveDomain(ctx(), d, p)));
326
+ projects
327
+ .command("rename <slug>")
328
+ .description("改项目显示名 / 标识(default 不能改标识)")
329
+ .option("--name <name>", "新显示名")
330
+ .option("--slug <newSlug>", "新标识(检出过旧标识的目录要重新 clize use)")
331
+ .action((slug, opts) => emit(remote
332
+ ? remoteClient.api.projectRename(remote, slug, { name: opts.name, slug: opts.slug })
333
+ : projectsCore.renameProject(ctx(), slug, { name: opts.name, slug: opts.slug })));
334
+ projects
335
+ .command("rm <slug>")
336
+ .description("删项目(default 不可删;非空项目必须 --into 指定资源去向,绝不级联删资源)")
337
+ .option("--into <project>", "把项目下资源(域名 + 媒体资产)归并到哪个项目,如 default")
338
+ .action((slug, opts) => emit(remote
339
+ ? remoteClient.api.projectRemove(remote, slug, opts.into)
340
+ : projectsCore.removeProject(ctx(), slug, { into: opts.into })));
341
+ program
342
+ .command("use <project>")
343
+ .description("把当前目录切到某个项目(此后本目录的命令默认作用于它;写 ./clize.json)")
344
+ .action((project) => emit((async () => {
345
+ const list = (remote
346
+ ? await remoteClient.api.projectsList(remote)
347
+ : await projectsCore.listProjects(ctx()));
348
+ if (!Array.isArray(list) || !list.some((p) => p.slug === project))
349
+ throw new Error(`没有叫 "${project}" 的项目;先看看有哪些:clize projects(新建:clize projects new "<名字>")`);
350
+ return setupCore.useProject(project);
351
+ })()));
234
352
  program
235
353
  .command("install")
236
354
  .description("把 clize 接进本机 coding agent(Claude Code / Codex):默认只铺 skill;加 --mcp 才另登记 MCP server")
@@ -254,11 +372,10 @@ program
254
372
  .command("check [domain]")
255
373
  .description("连通性自检:验证凭证 / 账户 / 域名查价(只读,不花钱)")
256
374
  .action((domain) => remote ? emit(remoteClient.check(remote)) : selfcheck(ctx(), domain));
257
- program
375
+ withProject(program
258
376
  .command("audit")
259
- .description("查花钱 / 对外动作的审计记录")
260
- .option("-n, --limit <n>", "返回条数", "20")
261
- .action((opts) => emit(remote
377
+ .description("查花钱 / 对外动作的审计记录(检出项目时只看该项目的花钱动作)")
378
+ .option("-n, --limit <n>", "返回条数", "20")).action((opts) => emit(remote
262
379
  ? remoteClient.api.audit(remote, Number(opts.limit))
263
380
  : domainsCore.auditLog(ctx(), Number(opts.limit))));
264
381
  program
@@ -390,15 +507,13 @@ billing
390
507
  return emit(Promise.reject(new Error("指定 --on / --off / --amount 之一。")));
391
508
  return emit(remoteClient.api.billingAutopay(remote, patch));
392
509
  });
393
- program
510
+ withProject(program
394
511
  .command("context [address]")
395
- .description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")
396
- .action((address) => emit(remote ? remoteClient.api.context(remote, address) : addressesCore.addressContext(ctx(), address)));
397
- program
512
+ .description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")).action((address) => emit(remote ? remoteClient.api.context(remote, address) : addressesCore.addressContext(ctx(), address)));
513
+ withProject(program
398
514
  .command("claim <slug>")
399
- .description("占一个免费 handle <slug>.clize.app(先到先得;默认自动开 support@ 收信)")
400
- .option("--no-email", "只占名,不自动开邮箱")
401
- .action((slug, opts) => emit(remote
515
+ .description("占一个免费 handle <slug>.clize.app(先到先得;默认自动开 support@ 收信;归当前检出项目)")
516
+ .option("--no-email", "只占名,不自动开邮箱")).action((slug, opts) => emit(remote
402
517
  ? remoteClient.api.claim(remote, slug)
403
518
  : handleCore.claimHandle(ctx(), slug, { provisionEmail: opts.email })));
404
519
  program
@@ -422,12 +537,11 @@ domain
422
537
  supportedTlds: domainsCore.SUPPORTED_TLDS,
423
538
  note: "Cloudflare 注册商 beta;.co / .io / .ai / .consulting / .agency 等暂不支持。",
424
539
  }));
425
- domain
540
+ withProject(domain
426
541
  .command("buy <domain>")
427
- .description("注册域名(💰 花钱:必须 --confirm;缺则只返报价)")
542
+ .description("注册域名(💰 花钱:必须 --confirm;缺则只返报价;归当前检出项目)")
428
543
  .option("--confirm", "确认花钱注册")
429
- .option("--no-auto-renew", "关闭自动续费(默认开,防过期)")
430
- .action((d, opts) => {
544
+ .option("--no-auto-renew", "关闭自动续费(默认开,防过期)")).action((d, opts) => {
431
545
  if (remote)
432
546
  return emit(remoteClient.api.domainBuy(remote, d, !!opts.confirm, opts.autoRenew));
433
547
  if (!opts.confirm) {
@@ -438,23 +552,17 @@ domain
438
552
  }
439
553
  return emit(domainsCore.registerDomain(ctx(), d, { autoRenew: opts.autoRenew }));
440
554
  });
441
- domain
442
- .command("import <domain>")
443
- .description("接入已有域名(迁 NS 托管,幂等)")
444
- .action((d) => emit(remote ? remoteClient.api.domainImport(remote, d) : domainsCore.importDomain(ctx(), d)));
445
- domain
446
- .command("list")
447
- .description("列出本租户的域名")
448
- .action(() => emit(remote ? remoteClient.api.domainList(remote) : domainsCore.listDomains(ctx())));
555
+ withProject(domain.command("import <domain>").description("接入已有域名(迁 NS 托管,幂等;新接入归当前检出项目)")).action((d) => emit(remote ? remoteClient.api.domainImport(remote, d) : domainsCore.importDomain(ctx(), d)));
556
+ withProject(domain.command("list").description("列出本租户的域名(检出项目时只列该项目)")).action(() => emit(remote ? remoteClient.api.domainList(remote) : domainsCore.listDomains(ctx())));
449
557
  // ───────── 邮箱 ─────────
450
558
  const email = program.command("email").description("邮箱:配置 / 发信 / 收信");
451
559
  email
452
560
  .command("setup <domain>")
453
561
  .description("配自定义域收发链路(MX / SPF + 发信域名验证)")
454
562
  .action((d) => emit(remote ? remoteClient.api.emailSetup(remote, d) : emailCore.setupEmail(ctx(), d)));
455
- email
563
+ withProject(email
456
564
  .command("send")
457
- .description("发信(📨 身份对外硬闸:默认只出草稿,加 --confirm 才真发)")
565
+ .description("发信(📨 身份对外硬闸:默认只出草稿,加 --confirm 才真发)"))
458
566
  .requiredOption("--to <addr>", "收件人(逗号分隔多个)")
459
567
  .requiredOption("--subject <s>", "主题")
460
568
  .option("--from <addr>", "发件人(默认项目地址)")
@@ -462,23 +570,30 @@ email
462
570
  .option("--html <h>", "HTML 正文")
463
571
  .option("--attach <paths>", "附件路径(逗号分隔多个;如 gen image 生成的图)")
464
572
  .option("--confirm", "确认发送(缺省只返回草稿供人核)")
573
+ .option("--allow-duplicate", "越过防重发闸(10 分钟内同收件人同主题默认拒发,防重试重复发信)")
465
574
  .action((opts) => {
466
575
  const to = String(opts.to).includes(",")
467
576
  ? String(opts.to).split(",").map((s) => s.trim())
468
577
  : opts.to;
469
578
  const attachments = loadAttachments(opts.attach);
470
- // 托管模式:from 必填(无本地 clize.json 推断),confirm 透传给后端裁决草稿/真发。
579
+ // 托管模式:from 缺省同样从本地 clize.json 推导(CLI 在用户机器上跑,读得到);
580
+ // confirm 透传给后端裁决草稿/真发。
471
581
  if (remote) {
472
- if (!opts.from)
473
- return emit(Promise.reject(new Error("托管模式发信需 --from <地址>")));
582
+ const rSender = opts.from
583
+ ? { from: opts.from, replyTo: undefined }
584
+ : setupCore.projectSender();
585
+ if (!rSender?.from)
586
+ return emit(Promise.reject(new Error("需 --from <地址>,或先 `clize init --handle <slug>` 绑定项目以自动带出发件人")));
474
587
  return emit(remoteClient.api.emailSend(remote, {
475
- from: opts.from,
588
+ from: rSender.from,
476
589
  to,
477
590
  subject: opts.subject,
478
591
  text: opts.text,
479
592
  html: opts.html,
593
+ replyTo: rSender.replyTo,
480
594
  attachments,
481
595
  confirm: !!opts.confirm,
596
+ allowDuplicate: !!opts.allowDuplicate,
482
597
  }));
483
598
  }
484
599
  // from 缺省时从项目(clize.json)带出发件人 + 回信地址
@@ -503,7 +618,7 @@ email
503
618
  },
504
619
  message: "📨 这是草稿,还没发。把它给用户看、确认后加 --confirm 才真发:clize email send … --confirm",
505
620
  });
506
- return emit(emailCore.sendEmail(ctx(), msg));
621
+ return emit(emailCore.sendEmail(ctx(), msg, { allowDuplicate: !!opts.allowDuplicate }));
507
622
  });
508
623
  email
509
624
  .command("inbox <domain>")
@@ -525,9 +640,16 @@ email
525
640
  .description("读单封邮件全文")
526
641
  .action((d, id) => emit(remote ? remoteClient.api.emailShow(remote, d, id) : emailCore.getMessage(ctx(), d, id)));
527
642
  email
528
- .command("thread <domain> <contact>")
529
- .description("读与某联系人的往来(<domain>=你的收信域,<contact>=对方地址)")
530
- .action((d, contact) => emit(remote ? remoteClient.api.emailThread(remote, d, contact) : emailCore.emailThread(ctx(), d, contact)));
643
+ .command("thread <contactOrDomain> [contact]")
644
+ .description("读与某联系人的往来。init 过的目录单参即可:email thread <对方地址>;双参 = <收信域> <对方地址>")
645
+ .action((a, b) => {
646
+ // 单参:a=联系人,收信域从 clize.json 推导(与 deploy 免 --domain、send 免 --from 同一红利)。
647
+ const domain = b ? a : setupCore.projectDomain();
648
+ const contact = b ?? a;
649
+ if (!domain)
650
+ return emit(Promise.reject(new Error("没绑定收信域:clize email thread <domain> <contact>,或先 clize init 绑定本目录后单参使用")));
651
+ return emit(remote ? remoteClient.api.emailThread(remote, domain, contact) : emailCore.emailThread(ctx(), domain, contact));
652
+ });
531
653
  email
532
654
  .command("inbox-setup <address>")
533
655
  .description("开启 agent 自持收信(Email Worker 接住入站,存收件箱)")
@@ -577,18 +699,16 @@ address
577
699
  .action((addr) => emit(remote
578
700
  ? remoteClient.api.emailAddressRemove(remote, addr)
579
701
  : addressesCore.removeAddress(ctx(), addr)));
580
- email
581
- .command("list")
582
- .description("列出所有地址 + tag")
583
- .action(() => emit(remote ? remoteClient.api.emailListAddresses(remote) : addressesCore.listAddresses(ctx())));
702
+ withProject(email.command("list").description("列出所有地址 + tag(检出项目时只列该项目域名下的)")).action(() => emit(remote ? remoteClient.api.emailListAddresses(remote) : addressesCore.listAddresses(ctx())));
584
703
  // ───────── 部署 ─────────
585
- program
704
+ withProject(program
586
705
  .command("deploy <path>")
587
706
  .description("部署:目录 + <slug>.clize.app → 免费 handle 多文件站;或 <名> --html 内联单页")
588
707
  .option("--domain <domain>", "目标域(<slug>.clize.app 走免费 handle)")
589
708
  .option("--html <html>", "内联 HTML(单页 worker,不读目录)")
590
- .option("--html-file <file>", "从文件读 HTML")
591
- .action((p, opts) => {
709
+ .option("--html-file <file>", "从文件读 HTML")).action((p, opts) => {
710
+ // 缺 --domain 时从 ./clize.json 推导(clize init 绑过的目录免显式传参,与 email send 推 from 对称)。
711
+ const domain = opts.domain ?? setupCore.projectDomain();
592
712
  if (remote) {
593
713
  let dir = false;
594
714
  try {
@@ -597,38 +717,71 @@ program
597
717
  catch {
598
718
  /* 非路径 */
599
719
  }
600
- if (!dir || !opts.domain)
601
- return emit(Promise.reject(new Error("托管模式部署:clize deploy <目录> --domain <已 claim 的 host>(--html 内联单页暂仅本地模式)")));
720
+ if (!dir)
721
+ return emit(Promise.reject(new Error(`${p} 不是存在的目录 —— 先生成站点文件再 deploy(托管模式只支持目录部署;--html 内联单页暂仅本地模式)`)));
722
+ if (!domain)
723
+ return emit(Promise.reject(new Error("缺目标域:clize deploy <目录> --domain <已 claim 的 host>,或先 clize init --handle <slug> 绑定本目录免传 --domain")));
602
724
  const files = sitesCore.readDirFiles(p);
603
725
  const bytes = files.reduce((n, f) => n + Math.ceil(f.base64.length * 0.75), 0);
604
726
  if (bytes > 25 * 1024 * 1024)
605
727
  return emit(Promise.reject(new Error(`站点约 ${(bytes / 1048576).toFixed(1)}MB,超过托管部署上限 25MB;请精简`)));
606
- return emit(remoteClient.api.deploy(remote, opts.domain, files));
728
+ return emit(remoteClient.api.deploy(remote, domain, files));
607
729
  }
608
730
  let isDir = false;
609
731
  try {
610
732
  isDir = fs.statSync(p).isDirectory();
611
733
  }
612
734
  catch {
613
- /* 不是路径 当作 worker 名走内联 */
735
+ /* 非路径:仅显式 --html/--html-file 时才允许当 worker 名走内联(否则下面报"目录不存在") */
614
736
  }
615
737
  if (isDir) {
616
- if (!opts.domain)
617
- return emit(Promise.reject(new Error("目录(多文件)部署需要 --domain <域名>(需已 claim / buy / import)")));
618
- return emit(sitesCore.deployToHandle(ctx(), opts.domain, p));
738
+ if (!domain)
739
+ return emit(Promise.reject(new Error("目录(多文件)部署需要 --domain <域名>(需已 claim / buy / import),或先 clize init 绑定本目录")));
740
+ return emit(sitesCore.deployToHandle(ctx(), domain, p));
619
741
  }
742
+ // 路径不存在且没给内联 HTML:大概率是站点还没生成 / 路径打错 —— 直接说清,
743
+ // 绝不带着错参数去打 CF(那会报回毫不相干的 workers.dev 错误,误导排查方向)。
744
+ if (!opts.html && !opts.htmlFile)
745
+ return emit(Promise.reject(new Error(`${p} 不是存在的目录 —— 先生成站点文件再 deploy;内联单页要显式 --html "<...>" 或 --html-file <path>`)));
620
746
  const html = opts.htmlFile ? fs.readFileSync(opts.htmlFile, "utf8") : opts.html;
621
747
  return emit(siteCore.deploySite(ctx(), { workerName: p, html, domain: opts.domain }));
622
748
  });
749
+ // ───────── 本地预览(serve:deploy 的本地对偶)─────────
750
+ // 起一个支持 Range + 媒体不灌 no-store 的静态 server —— 预览含 <video> 的站点 / 片子用它,
751
+ // 别 `python -m http.server`(不支持 Range:Safari 不播、不能 seek)。只伺服本地路径、不碰项目资源,故不包 withProject。
752
+ program
753
+ .command("serve [path]")
754
+ .description("本地预览:起支持 Range·媒体不灌 no-store 的静态 server(预览含视频的站点 / 片子用它,别 python -m http.server)")
755
+ .option("--port <n>", "端口(被占用自动顺延)", "8000")
756
+ .option("--open", "起好后用默认浏览器打开(给人看;agent 不用)")
757
+ .action(async (pathArg, opts) => {
758
+ const root = pathArg
759
+ ? path.resolve(pathArg)
760
+ : fs.existsSync("site")
761
+ ? path.resolve("site")
762
+ : process.cwd();
763
+ try {
764
+ const h = await serveCore.serve(root, { port: Number(opts.port) || 8000 });
765
+ process.stdout.write(`clize serve · ${h.url} (root: ${h.root})\n` +
766
+ `支持 Range·媒体不灌 no-store —— <video> 能 seek、Safari 能播。Ctrl-C 停止。\n`);
767
+ if (opts.open)
768
+ openBrowser(h.url);
769
+ await new Promise(() => { }); // 常驻:阻塞到 Ctrl-C
770
+ }
771
+ catch (e) {
772
+ process.stderr.write(`serve 失败:${e.message}\n`);
773
+ process.exitCode = 1;
774
+ }
775
+ });
623
776
  // ───────── 生成式媒体(gen:图 / 视频 / 音乐)─────────
624
777
  // G1 = 本地算力 + 你自带的 model key,与 clize 登录模式无关(remote 登录也在本地跑、写本地盘);
625
778
  // 托管(clize 代付 + R2 托管)是 G2,所以这里不按 remote 分流。
626
779
  const gen = program
627
780
  .command("gen")
628
- .description("生成式媒体:你写 prompt,clize 产字节落盘(图 / 视频 / 音乐)");
629
- gen
781
+ .description("生成式媒体:你写 prompt,clize 产字节落盘(图 / 视频 / 音乐;蓝图批量 = check + render)");
782
+ withProject(gen
630
783
  .command("image <prompt>")
631
- .description("文生图 / 图生图(💰:--confirm 才生成,缺则只报价;可先 gen budget 预批免逐次确认)")
784
+ .description("文生图 / 图生图(💰:--confirm 才生成,缺则只报价;可先 gen budget 预批免逐次确认)"))
632
785
  .option("--model <m>", "模型:gpt-image-2(OpenAI)| nano-banana-2(Google)", "gpt-image-2")
633
786
  .option("--n <k>", "出几张", "1")
634
787
  .option("--size <s>", "尺寸,如 1024x1024")
@@ -664,11 +817,10 @@ gen
664
817
  out: opts.out,
665
818
  }, { confirm: !!opts.confirm }));
666
819
  });
667
- gen
820
+ withProject(gen
668
821
  .command("list")
669
- .description("列已生成资产(id / 模型 / prompt / 路径 / 花费)")
670
- .option("--modality <m>", "只看某模态:image | video | music")
671
- .action((opts) => emit(remote ? remoteClient.api.genList(remote, opts.modality) : mediaCore.listAssets(ctx(), opts.modality)));
822
+ .description("列已生成资产(id / 模型 / prompt / 路径 / 花费;检出项目时只列该项目)")
823
+ .option("--modality <m>", "只看某模态:image | video | music")).action((opts) => emit(remote ? remoteClient.api.genList(remote, opts.modality) : mediaCore.listAssets(ctx(), opts.modality)));
672
824
  gen
673
825
  .command("show <id>")
674
826
  .description("看单个生成任务 / 资产全貌")
@@ -683,12 +835,13 @@ gen
683
835
  const amount = raw != null && raw !== "" && !Number.isNaN(Number(raw)) ? Number(raw) : undefined;
684
836
  return emit(amount != null ? mediaCore.setGenBudget(ctx(), amount) : mediaCore.genBudget(ctx()));
685
837
  });
686
- gen
838
+ withProject(gen
687
839
  .command("video <prompt>")
688
- .description("文生视频 / 图生视频(长任务:默认等到完成,--async 后台返 job id)")
840
+ .description("文生视频 / 图生视频(长任务:默认等到完成,--async 后台返 job id)"))
689
841
  .option("--model <m>", "模型(默认 veo)", "veo")
690
842
  .option("--from-image <path>", "首帧图(图生视频)")
691
- .option("--duration <sec>", "时长()")
843
+ .option("--ref <paths>", "参考图(逗号分隔多张:角色图+场景图保跨镜一致性;veo 上限 3 张)")
844
+ .option("--duration <sec>", "时长(秒;veo 忽略 —— 时长模型自定、单次 ≤8s)")
692
845
  .option("--aspect <r>", "画幅,如 16:9")
693
846
  .option("--out <path>", "落盘路径(--wait 时;--async 走默认 ./clize-assets)")
694
847
  .option("--async", "后台生成,只返 job id(默认等到完成)")
@@ -696,28 +849,30 @@ gen
696
849
  .option("--confirm", "确认花钱生成")
697
850
  .action((prompt, opts) => {
698
851
  if (remote) {
699
- if (opts.fromImage)
700
- return emit(Promise.reject(new Error("托管模式暂不支持图生视频(--from-image);用纯文生视频,或切本地模式。")));
701
- return emit(remoteGenAsync(remote, "video", {
852
+ // 托管:参考图在 CLI 端读盘转 data URI 上传(首帧排第一,与本地 core 同序)
853
+ const paths = [...new Set([opts.fromImage, ...splitRefs(opts.ref)].filter(Boolean))];
854
+ return emit((async () => remoteGenAsync(remote, "video", {
702
855
  prompt,
703
856
  model: opts.model,
857
+ refs: paths.length ? refsToDataUris(paths) : undefined,
704
858
  durationSec: opts.duration ? Number(opts.duration) : undefined,
705
859
  aspect: opts.aspect,
706
860
  confirm: !!opts.confirm,
707
- }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }));
861
+ }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }))());
708
862
  }
709
863
  return emit(mediaCore.genVideo(ctx(), {
710
864
  prompt,
711
865
  model: opts.model,
712
866
  fromImage: opts.fromImage,
867
+ refs: splitRefs(opts.ref),
713
868
  durationSec: opts.duration ? Number(opts.duration) : undefined,
714
869
  aspect: opts.aspect,
715
870
  out: opts.out,
716
871
  }, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
717
872
  });
718
- gen
873
+ withProject(gen
719
874
  .command("music <prompt>")
720
- .description("文生音乐(长任务:默认等到完成,--async 后台返 job id)")
875
+ .description("文生音乐(长任务:默认等到完成,--async 后台返 job id)"))
721
876
  .option("--model <m>", "模型(默认 suno)", "suno")
722
877
  .option("--instrumental", "纯器乐(无人声)")
723
878
  .option("--duration <sec>", "时长(秒)")
@@ -743,10 +898,7 @@ gen
743
898
  out: opts.out,
744
899
  }, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
745
900
  });
746
- gen
747
- .command("jobs")
748
- .description("列生成任务(running 置顶;断会话后重连长任务用)")
749
- .action(() => emit(remote ? remoteClient.api.genJobs(remote) : mediaCore.genJobs(ctx())));
901
+ withProject(gen.command("jobs").description("列生成任务(running 置顶;断会话后重连长任务用;检出项目时只列该项目)")).action(() => emit(remote ? remoteClient.api.genJobs(remote) : mediaCore.genJobs(ctx())));
750
902
  gen
751
903
  .command("status <id>")
752
904
  .description("查单任务:running 则向 provider 轮询一次(可能就此完成落盘)")
@@ -759,43 +911,169 @@ gen
759
911
  .command("rm <id>")
760
912
  .description("删任务记录 + 本地文件")
761
913
  .action((id) => emit(remote ? remoteClient.api.genRm(remote, id) : mediaCore.genRm(ctx(), id)));
762
- // ───────── 设计系统(lokuma,仅托管:宪法库 + 方法在 clize 云端,运行时下发)─────────
763
- const hostedDesign = (fn) => emit(remote
914
+ // ───────── build:作品域(端到端做一个作品;方法 + 校验免费,成品步骤花钱带闸)─────────
915
+ // taxonomy:gen = 原子生成 + 任务池(引擎),build = 作品编排门面(方法 → 你创作 → 校验 → 成品)。
916
+ // 第 N 种作品 = build 加子域(方法三件套 + 该作品特有的校验/成品命令),顶级命令面零膨胀。
917
+ const hostedBuild = (fn) => emit(remote
764
918
  ? fn(remote)
765
- : Promise.reject(new Error("clize design 需登录托管模式:先 `clize login`(设计宪法库在 clize 云端,只发登录用户)")));
766
- const design = program
767
- .command("design")
768
- .description("从风格宪法库设计站点(托管):start 取方法+推荐 · get 取整套宪法 · list / stack / search / review");
769
- design
919
+ : Promise.reject(new Error("clize build 的方法库需登录托管模式:先 `clize login`(作品方法 + 风格库在 clize 云端,只发登录用户)")));
920
+ const build = program
921
+ .command("build")
922
+ .description("做一个作品(端到端):build site = 站点/UI · build clip = 短片;方法/校验免费,成品步骤 💰");
923
+ /** 站点/UI 的 7 条方法命令构建器:build site(正式位)与裸 design(0.7.8 兼容别名,help 隐藏)各灌一套。 */
924
+ function siteBuildCommands() {
925
+ const start = new Command("start")
926
+ .argument("<brief...>")
927
+ .description("开工:返回工作流(7 步 + 反 slop)+ 风格目录 + 针对 brief 的推荐")
928
+ .option("--project <name>", "项目名")
929
+ .action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteStart(rc, brief.join(" "), opts.project)));
930
+ const recommend = new Command("recommend")
931
+ .argument("<brief...>")
932
+ .description("按 brief 推荐:风格 + 调色板 + 字体对 + 反模式")
933
+ .option("--project <name>", "项目名")
934
+ .action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteRecommend(rc, brief.join(" "), opts.project)));
935
+ const list = new Command("list")
936
+ .description("列出全部风格宪法(slug + 一句话 best-for)")
937
+ .action(() => hostedBuild((rc) => remoteClient.api.buildSiteList(rc)));
938
+ const get = new Command("get")
939
+ .argument("<slug>")
940
+ .description("取某风格的完整宪法(色 / 字 / 形 / 签名手法)+ imagery 配图契约 + 调色板 / 字体对 + DESIGN.md 模板")
941
+ .action((slug) => hostedBuild((rc) => remoteClient.api.buildSiteGet(rc, slug)));
942
+ const stack = new Command("stack")
943
+ .argument("<stack>")
944
+ .argument("[query...]")
945
+ .description("取某技术栈实现指南(React/Next/SwiftUI/Flutter/shadcn… 惯用法 + 行为规则;跨栈 UI 指南)")
946
+ .action((stackName, query) => hostedBuild((rc) => remoteClient.api.buildSiteStack(rc, stackName, (query || []).join(" "))));
947
+ const search = new Command("search")
948
+ .argument("<query...>")
949
+ .description("查 UUPM 库(ux/color/chart/landing/product/typography/icons…;默认自动判域)")
950
+ .option("--domain <d>", "限定域")
951
+ .action((query, opts) => hostedBuild((rc) => remoteClient.api.buildSiteSearch(rc, query.join(" "), opts.domain)));
952
+ const review = new Command("review")
953
+ .description("取合并自审清单(anti-slop + DESIGN.md 保真 + UX 行为)")
954
+ .action(() => hostedBuild((rc) => remoteClient.api.buildSiteReview(rc)));
955
+ return [start, recommend, list, get, stack, search, review];
956
+ }
957
+ const buildSite = build
958
+ .command("site")
959
+ .description("站点/UI 作品:start 取方法+推荐 → 你写码 → review 自审 → deploy 上线(成品步骤复用顶级 deploy)");
960
+ for (const c of siteBuildCommands())
961
+ buildSite.addCommand(c);
962
+ // 兼容别名:0.7.8 起发布的 clize design start… 继续工作;help 不展示(正式位 build site)。
963
+ const designAlias = new Command("design");
964
+ for (const c of siteBuildCommands())
965
+ designAlias.addCommand(c);
966
+ program.addCommand(designAlias, { hidden: true });
967
+ /** 读蓝图 JSON 文件(build clip check / render 共用)。 */
968
+ function readBlueprint(file) {
969
+ let raw;
970
+ try {
971
+ raw = fs.readFileSync(file, "utf8");
972
+ }
973
+ catch {
974
+ throw new Error(`读不到蓝图文件:${file}`);
975
+ }
976
+ try {
977
+ return JSON.parse(raw);
978
+ }
979
+ catch (e) {
980
+ throw new Error(`蓝图不是合法 JSON:${e instanceof Error ? e.message : String(e)}`);
981
+ }
982
+ }
983
+ /** render 后端(本地):探价走 provider.quote;提交/轮询走 core/media(任务进 gen jobs 体系)。 */
984
+ function localRenderBackend() {
985
+ return {
986
+ quote: (p) => mediaCore.quoteVideo(ctx(), p),
987
+ submit: async (p) => {
988
+ const r = (await mediaCore.genVideo(ctx(), { ...p }, { confirm: true, async: true }));
989
+ if (!r.id)
990
+ throw new Error(`提交未返回任务 id${r.message ? `:${String(r.message)}` : ""}`);
991
+ return { id: String(r.id), state: String(r.state ?? "running") };
992
+ },
993
+ poll: async (id, outPath) => {
994
+ const r = (await mediaCore.genJobStatus(ctx(), id));
995
+ if (r.state === "succeeded") {
996
+ const src = r.files?.[0]?.path;
997
+ if (src && path.resolve(src) !== path.resolve(outPath)) {
998
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
999
+ fs.copyFileSync(src, outPath);
1000
+ }
1001
+ return { state: "succeeded" };
1002
+ }
1003
+ if (r.state === "failed")
1004
+ return { state: "failed", error: r.error ? String(r.error) : undefined };
1005
+ return { state: "running" };
1006
+ },
1007
+ };
1008
+ }
1009
+ /** render 后端(托管):参考图逐镜转 data URI 上传;confirm=false 探价是后端契约(永不生成)。 */
1010
+ function remoteRenderBackend(rc) {
1011
+ const body = (p, confirm) => ({
1012
+ prompt: p.prompt,
1013
+ model: p.model,
1014
+ refs: p.refs?.length ? refsToDataUris(p.refs) : undefined,
1015
+ durationSec: p.durationSec,
1016
+ aspect: p.aspect,
1017
+ confirm,
1018
+ });
1019
+ return {
1020
+ quote: async (p) => {
1021
+ const r = (await remoteClient.api.genVideo(rc, body(p, false)));
1022
+ const est = r?.quote?.estUsd;
1023
+ if (typeof est !== "number")
1024
+ throw new Error("探价未返回 quote(后端响应异常)");
1025
+ return est;
1026
+ },
1027
+ submit: async (p) => {
1028
+ const r = (await remoteClient.api.genVideo(rc, body(p, true)));
1029
+ if (!r.id)
1030
+ throw new Error(`提交未返回任务 id${r.message ? `:${String(r.message)}` : ""}`);
1031
+ return { id: String(r.id), state: String(r.state ?? "running") };
1032
+ },
1033
+ poll: async (id, outPath) => {
1034
+ const r = (await remoteClient.api.genStatus(rc, id));
1035
+ if (r.state === "succeeded") {
1036
+ await mediaCore.materializeRemote(r, outPath);
1037
+ return { state: "succeeded" };
1038
+ }
1039
+ if (r.state === "failed")
1040
+ return { state: "failed", error: r.error ? String(r.error) : undefined };
1041
+ return { state: "running" };
1042
+ },
1043
+ };
1044
+ }
1045
+ // build clip:短片作品(蓝图制)—— 方法 + 校验 + 成品一条龙;render 的任务进 gen 任务池(gen jobs/status 可查)。
1046
+ const buildClip = build
1047
+ .command("clip")
1048
+ .description("短片作品(蓝图制):start 取方法+风格 → 你写蓝图 → check 校验(免费)→ render 成片(💰;素材级单镜用 gen video)");
1049
+ buildClip
770
1050
  .command("start <brief...>")
771
- .description("开工:返回方法(4 阶段 + DESIGN.md 模板)+ 风格目录 + 针对 brief 的推荐")
772
- .option("--project <name>", "项目名")
773
- .action((brief, opts) => hostedDesign((rc) => remoteClient.api.designStart(rc, brief.join(" "), opts.project)));
774
- design
775
- .command("recommend <brief...>")
776
- .description("按 brief 推荐:风格 + 调色板 + 字体对 + 反模式")
777
- .option("--project <name>", "项目名")
778
- .action((brief, opts) => hostedDesign((rc) => remoteClient.api.designRecommend(rc, brief.join(" "), opts.project)));
779
- design
1051
+ .description("开工:返回方法(4 阶段 + 蓝图 schema + 反穿帮清单)+ 风格目录 + 针对 brief 的推荐")
1052
+ .action((brief) => hostedBuild((rc) => remoteClient.api.buildClipStart(rc, brief.join(" "))));
1053
+ buildClip
780
1054
  .command("list")
781
- .description("列出全部风格宪法(slug + 一句话 best-for)")
782
- .action(() => hostedDesign((rc) => remoteClient.api.designList(rc)));
783
- design
1055
+ .description("列出全部视频风格(id + 一句话适用场景)")
1056
+ .action(() => hostedBuild((rc) => remoteClient.api.buildClipList(rc)));
1057
+ buildClip
784
1058
  .command("get <slug>")
785
- .description("取某风格的完整宪法( / / / 签名手法)+ 调色板 + 字体对")
786
- .action((slug) => hostedDesign((rc) => remoteClient.api.designGet(rc, slug)));
787
- design
788
- .command("stack <stack> [query...]")
789
- .description("取某技术栈实现指南(React/Next/SwiftUI/Flutter/shadcn… 惯用法 + 行为规则)")
790
- .action((stack, query) => hostedDesign((rc) => remoteClient.api.designStack(rc, stack, (query || []).join(" "))));
791
- design
792
- .command("search <query...>")
793
- .description(" UUPM 库(ux/color/chart/landing/product/typography/icons…;默认自动判域)")
794
- .option("--domain <d>", "限定域")
795
- .action((query, opts) => hostedDesign((rc) => remoteClient.api.designSearch(rc, query.join(" "), opts.domain)));
796
- design
797
- .command("review")
798
- .description("取合并自审清单(anti-slop + DESIGN.md 保真 + UX 行为)")
799
- .action(() => hostedDesign((rc) => remoteClient.api.designReview(rc)));
1059
+ .description("取某风格完整包(资产图模板 + 视频视觉词汇 + strict 后端替换行)")
1060
+ .action((slug) => hostedBuild((rc) => remoteClient.api.buildClipGet(rc, slug)));
1061
+ buildClip
1062
+ .command("check <blueprint>")
1063
+ .description("蓝图机械校验(本地、免费、零凭证):防穿帮 lint + 台词覆盖 + 时长公式 + 结构;error 清零才 render")
1064
+ .action((file) => emit((async () => videoCore.checkBlueprint(readBlueprint(file)))()));
1065
+ withProject(buildClip
1066
+ .command("render <blueprint>")
1067
+ .description("按蓝图批量成片(💰:报价合计一次 --confirm;断点续传,重跑同命令只补未完成镜头;任务在 gen jobs 可查)"))
1068
+ .option("--candidates <n>", "每镜候选数(真人镜头建议 2-3 挑最好)", "1")
1069
+ .option("--out-dir <dir>", "输出目录(默认 clize-video/<title>)")
1070
+ .option("--timeout <sec>", "轮询总时限(秒)", "1800")
1071
+ .option("--confirm", "确认花钱生成(报价见无 --confirm 的输出)")
1072
+ .action((file, opts) => emit(videoCore.renderBlueprint(remote ? remoteRenderBackend(remote) : localRenderBackend(), readBlueprint(file), {
1073
+ confirm: !!opts.confirm,
1074
+ candidates: Number(opts.candidates) || 1,
1075
+ outDir: opts.outDir,
1076
+ timeoutSec: Number(opts.timeout) || 1800,
1077
+ })));
800
1078
  program.parseAsync(process.argv);
801
1079
  //# sourceMappingURL=cli.js.map