@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.
- package/README.md +45 -0
- package/build/_shared.mjs +54 -5
- package/build/prod.mjs +12 -3
- package/package.json +2 -2
- package/packages/awb-cli/package.json +2 -2
- package/packages/awb-core/package.json +1 -1
- package/packages/awb-core/src/api.js +22 -0
- package/packages/awb-core/src/commands.js +112 -39
- package/packages/awb-core/src/common.js +8 -0
- package/packages/awb-core/src/output.js +166 -9
- package/packages/awb-core/src/services.js +1669 -186
- package/packages/awb-core/src/standalone.js +110 -12
- package/packages/awb-core/src/update.js +327 -0
- package/skills/lj-awb/SKILL.md +29 -9
- package/skills/lj-awb/VERSION +1 -1
- package/skills/lj-awb/compat.json +3 -3
- package/skills/lj-awb/modules/asset.md +10 -1
- package/skills/lj-awb/modules/auth.md +9 -1
- package/skills/lj-awb/modules/create-contract.md +5 -2
- package/skills/lj-awb/modules/create.md +4 -2
- package/skills/lj-awb/modules/driver.md +1 -0
- package/skills/lj-awb/modules/image.md +3 -1
- package/skills/lj-awb/modules/model.md +5 -4
- package/skills/lj-awb/modules/task.md +4 -1
- package/skills/lj-awb/modules/upload.md +1 -1
- package/skills/lj-awb/modules/video.md +11 -2
- package/skills/lj-awb/modules/workflows.md +3 -1
- package/skills/lj-awb/references/error-codes.md +24 -0
- package/skills/lj-awb/references/model-options-read.md +7 -6
- package/skills/lj-awb/references/output-fields.md +8 -5
- package/skills/lj-awb/scripts/resolve-lj-awb-cmd.sh +106 -4
|
@@ -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:
|
|
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) =>
|
|
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, {
|
|
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
|
-
|
|
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
|
+
}
|