@lingjingai/lj-awb-cli-pre 0.4.0 → 0.4.5

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.
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { LingjingAwbCliError, createEnvelope, toBool, toInt } from './common.js';
5
5
  import { registerAwbCommands } from './commands.js';
6
+ import { checkCliUpdate } from './update.js';
6
7
  import {
7
8
  formatCsvOutput,
8
9
  formatPrettyError,
@@ -186,10 +187,14 @@ function commandInvocation(commandName, kwargs = {}, options = {}) {
186
187
  return parts.join(' ');
187
188
  }
188
189
 
189
- function buildOutputContext(command, kwargs = {}) {
190
+ function buildOutputContext(command, kwargs = {}, data = {}) {
190
191
  const optionKeys = new Set((command.args || []).map((arg) => normalizeKey(arg.name)));
191
192
  const canSuggestDryRunExecution = command.name !== 'auth login';
192
193
  const withoutDryRun = { omit: ['dryRun'] };
194
+ const confirmSet = { yes: true };
195
+ if (command.name === 'create asset' && data?.conversionPlan && !toBool(kwargs.autoConvert)) {
196
+ confirmSet.autoConvert = true;
197
+ }
193
198
  const isModelList = command.name === 'model image-models' || command.name === 'model video-models';
194
199
  const modelListAll = isModelList && toBool(kwargs.all);
195
200
  const modelListLimit = isModelList
@@ -204,12 +209,27 @@ function buildOutputContext(command, kwargs = {}) {
204
209
  outputKind: buildCommandWorkflow(command).outputKind,
205
210
  executeCommand: canSuggestDryRunExecution && toBool(kwargs.dryRun) ? commandInvocation(command.name, kwargs, withoutDryRun) : null,
206
211
  confirmCommand: canSuggestDryRunExecution && toBool(kwargs.dryRun) && optionKeys.has('yes')
207
- ? commandInvocation(command.name, kwargs, { omit: ['dryRun'], set: { yes: true } })
212
+ ? commandInvocation(command.name, kwargs, { omit: ['dryRun'], set: confirmSet })
208
213
  : null,
209
214
  ...(isModelList ? { listLimit: modelListLimit, moreCommand: modelListMoreCommand } : {}),
210
215
  };
211
216
  }
212
217
 
218
+ function updateCheckDisabledByEnv() {
219
+ return ['1', 'true', 'yes', 'on'].includes(String(process.env.LINGJING_AWB_DISABLE_UPDATE_CHECK || '').trim().toLowerCase())
220
+ || ['1', 'true', 'yes', 'on'].includes(String(process.env.AWB_DISABLE_UPDATE_CHECK || '').trim().toLowerCase());
221
+ }
222
+
223
+ function shouldSkipUpdateCheck(argv = [], commandName = '') {
224
+ if (updateCheckDisabledByEnv()) return true;
225
+ return commandName === 'update';
226
+ }
227
+
228
+ async function resolveUpdateNotice(argv = [], commandName = '', version = 'unknown') {
229
+ if (shouldSkipUpdateCheck(argv, commandName)) return null;
230
+ return await checkCliUpdate({ currentVersion: version }).catch(() => null);
231
+ }
232
+
213
233
  const OUTPUT_FORMAT_ALIASES = {
214
234
  agent: 'text',
215
235
  table: 'pretty',
@@ -342,6 +362,9 @@ const RENAMED_COMMAND_HINTS = {
342
362
  'video create-batch': 'create video-batch',
343
363
  'video status': 'task video-status',
344
364
  'video subtitle-remove': 'create video-subtitle-removal',
365
+ 'video super-resolution': 'create video-super-resolution',
366
+ 'video super-resolution-fee': 'create video-super-resolution-fee',
367
+ 'video super-resolution-status': 'task video-super-resolution-status',
345
368
  'video subtitle-status': 'task video-subtitle-status',
346
369
  asset: 'create asset',
347
370
  'asset match-actor': 'create asset-match-actor',
@@ -480,6 +503,8 @@ const GROUP_EXAMPLES = {
480
503
  system: [
481
504
  'lj-awb doctor',
482
505
  'lj-awb doctor --verify',
506
+ 'lj-awb update --check',
507
+ 'lj-awb update',
483
508
  'lj-awb schema --brief -f json',
484
509
  'lj-awb schema -f json',
485
510
  'lj-awb schema --domain create -f json',
@@ -487,8 +512,12 @@ const GROUP_EXAMPLES = {
487
512
  auth: [
488
513
  'lj-awb auth status',
489
514
  'lj-awb auth verify',
515
+ 'lj-awb auth login',
516
+ 'lj-awb auth login --no-wait --json',
517
+ 'lj-awb auth login --flow-id <flowId>',
490
518
  'lj-awb auth login --access-key <access-key>',
491
519
  'LINGJING_AWB_ACCESS_KEY=<access-key> lj-awb auth login',
520
+ 'lj-awb auth logout',
492
521
  'lj-awb auth clear --dry-run',
493
522
  ],
494
523
  account: [
@@ -522,6 +551,7 @@ const GROUP_EXAMPLES = {
522
551
  'lj-awb create image-fee --model-group-code <code> --prompt "一只小狗"',
523
552
  'lj-awb create video --model-group-code <code> --prompt "镜头推进" --resource image:first_frame=./actor.png --dry-run',
524
553
  'lj-awb create video-fee --model-group-code <code> --prompt "雨夜奔跑" --duration 5',
554
+ 'lj-awb create video-super-resolution --object-name material/video-super/example.mp4 --dry-run',
525
555
  'lj-awb create subject --model-code tx --name 女主 --resource primary:./three-view.png --dry-run',
526
556
  'lj-awb create subject-wait --element-id <elementId> --wait-seconds 300',
527
557
  'lj-awb create asset --group-id <id> --platform JIMENG --file ./actor.png --name "女主正面" --dry-run',
@@ -532,6 +562,7 @@ const GROUP_EXAMPLES = {
532
562
  'lj-awb task image-status --task-id <imageTaskId>',
533
563
  'lj-awb task video-status --task-id <videoTaskId>',
534
564
  'lj-awb task video-subtitle-status --task-id <subtitleTaskId>',
565
+ 'lj-awb task video-super-resolution-status --task-id <superResolutionTaskId>',
535
566
  'lj-awb task wait --task-id <id> --task-type IMAGE_CREATE --wait-seconds 180',
536
567
  'lj-awb task records --task-record-file .awb/tasks.jsonl',
537
568
  ],
@@ -556,8 +587,11 @@ const COMMAND_REQUIRED_OPTIONS = {
556
587
  'create video-fee': ['modelGroupCode'],
557
588
  'create video': ['modelGroupCode'],
558
589
  'create video-batch': ['inputFile', 'modelGroupCode'],
590
+ 'create video-super-resolution-fee': ['objectName'],
591
+ 'create video-super-resolution': ['objectName'],
559
592
  'task video-status': ['taskId'],
560
593
  'task video-subtitle-status': ['taskId'],
594
+ 'task video-super-resolution-status': ['taskId'],
561
595
  'task wait': ['taskId', 'taskType'],
562
596
  'create asset-match-actor': ['description'],
563
597
  'create asset-groups': ['platform'],
@@ -607,6 +641,8 @@ const COMMAND_REQUIRED_ANY_OPTIONS = {
607
641
  'upload files': [['file', 'files', 'filesJson']],
608
642
  'create video-fee': [['prompt', 'resource', 'resourcesJson']],
609
643
  'create video': [['prompt', 'resource', 'resourcesJson']],
644
+ 'create video-super-resolution-fee': [['objectName']],
645
+ 'create video-super-resolution': [['objectName']],
610
646
  'create video-subtitle-removal': [['sourceTaskId']],
611
647
  'create asset-group-update': [['name', 'description', 'projectName']],
612
648
  'create asset': [['file', 'url', 'backendPath']],
@@ -672,6 +708,7 @@ const CONFIRMATION_COMMANDS = new Set([
672
708
  'create image-batch',
673
709
  'create video',
674
710
  'create video-batch',
711
+ 'create video-super-resolution',
675
712
  'create video-subtitle-removal',
676
713
  'create asset-group',
677
714
  'create asset-group-update',
@@ -687,6 +724,7 @@ const COST_COMMANDS = new Set([
687
724
  'create image-batch',
688
725
  'create video',
689
726
  'create video-batch',
727
+ 'create video-super-resolution',
690
728
  'create video-subtitle-removal',
691
729
  ]);
692
730
 
@@ -701,6 +739,7 @@ const REMOTE_WRITE_COMMANDS = new Set([
701
739
  'create image-batch',
702
740
  'create video',
703
741
  'create video-batch',
742
+ 'create video-super-resolution',
704
743
  'create video-subtitle-removal',
705
744
  'create asset-group',
706
745
  'create asset-group-update',
@@ -712,24 +751,28 @@ const REMOTE_WRITE_COMMANDS = new Set([
712
751
  ]);
713
752
 
714
753
  const LOCAL_STATE_WRITE_COMMANDS = new Set([
754
+ 'update',
715
755
  'auth login',
716
756
  'auth clear',
757
+ 'auth logout',
717
758
  'project use',
718
759
  'project create',
719
760
  'project ensure',
720
761
  ]);
721
762
 
722
763
  const DESTRUCTIVE_COMMANDS = new Set(['auth clear', ...ARTIFACT_DELETE_COMMANDS]);
723
- const LONG_RUNNING_COMMANDS = new Set(['task wait', 'task record-poll', 'create subject-wait', 'create subject-voice-wait']);
724
- const NETWORK_NONE_COMMANDS = new Set(['schema', 'auth status', 'auth clear', 'model input-guide', 'task records']);
764
+ const LONG_RUNNING_COMMANDS = new Set(['auth login', 'task wait', 'task record-poll', 'create subject-wait', 'create subject-voice-wait']);
765
+ const NETWORK_NONE_COMMANDS = new Set(['schema', 'auth status', 'auth clear', 'auth logout', 'model input-guide', 'task records']);
725
766
  const NETWORK_CONDITIONAL_COMMANDS = new Set(['doctor', 'auth login']);
726
767
 
727
768
  const OUTPUT_KIND_BY_COMMAND = {
728
769
  doctor: 'environment_report',
770
+ update: 'update_result',
729
771
  schema: 'command_schema',
730
772
  'auth status': 'auth_status',
731
773
  'auth verify': 'auth_status',
732
774
  'auth login': 'auth_result',
775
+ 'auth logout': 'auth_result',
733
776
  'account info': 'account_summary',
734
777
  'account teams': 'team_list',
735
778
  'project list': 'project_list',
@@ -751,9 +794,12 @@ const OUTPUT_KIND_BY_COMMAND = {
751
794
  'create video-fee': 'fee_estimate',
752
795
  'create video': 'task_submission',
753
796
  'create video-batch': 'batch_task_submission',
797
+ 'create video-super-resolution-fee': 'fee_estimate',
798
+ 'create video-super-resolution': 'task_submission',
754
799
  'task video-status': 'task_status',
755
800
  'create video-subtitle-removal': 'subtitle_task_submission',
756
801
  'task video-subtitle-status': 'subtitle_task_status',
802
+ 'task video-super-resolution-status': 'task_status',
757
803
  'task list': 'task_list',
758
804
  'task wait': 'task_status',
759
805
  'task records': 'local_task_records',
@@ -829,6 +875,8 @@ const PREFLIGHTS_BY_COMMAND = {
829
875
  'create image-batch': ['doctor --verify', 'model image-models', 'model options', 'model create-spec', 'prepare JSONL input', 'create image-batch --dry-run'],
830
876
  'create video': ['doctor --verify', 'model video-models', 'model options', 'model create-spec', 'confirm missing key params', 'create video-fee', 'create video --dry-run'],
831
877
  'create video-batch': ['doctor --verify', 'model video-models', 'model options', 'model create-spec', 'prepare JSONL input', 'create video-batch --dry-run'],
878
+ 'create video-super-resolution-fee': ['doctor --verify', 'create video-super-resolution-fee --object-name <objectName>'],
879
+ 'create video-super-resolution': ['doctor --verify', 'create video-super-resolution --object-name <objectName> --dry-run'],
832
880
  'create subject': ['doctor --verify', 'create subject --model-code tx|vidu --dry-run'],
833
881
  'create subject-batch': ['doctor --verify', 'prepare JSONL input with modelCode', 'create subject-batch --dry-run'],
834
882
  'create video-subtitle-removal': ['doctor --verify', 'create video-subtitle-removal --source-task-id <videoTaskId> --dry-run'],
@@ -891,9 +939,10 @@ function buildCommandSafety(command, optionKeys) {
891
939
  const costsPoints = COST_COMMANDS.has(name);
892
940
  const destructive = DESTRUCTIVE_COMMANDS.has(name);
893
941
  const supportsDryRun = optionKeys.has('dryRun');
942
+ const updateInstall = name === 'update';
894
943
  return {
895
- safeToAutoRun: !(remoteWrite || localStateWrite || requiresConfirmation || costsPoints || destructive),
896
- network: commandNetworkMode(name),
944
+ safeToAutoRun: !(remoteWrite || localStateWrite || requiresConfirmation || costsPoints || destructive || updateInstall),
945
+ network: updateInstall ? 'conditional' : commandNetworkMode(name),
897
946
  remoteWrite,
898
947
  localStateWrite,
899
948
  costsPoints,
@@ -915,6 +964,9 @@ function buildCommandWorkflow(command) {
915
964
  if (command.name === 'create video') {
916
965
  next.push('读取 data.taskId 和 data.nextCommand', '立即运行响应里的 nextCommand,或 task wait --task-type VIDEO_GROUP 等待结果');
917
966
  }
967
+ if (command.name === 'create video-super-resolution') {
968
+ next.push('读取 data.taskId 和 data.nextCommand', '运行响应里的 task video-super-resolution-status 命令查询结果');
969
+ }
918
970
  if (command.name === 'create video-subtitle-removal') {
919
971
  next.push('读取 data.taskId 和 data.nextCommand', '运行 task video-subtitle-status 或响应里的 nextCommand 等待结果');
920
972
  }
@@ -925,7 +977,7 @@ function buildCommandWorkflow(command) {
925
977
  next.push('读取 data.voiceRecordId / data.externalId', 'externalId 为空时立刻运行 create subject-voice-wait 获取 nextVoiceArg');
926
978
  }
927
979
  if (command.name === 'create asset') {
928
- next.push('读取 data.assetPath', '若后端返回审核 taskId,再运行 task wait --task-type ASSET_REGISTER');
980
+ next.push('读取 data.assetType / data.assetPath', 'data.conversionRequired=true 时先确认是否转码;正式执行可交互确认或追加 --auto-convert', '若后端返回审核 taskId,再运行 task wait --task-type ASSET_REGISTER');
929
981
  }
930
982
  if (command.name === 'model image-models' || command.name === 'model video-models') {
931
983
  next.push('读取 data.models[] 得到全部候选;用户给了模型口语名时保留同族全部候选,不只取第一个');
@@ -1134,10 +1186,11 @@ function buildAgentContract() {
1134
1186
  workflowPolicy: [
1135
1187
  '模型口语名命中后必须先展示候选模型、真实参数取值和资源能力,并在清单后 STOP 等用户选择或明确授权默认;没有完成用户可见候选清单和确认前,不得代选默认模型 / 参数,也不得进入 create-spec、fee、dry-run 或 create。',
1136
1188
  '模型探索阶段只读 model list / options / create-spec;fee 只在用户确认关键参数后跑一次。',
1189
+ 'model options.params[] 中带 cliArg/genericModelParam 的新增模型配置参数可通过 --model-param key=value、--model-params-json 或同名下划线长参数传入;defaultValue 只能展示为候选默认,不能静默代选。',
1137
1190
  'supportsDryRun=true 的写入 / 扣费命令先 dry-run,确认后 yes;不要把 dry-run 当参数探索工具反复跑。',
1138
1191
  '用户给出多条同模型同参数任务时优先 batch + task-record-file,不要单条循环 create。',
1139
1192
  '命令返回 nextCommand / nextRefSubject / nextVoiceArg 时优先复用返回值,不手拼等价命令。',
1140
- '素材加白平台先通过 model asset-review-models 或用户明确输入确定;create asset-* 必须显式传 --platform,不要依赖默认平台。',
1193
+ '素材加白平台先通过 model asset-review-models 或用户明确输入确定;create asset-* 必须显式传 --platform,不要依赖默认平台。create asset 会自动判断 assetType=Image/Video/Audio,本地文件会用 ffprobe 校验图片/视频尺寸、视频时长/FPS/像素数和音频时长;不合法素材正式执行时会询问是否转码,非交互场景可追加 --auto-convert;macOS + Homebrew 环境缺失时 CLI 会自动安装 ffmpeg。',
1141
1194
  '旧根域 image / video / asset / subject 已移除;只使用 create / task / artifact 等 schema 暴露的 domain。',
1142
1195
  ],
1143
1196
  canonicalFields: {
@@ -1190,7 +1243,7 @@ function buildAgentContract() {
1190
1243
  jsonCommand: `${commandPrefix()} model options --model-group-code <code> -f json`,
1191
1244
  purpose: '查询指定模型支持的 CLI 参数、枚举值、默认值、素材约束和条件约束。Agent 默认读 textCommand;只有程序化校验完整嵌套结构时读 jsonCommand。',
1192
1245
  useBefore: ['model create-spec', 'create image-fee', 'create image', 'create video-fee', 'create video'],
1193
- keyFields: ['params[].values', 'params[].defaultValue', 'resources[].mediaType', 'resources[].usage', 'resources[].fileTypes', 'resources[].maxFiles', 'resources[].supportLastFrameOnly', 'resources[].minDurationMs', 'resources[].maxDurationMs', 'constraints[].target', 'constraints[].conditions', 'constraints[].effect'],
1246
+ keyFields: ['params[].key', 'params[].cliArg', 'params[].genericModelParam', 'params[].values', 'params[].defaultValue', 'resources[].mediaType', 'resources[].usage', 'resources[].fileTypes', 'resources[].maxFiles', 'resources[].supportLastFrameOnly', 'resources[].minDurationMs', 'resources[].maxDurationMs', 'constraints[].target', 'constraints[].conditions', 'constraints[].effect'],
1194
1247
  },
1195
1248
  modelCreateSpec: {
1196
1249
  command: `${commandPrefix()} model create-spec --model-group-code <code>`,
@@ -1210,6 +1263,7 @@ function buildAgentContract() {
1210
1263
  taskTypes: {
1211
1264
  image: 'IMAGE_CREATE',
1212
1265
  video: 'VIDEO_GROUP',
1266
+ videoSuperResolution: 'VIDEO_SUPER_RESOLUTION',
1213
1267
  subtitleRemoval: 'VIDEO_SUBTITLE_REMOVAL',
1214
1268
  assetRegister: 'ASSET_REGISTER',
1215
1269
  },
@@ -1452,7 +1506,10 @@ function printCommandHelp(command) {
1452
1506
  function validateCommandOptions(command, kwargs, kwargRawFlags = {}) {
1453
1507
  const allowedKeys = (command.args || []).map((arg) => normalizeKey(arg.name));
1454
1508
  const allowed = new Set(allowedKeys);
1455
- const unknown = Object.keys(kwargs).filter((key) => !allowed.has(key));
1509
+ const unknown = Object.keys(kwargs).filter((key) => (
1510
+ !allowed.has(key)
1511
+ && !(command.allowDynamicModelParams && isAllowedDynamicModelParamFlag(key))
1512
+ ));
1456
1513
  if (!unknown.length) return;
1457
1514
  const displayFlag = (key) => kwargRawFlags[key] || formatOptionName(key);
1458
1515
  const suggestions = {};
@@ -1485,6 +1542,25 @@ function validateCommandOptions(command, kwargs, kwargRawFlags = {}) {
1485
1542
  });
1486
1543
  }
1487
1544
 
1545
+ function normalizeDynamicModelParamFlags(command, kwargs = {}, kwargRawFlags = {}) {
1546
+ if (!command.allowDynamicModelParams) return;
1547
+ const allowed = new Set((command.args || []).map((arg) => normalizeKey(arg.name)));
1548
+ for (const key of Object.keys(kwargs)) {
1549
+ if (allowed.has(key) || key.includes('_')) continue;
1550
+ const flag = kwargRawFlags[key] || formatOptionName(key);
1551
+ if (!flag.startsWith('--') || !flag.includes('-')) continue;
1552
+ const normalized = flag.slice(2).replaceAll('-', '_');
1553
+ if (!normalized || normalized === key || allowed.has(normalized)) continue;
1554
+ if (kwargs[normalized] === undefined) kwargs[normalized] = kwargs[key];
1555
+ delete kwargs[key];
1556
+ if (kwargRawFlags[key] && !kwargRawFlags[normalized]) kwargRawFlags[normalized] = kwargRawFlags[key];
1557
+ }
1558
+ }
1559
+
1560
+ function isAllowedDynamicModelParamFlag(key) {
1561
+ return /^(generation|prompt|negative|seed|style|mode|cfg|guidance|aspect|image|video|audio|subject|output|response|media|safety|watermark|camera|motion|strength|steps)_/i.test(String(key || ''));
1562
+ }
1563
+
1488
1564
  function printData(data, format, meta = {}, context = {}) {
1489
1565
  const outputFormat = resolveOutputFormat(format);
1490
1566
  if (outputFormat === 'json') {
@@ -1506,6 +1582,19 @@ function printData(data, format, meta = {}, context = {}) {
1506
1582
  process.stdout.write(formatPrettyOutput(meta.command, data, context));
1507
1583
  }
1508
1584
 
1585
+ function printUpdateNotice(updateNotice) {
1586
+ if (!updateNotice?.message) return;
1587
+ process.stderr.write(`${updateNotice.message}\n`);
1588
+ }
1589
+
1590
+ function isStructuredOutputFormat(format) {
1591
+ try {
1592
+ return ['json', 'yaml'].includes(resolveOutputFormat(format));
1593
+ } catch {
1594
+ return false;
1595
+ }
1596
+ }
1597
+
1509
1598
  function printError(error, format, meta = {}) {
1510
1599
  let outputFormat = 'text';
1511
1600
  try {
@@ -1567,6 +1656,7 @@ export async function runStandaloneCli(argv = process.argv.slice(2)) {
1567
1656
  const { commandName: rawCommandName, kwargs, kwargRawFlags, format } = parseArgv(argv);
1568
1657
  const commandName = resolveCommandName(rawCommandName, commands);
1569
1658
  const command = commands.find((item) => item.name === commandName);
1659
+ const updateNotice = await resolveUpdateNotice(argv, commandName || rawCommandName, version);
1570
1660
  if (!command) {
1571
1661
  if (isCommandSubgroupName(rawCommandName, commands)) {
1572
1662
  const [group, subgroup] = rawCommandName.split(' ');
@@ -1581,19 +1671,27 @@ export async function runStandaloneCli(argv = process.argv.slice(2)) {
1581
1671
  type: 'unknown_command',
1582
1672
  exitCode: 2,
1583
1673
  hint: unknownCommandHint(rawCommandName),
1584
- }), format, { command: rawCommandName });
1674
+ }), format, {
1675
+ command: rawCommandName,
1676
+ ...(updateNotice ? { _notice: { update: updateNotice } } : {}),
1677
+ });
1585
1678
  process.exitCode = 2;
1679
+ if (!isStructuredOutputFormat(format)) printUpdateNotice(updateNotice);
1586
1680
  return;
1587
1681
  }
1588
1682
 
1589
1683
  const startedAt = Date.now();
1590
1684
  try {
1591
1685
  const outputFormat = resolveOutputFormat(format);
1686
+ normalizeDynamicModelParamFlags(command, kwargs, kwargRawFlags);
1592
1687
  validateCommandOptions(command, kwargs, kwargRawFlags);
1593
1688
  const data = command.virtual === 'schema'
1594
1689
  ? buildCommandSchema(commands, kwargs, version)
1595
1690
  : await command.func({ command }, kwargs);
1596
- printData(data, outputFormat, { command: command.name, elapsedMs: Date.now() - startedAt }, buildOutputContext(command, kwargs));
1691
+ const meta = { command: command.name, elapsedMs: Date.now() - startedAt };
1692
+ if (updateNotice) meta._notice = { update: updateNotice };
1693
+ printData(data, outputFormat, meta, buildOutputContext(command, kwargs, data));
1694
+ if (outputFormat !== 'json' && outputFormat !== 'yaml') printUpdateNotice(updateNotice);
1597
1695
  } catch (error) {
1598
1696
  const cliError = error instanceof LingjingAwbCliError
1599
1697
  ? error
@@ -0,0 +1,327 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fsSync from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+ import {
8
+ LingjingAwbCliError,
9
+ loadState,
10
+ nowIso,
11
+ saveState,
12
+ toBool,
13
+ toInt,
14
+ trimToNull,
15
+ } from './common.js';
16
+
17
+ const execFileAsync = promisify(execFile);
18
+ const DEFAULT_CLI_PACKAGE_NAME = '@lingjingai/lj-awb-cli';
19
+ const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/';
20
+ const DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
21
+ const DEFAULT_NPM_TIMEOUT_MS = 15_000;
22
+ const AUTO_CHECK_TIMEOUT_MS = 4_000;
23
+
24
+ function commandPrefix() {
25
+ return process.env.LINGJING_AWB_COMMAND_PREFIX || 'lj-awb';
26
+ }
27
+
28
+ function boolEnv(value) {
29
+ return ['1', 'true', 'yes', 'y', 'on'].includes(String(value ?? '').trim().toLowerCase());
30
+ }
31
+
32
+ function readPackageJsonSync(packagePath) {
33
+ try {
34
+ return JSON.parse(fsSync.readFileSync(packagePath, 'utf8'));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function corePackagePath() {
41
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
42
+ }
43
+
44
+ function rootPackagePath() {
45
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json');
46
+ }
47
+
48
+ export async function readCurrentCliVersion() {
49
+ const pkg = JSON.parse(await fs.readFile(corePackagePath(), 'utf8'));
50
+ return pkg.version || 'unknown';
51
+ }
52
+
53
+ function currentRootDir() {
54
+ return path.dirname(rootPackagePath());
55
+ }
56
+
57
+ function isLocalCheckout() {
58
+ return fsSync.existsSync(path.join(currentRootDir(), '.git'));
59
+ }
60
+
61
+ export function resolveCliPackageName(kwargs = {}) {
62
+ const explicit = trimToNull(kwargs.packageName || kwargs.package || process.env.LINGJING_AWB_CLI_PACKAGE);
63
+ if (explicit) return explicit;
64
+ const rootPkg = readPackageJsonSync(rootPackagePath());
65
+ const name = trimToNull(rootPkg?.name);
66
+ return name?.startsWith('@lingjingai/lj-awb-cli') ? name : DEFAULT_CLI_PACKAGE_NAME;
67
+ }
68
+
69
+ function resolveRegistry(kwargs = {}) {
70
+ return trimToNull(
71
+ kwargs.registry
72
+ || process.env.LINGJING_AWB_UPDATE_REGISTRY
73
+ || process.env.NPM_CONFIG_REGISTRY
74
+ || process.env.npm_config_registry,
75
+ ) || DEFAULT_NPM_REGISTRY;
76
+ }
77
+
78
+ function updateCheckIntervalMs() {
79
+ const raw = process.env.LINGJING_AWB_UPDATE_CHECK_INTERVAL_MS;
80
+ if (raw === undefined || raw === null || raw === '') return DEFAULT_CHECK_INTERVAL_MS;
81
+ return Math.max(0, toInt(raw, DEFAULT_CHECK_INTERVAL_MS));
82
+ }
83
+
84
+ function updateCheckDisabled() {
85
+ return boolEnv(process.env.LINGJING_AWB_DISABLE_UPDATE_CHECK)
86
+ || boolEnv(process.env.AWB_DISABLE_UPDATE_CHECK)
87
+ || ['0', 'false', 'off', 'no'].includes(String(process.env.LINGJING_AWB_UPDATE_CHECK ?? '').trim().toLowerCase());
88
+ }
89
+
90
+ function forceAutoUpdateCheck() {
91
+ return boolEnv(process.env.LINGJING_AWB_UPDATE_CHECK)
92
+ || String(process.env.LINGJING_AWB_UPDATE_CHECK || '').trim().toLowerCase() === 'force';
93
+ }
94
+
95
+ function parseVersion(value) {
96
+ const text = String(value || '').trim().replace(/^v/i, '');
97
+ const [main, pre = ''] = text.split('-', 2);
98
+ const parts = main.split('.').map((item) => Number.parseInt(item, 10));
99
+ if (parts.some((item) => !Number.isFinite(item))) return null;
100
+ return { parts, pre };
101
+ }
102
+
103
+ export function semverLessThan(a, b) {
104
+ const left = parseVersion(a);
105
+ const right = parseVersion(b);
106
+ if (!left || !right) return false;
107
+ for (let index = 0; index < 3; index += 1) {
108
+ const lv = left.parts[index] || 0;
109
+ const rv = right.parts[index] || 0;
110
+ if (lv < rv) return true;
111
+ if (lv > rv) return false;
112
+ }
113
+ if (left.pre && !right.pre) return true;
114
+ if (!left.pre && right.pre) return false;
115
+ return left.pre < right.pre;
116
+ }
117
+
118
+ function npmArgsWithRegistry(args, registry) {
119
+ return registry ? [...args, `--registry=${registry}`] : args;
120
+ }
121
+
122
+ function npmInstallSpec(packageName, version) {
123
+ return version && version !== 'latest' ? `${packageName}@${version}` : packageName;
124
+ }
125
+
126
+ async function runNpm(args, options = {}) {
127
+ try {
128
+ const result = await execFileAsync('npm', args, {
129
+ cwd: currentRootDir(),
130
+ encoding: 'utf8',
131
+ timeout: options.timeoutMs || DEFAULT_NPM_TIMEOUT_MS,
132
+ maxBuffer: 1024 * 1024,
133
+ env: process.env,
134
+ });
135
+ return result;
136
+ } catch (error) {
137
+ const stderr = String(error.stderr || '').trim();
138
+ const stdout = String(error.stdout || '').trim();
139
+ throw new LingjingAwbCliError('npm 命令执行失败', {
140
+ type: 'network_error',
141
+ exitCode: 30,
142
+ hint: stderr || stdout || error.message || '请确认 npm 可用,并且当前网络可以访问 npm registry。',
143
+ details: {
144
+ command: `npm ${args.join(' ')}`,
145
+ code: error.code,
146
+ signal: error.signal,
147
+ },
148
+ });
149
+ }
150
+ }
151
+
152
+ export async function fetchLatestCliVersion(options = {}) {
153
+ const mocked = trimToNull(process.env.LINGJING_AWB_UPDATE_LATEST_VERSION);
154
+ if (mocked) return mocked;
155
+ const packageName = options.packageName || resolveCliPackageName(options);
156
+ const registry = options.registry || resolveRegistry(options);
157
+ const result = await runNpm(
158
+ npmArgsWithRegistry(['view', packageName, 'version'], registry),
159
+ { timeoutMs: options.timeoutMs || DEFAULT_NPM_TIMEOUT_MS },
160
+ );
161
+ const version = String(result.stdout || '').trim().replace(/^"|"$/g, '');
162
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
163
+ throw new LingjingAwbCliError('无法识别 npm latest 版本', {
164
+ type: 'runtime_error',
165
+ exitCode: 1,
166
+ hint: `npm view ${packageName} version 返回了非 semver 内容。`,
167
+ details: { version },
168
+ });
169
+ }
170
+ return version;
171
+ }
172
+
173
+ function buildUpdateNotice({ currentVersion, latestVersion, packageName, checkedAt }) {
174
+ const command = `${commandPrefix()} update`;
175
+ return {
176
+ message: `检测到 ${commandPrefix()} 新版本 ${latestVersion}(当前 ${currentVersion})。运行 ${command} 更新 CLI 和 skill。`,
177
+ command,
178
+ checkCommand: `${commandPrefix()} update --check`,
179
+ packageName,
180
+ currentVersion,
181
+ latestVersion,
182
+ checkedAt,
183
+ };
184
+ }
185
+
186
+ function cachedNotice(state, currentVersion, packageName) {
187
+ const cached = state?.updateCheck;
188
+ if (!cached || cached.packageName !== packageName || cached.currentVersion !== currentVersion) return null;
189
+ if (!cached.latestVersion || !semverLessThan(currentVersion, cached.latestVersion)) return null;
190
+ return buildUpdateNotice({
191
+ currentVersion,
192
+ latestVersion: cached.latestVersion,
193
+ packageName,
194
+ checkedAt: cached.checkedAtText || null,
195
+ });
196
+ }
197
+
198
+ function shouldSkipAutoCheck() {
199
+ if (updateCheckDisabled()) return true;
200
+ if (forceAutoUpdateCheck()) return false;
201
+ if (process.env.CI) return true;
202
+ if (isLocalCheckout()) return true;
203
+ return false;
204
+ }
205
+
206
+ export async function checkCliUpdate(options = {}) {
207
+ const currentVersion = options.currentVersion || await readCurrentCliVersion().catch(() => 'unknown');
208
+ const packageName = resolveCliPackageName(options);
209
+ if (!currentVersion || currentVersion === 'unknown' || shouldSkipAutoCheck()) return null;
210
+
211
+ const state = await loadState().catch(() => ({}));
212
+ const cached = state?.updateCheck || {};
213
+ const now = Date.now();
214
+ const intervalMs = options.force ? 0 : updateCheckIntervalMs();
215
+ const cacheHit = cached.packageName === packageName
216
+ && cached.currentVersion === currentVersion
217
+ && cached.checkedAt
218
+ && (now - Number(cached.checkedAt) < intervalMs);
219
+ if (cacheHit) return cachedNotice(state, currentVersion, packageName);
220
+
221
+ try {
222
+ const registry = resolveRegistry(options);
223
+ const latestVersion = await fetchLatestCliVersion({
224
+ packageName,
225
+ registry,
226
+ timeoutMs: options.timeoutMs || AUTO_CHECK_TIMEOUT_MS,
227
+ });
228
+ const checkedAtText = nowIso();
229
+ await saveState({
230
+ updateCheck: {
231
+ packageName,
232
+ registry,
233
+ currentVersion,
234
+ latestVersion,
235
+ checkedAt: now,
236
+ checkedAtText,
237
+ },
238
+ }).catch(() => {});
239
+ if (!semverLessThan(currentVersion, latestVersion)) return null;
240
+ return buildUpdateNotice({ currentVersion, latestVersion, packageName, checkedAt: checkedAtText });
241
+ } catch (error) {
242
+ await saveState({
243
+ updateCheck: {
244
+ packageName,
245
+ currentVersion,
246
+ latestVersion: cached.latestVersion || null,
247
+ checkedAt: now,
248
+ checkedAtText: nowIso(),
249
+ error: error.message || String(error),
250
+ },
251
+ }).catch(() => {});
252
+ return cachedNotice({ updateCheck: cached }, currentVersion, packageName);
253
+ }
254
+ }
255
+
256
+ function tailLines(value, limit = 8) {
257
+ const lines = String(value || '').trim().split(/\r?\n/).filter(Boolean);
258
+ return lines.slice(Math.max(0, lines.length - limit));
259
+ }
260
+
261
+ export async function runCliUpdate(kwargs = {}) {
262
+ const currentVersion = await readCurrentCliVersion().catch(() => 'unknown');
263
+ const packageName = resolveCliPackageName(kwargs);
264
+ const registry = resolveRegistry(kwargs);
265
+ const latestVersion = await fetchLatestCliVersion({ packageName, registry });
266
+ const updateAvailable = currentVersion !== 'unknown' && semverLessThan(currentVersion, latestVersion);
267
+ const checkOnly = toBool(kwargs.check);
268
+ const force = toBool(kwargs.force);
269
+ const installVersion = latestVersion || 'latest';
270
+ const installSpec = npmInstallSpec(packageName, force ? 'latest' : installVersion);
271
+ const installArgs = npmArgsWithRegistry(['install', '-g', installSpec], registry);
272
+
273
+ if (checkOnly) {
274
+ return {
275
+ checked: true,
276
+ updated: false,
277
+ updateAvailable,
278
+ packageName,
279
+ currentVersion,
280
+ latestVersion,
281
+ command: updateAvailable ? `${commandPrefix()} update` : null,
282
+ message: updateAvailable
283
+ ? `发现新版本 ${latestVersion},运行 ${commandPrefix()} update 更新。`
284
+ : '当前已是最新版本。',
285
+ };
286
+ }
287
+
288
+ if (!updateAvailable && !force) {
289
+ return {
290
+ checked: true,
291
+ updated: false,
292
+ updateAvailable: false,
293
+ packageName,
294
+ currentVersion,
295
+ latestVersion,
296
+ message: '当前已是最新版本。',
297
+ };
298
+ }
299
+
300
+ const result = await runNpm(installArgs, { timeoutMs: 120_000 });
301
+ await saveState({
302
+ updateCheck: {
303
+ packageName,
304
+ registry,
305
+ currentVersion: latestVersion,
306
+ latestVersion,
307
+ checkedAt: Date.now(),
308
+ checkedAtText: nowIso(),
309
+ },
310
+ }).catch(() => {});
311
+
312
+ return {
313
+ checked: true,
314
+ updated: true,
315
+ updateAvailable,
316
+ packageName,
317
+ previousVersion: currentVersion,
318
+ currentVersion: latestVersion,
319
+ latestVersion,
320
+ command: `npm ${installArgs.join(' ')}`,
321
+ skillUpdated: true,
322
+ restartRecommended: true,
323
+ message: '更新完成。请退出并重新打开 AI Agent,以加载最新 CLI 和 skill。',
324
+ stdoutTail: tailLines(result.stdout),
325
+ stderrTail: tailLines(result.stderr),
326
+ };
327
+ }