@formigio/fazemos-cli 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +626 -12
- package/dist/index.js.map +1 -1
- package/dist/monitor.d.ts +2 -0
- package/dist/monitor.js +533 -0
- package/dist/monitor.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -697,6 +697,26 @@ commitments
|
|
|
697
697
|
process.exit(1);
|
|
698
698
|
}
|
|
699
699
|
});
|
|
700
|
+
// ── Template I/O helpers ───────────────────────────────────
|
|
701
|
+
function allSteps(definition) {
|
|
702
|
+
const steps = [];
|
|
703
|
+
for (const phase of definition?.phases || []) {
|
|
704
|
+
for (const step of phase.steps || []) {
|
|
705
|
+
steps.push(step);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return steps;
|
|
709
|
+
}
|
|
710
|
+
function findStepById(definition, stepId) {
|
|
711
|
+
return allSteps(definition).find((s) => s.id === stepId);
|
|
712
|
+
}
|
|
713
|
+
const VALID_IO_TYPES = ['text', 'markdown', 'number', 'boolean', 'url', 'json', 'object', 'array'];
|
|
714
|
+
function requireDraftStatus(template) {
|
|
715
|
+
if (template.status !== 'draft') {
|
|
716
|
+
console.error(chalk.yellow(`Template is "${template.status}" — edits require draft status`));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
700
720
|
// ── Templates ──────────────────────────────────────────────
|
|
701
721
|
const templates = program.command('templates').alias('tpl').description('Pipeline template commands');
|
|
702
722
|
templates
|
|
@@ -734,11 +754,45 @@ templates
|
|
|
734
754
|
console.log(` ID: ${t.id}`);
|
|
735
755
|
console.log(` Status: ${t.status}`);
|
|
736
756
|
console.log(` Version: ${t.version}`);
|
|
757
|
+
// Pipeline-level inputs
|
|
758
|
+
if (t.definition?.inputs?.length) {
|
|
759
|
+
console.log(chalk.cyan('\n Pipeline Inputs:'));
|
|
760
|
+
for (const inp of t.definition.inputs) {
|
|
761
|
+
const req = inp.required === false ? 'optional' : 'required';
|
|
762
|
+
const desc = inp.description ? ` — ${inp.description}` : '';
|
|
763
|
+
const def = inp.default_value != null ? ` [default: ${inp.default_value}]` : '';
|
|
764
|
+
console.log(` ${inp.name} (${inp.type}, ${req})${desc}${def}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
737
767
|
if (t.definition?.phases) {
|
|
738
768
|
for (const phase of t.definition.phases) {
|
|
739
769
|
console.log(chalk.cyan(`\n Phase: ${phase.name}`));
|
|
740
770
|
for (const step of phase.steps || []) {
|
|
741
|
-
console.log(` ${step.name}
|
|
771
|
+
console.log(` Step: ${step.name} [${step.role || 'unassigned'}] (${step.step_type || step.stepType || 'human'})`);
|
|
772
|
+
if (step.outputs?.length) {
|
|
773
|
+
console.log(' Outputs:');
|
|
774
|
+
for (const o of step.outputs) {
|
|
775
|
+
const req = o.required === false ? 'optional' : 'required';
|
|
776
|
+
const desc = o.description ? ` — ${o.description}` : '';
|
|
777
|
+
console.log(` → ${o.name} (${o.type}, ${req})${desc}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (step.inputs?.length) {
|
|
781
|
+
console.log(' Inputs:');
|
|
782
|
+
for (const inp of step.inputs) {
|
|
783
|
+
if (inp.pipeline_input) {
|
|
784
|
+
console.log(` ← ${inp.name} ← pipeline.${inp.pipeline_input}`);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
const srcStep = findStepById(t.definition, inp.source_step_id);
|
|
788
|
+
const srcName = srcStep ? srcStep.name : inp.source_step_id;
|
|
789
|
+
console.log(` ← ${inp.name} ← ${srcName}.${inp.source_output_name}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
else if (step.outputs?.length) {
|
|
794
|
+
console.log(' Inputs: none');
|
|
795
|
+
}
|
|
742
796
|
}
|
|
743
797
|
}
|
|
744
798
|
}
|
|
@@ -774,26 +828,48 @@ templates
|
|
|
774
828
|
.argument('<file>', 'Path to JOE template JSON file')
|
|
775
829
|
.action(async (file) => {
|
|
776
830
|
try {
|
|
777
|
-
const { readFileSync } = await import('fs');
|
|
778
|
-
const { resolve } = await import('path');
|
|
779
831
|
const raw = JSON.parse(readFileSync(resolve(file), 'utf-8'));
|
|
780
832
|
// Map JOE template to Fazemos format
|
|
781
|
-
const
|
|
782
|
-
//
|
|
833
|
+
const joeSteps = raw.steps || [];
|
|
834
|
+
// Build ID mapping so input source_step_id references survive UUID reassignment
|
|
835
|
+
const idMap = new Map();
|
|
836
|
+
for (const s of joeSteps) {
|
|
837
|
+
if (s.id)
|
|
838
|
+
idMap.set(s.id, crypto.randomUUID());
|
|
839
|
+
}
|
|
783
840
|
const definition = {
|
|
841
|
+
inputs: (raw.inputs || []).map((i) => ({
|
|
842
|
+
name: i.name,
|
|
843
|
+
type: i.type || 'text',
|
|
844
|
+
required: i.required !== false,
|
|
845
|
+
...(i.description ? { description: i.description } : {}),
|
|
846
|
+
...(i.default_value != null ? { default_value: i.default_value } : {}),
|
|
847
|
+
})),
|
|
784
848
|
phases: [{
|
|
785
849
|
id: crypto.randomUUID(),
|
|
786
850
|
name: raw.name || 'Main',
|
|
787
851
|
description: raw.description || '',
|
|
788
852
|
deliverables: [],
|
|
789
|
-
steps:
|
|
790
|
-
id: crypto.randomUUID(),
|
|
853
|
+
steps: joeSteps.map((s, i) => ({
|
|
854
|
+
id: idMap.get(s.id) || crypto.randomUUID(),
|
|
791
855
|
name: s.name,
|
|
792
856
|
description: s.description || '',
|
|
793
857
|
step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
|
|
794
858
|
role: s.role || s.agent || 'unassigned',
|
|
795
|
-
inputs: []
|
|
796
|
-
|
|
859
|
+
inputs: (s.inputs || []).map((inp) => ({
|
|
860
|
+
name: inp.name,
|
|
861
|
+
...(inp.source_step_id ? { source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id, source_output_name: inp.source_output_name } : {}),
|
|
862
|
+
...(inp.pipeline_input ? { pipeline_input: inp.pipeline_input } : {}),
|
|
863
|
+
...(inp.description ? { description: inp.description } : {}),
|
|
864
|
+
required: inp.required !== false,
|
|
865
|
+
})),
|
|
866
|
+
outputs: (s.outputs || []).map((o) => ({
|
|
867
|
+
name: o.name,
|
|
868
|
+
type: o.type || 'text',
|
|
869
|
+
...(o.description ? { description: o.description } : {}),
|
|
870
|
+
required: o.required !== false,
|
|
871
|
+
...(o.format ? { format: o.format } : {}),
|
|
872
|
+
})),
|
|
797
873
|
sections: s.sections || '',
|
|
798
874
|
reviewer: s.reviewer || null,
|
|
799
875
|
max_review_cycles: s.maxReviewCycles || 0,
|
|
@@ -813,7 +889,7 @@ templates
|
|
|
813
889
|
};
|
|
814
890
|
const data = await api('POST', '/api/pipeline-templates', body);
|
|
815
891
|
const t = data.template;
|
|
816
|
-
console.log(chalk.green(`Imported: ${t.name} (${
|
|
892
|
+
console.log(chalk.green(`Imported: ${t.name} (${joeSteps.length} steps)`));
|
|
817
893
|
console.log(` ID: ${t.id}`);
|
|
818
894
|
}
|
|
819
895
|
catch (err) {
|
|
@@ -835,6 +911,438 @@ templates
|
|
|
835
911
|
process.exit(1);
|
|
836
912
|
}
|
|
837
913
|
});
|
|
914
|
+
// ── Template I/O commands ──────────────────────────────────
|
|
915
|
+
templates
|
|
916
|
+
.command('steps')
|
|
917
|
+
.description('List step IDs and names in a template')
|
|
918
|
+
.argument('<id>', 'Template ID')
|
|
919
|
+
.action(async (id) => {
|
|
920
|
+
try {
|
|
921
|
+
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
922
|
+
const steps = allSteps(data.template.definition);
|
|
923
|
+
if (!steps.length) {
|
|
924
|
+
console.log(chalk.yellow('No steps'));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
for (const s of steps) {
|
|
928
|
+
console.log(` ${chalk.dim(s.id)} ${s.name} [${s.role || 'unassigned'}]`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
console.error(chalk.red(err.message));
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
templates
|
|
937
|
+
.command('add-output')
|
|
938
|
+
.description('Add an output declaration to a step')
|
|
939
|
+
.argument('<templateId>', 'Template ID')
|
|
940
|
+
.requiredOption('--step <stepId>', 'Step ID')
|
|
941
|
+
.requiredOption('--name <name>', 'Output name')
|
|
942
|
+
.requiredOption('--type <type>', `Output type: ${VALID_IO_TYPES.join(', ')}`)
|
|
943
|
+
.option('--description <desc>', 'Description')
|
|
944
|
+
.option('--optional', 'Mark as not required')
|
|
945
|
+
.option('--format <format>', 'Format hint (file_path, commit_sha, semver, date, etc.)')
|
|
946
|
+
.action(async (templateId, opts) => {
|
|
947
|
+
try {
|
|
948
|
+
if (!VALID_IO_TYPES.includes(opts.type)) {
|
|
949
|
+
console.error(chalk.red(`Invalid type "${opts.type}". Valid: ${VALID_IO_TYPES.join(', ')}`));
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
953
|
+
const t = data.template;
|
|
954
|
+
requireDraftStatus(t);
|
|
955
|
+
const step = findStepById(t.definition, opts.step);
|
|
956
|
+
if (!step) {
|
|
957
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
if (!step.outputs)
|
|
961
|
+
step.outputs = [];
|
|
962
|
+
if (step.outputs.find((o) => o.name === opts.name)) {
|
|
963
|
+
console.error(chalk.red(`Output "${opts.name}" already exists on step "${step.name}"`));
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
const output = { name: opts.name, type: opts.type, required: !opts.optional };
|
|
967
|
+
if (opts.description)
|
|
968
|
+
output.description = opts.description;
|
|
969
|
+
if (opts.format)
|
|
970
|
+
output.format = opts.format;
|
|
971
|
+
step.outputs.push(output);
|
|
972
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
973
|
+
console.log(chalk.green(`Added output → ${opts.name} (${opts.type}) to ${step.name}`));
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
console.error(chalk.red(err.message));
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
templates
|
|
981
|
+
.command('remove-output')
|
|
982
|
+
.description('Remove an output declaration from a step')
|
|
983
|
+
.argument('<templateId>', 'Template ID')
|
|
984
|
+
.requiredOption('--step <stepId>', 'Step ID')
|
|
985
|
+
.requiredOption('--name <name>', 'Output name')
|
|
986
|
+
.option('--force', 'Remove even if downstream steps reference this output')
|
|
987
|
+
.action(async (templateId, opts) => {
|
|
988
|
+
try {
|
|
989
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
990
|
+
const t = data.template;
|
|
991
|
+
requireDraftStatus(t);
|
|
992
|
+
const step = findStepById(t.definition, opts.step);
|
|
993
|
+
if (!step) {
|
|
994
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
const idx = (step.outputs || []).findIndex((o) => o.name === opts.name);
|
|
998
|
+
if (idx === -1) {
|
|
999
|
+
console.error(chalk.red(`Output "${opts.name}" not found on step "${step.name}"`));
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
// Check for downstream references
|
|
1003
|
+
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => inp.source_step_id === step.id && inp.source_output_name === opts.name));
|
|
1004
|
+
if (refs.length && !opts.force) {
|
|
1005
|
+
console.error(chalk.yellow(`Output "${opts.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
|
|
1006
|
+
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
step.outputs.splice(idx, 1);
|
|
1010
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1011
|
+
console.log(chalk.green(`Removed output → ${opts.name} from ${step.name}`));
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
console.error(chalk.red(err.message));
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
templates
|
|
1019
|
+
.command('add-input')
|
|
1020
|
+
.description('Add an input declaration to a step')
|
|
1021
|
+
.argument('<templateId>', 'Template ID')
|
|
1022
|
+
.requiredOption('--step <stepId>', 'Step ID')
|
|
1023
|
+
.requiredOption('--name <name>', 'Input name')
|
|
1024
|
+
.option('--source-step <stepId>', 'Source step ID (for step-sourced inputs)')
|
|
1025
|
+
.option('--source-output <name>', 'Source output name (for step-sourced inputs)')
|
|
1026
|
+
.option('--pipeline-input <name>', 'Pipeline-level input name (for pipeline-sourced inputs)')
|
|
1027
|
+
.option('--description <desc>', 'Description')
|
|
1028
|
+
.option('--optional', 'Mark as not required')
|
|
1029
|
+
.action(async (templateId, opts) => {
|
|
1030
|
+
try {
|
|
1031
|
+
const hasSrc = opts.sourceStep || opts.sourceOutput;
|
|
1032
|
+
const hasPipeline = opts.pipelineInput;
|
|
1033
|
+
if (!hasSrc && !hasPipeline) {
|
|
1034
|
+
console.error(chalk.red('Provide either --source-step + --source-output or --pipeline-input'));
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
if (hasSrc && hasPipeline) {
|
|
1038
|
+
console.error(chalk.red('Cannot use both --source-step and --pipeline-input'));
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
if (hasSrc && (!opts.sourceStep || !opts.sourceOutput)) {
|
|
1042
|
+
console.error(chalk.red('Both --source-step and --source-output are required for step-sourced inputs'));
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1046
|
+
const t = data.template;
|
|
1047
|
+
requireDraftStatus(t);
|
|
1048
|
+
const step = findStepById(t.definition, opts.step);
|
|
1049
|
+
if (!step) {
|
|
1050
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
if (!step.inputs)
|
|
1054
|
+
step.inputs = [];
|
|
1055
|
+
if (step.inputs.find((i) => i.name === opts.name)) {
|
|
1056
|
+
console.error(chalk.red(`Input "${opts.name}" already exists on step "${step.name}"`));
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
// Validate references
|
|
1060
|
+
if (opts.sourceStep) {
|
|
1061
|
+
if (opts.sourceStep === step.id) {
|
|
1062
|
+
console.error(chalk.red('Cannot reference own step (self-reference)'));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
const srcStep = findStepById(t.definition, opts.sourceStep);
|
|
1066
|
+
if (!srcStep) {
|
|
1067
|
+
console.error(chalk.red(`Source step "${opts.sourceStep}" not found`));
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
if (!(srcStep.outputs || []).find((o) => o.name === opts.sourceOutput)) {
|
|
1071
|
+
console.error(chalk.red(`Output "${opts.sourceOutput}" not declared on step "${srcStep.name}"`));
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (opts.pipelineInput) {
|
|
1076
|
+
const pInputs = t.definition.inputs || [];
|
|
1077
|
+
if (!pInputs.find((i) => i.name === opts.pipelineInput)) {
|
|
1078
|
+
console.error(chalk.red(`Pipeline input "${opts.pipelineInput}" not found. Add it first with tpl add-pipeline-input`));
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const input = { name: opts.name, required: !opts.optional };
|
|
1083
|
+
if (opts.sourceStep) {
|
|
1084
|
+
input.source_step_id = opts.sourceStep;
|
|
1085
|
+
input.source_output_name = opts.sourceOutput;
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
input.pipeline_input = opts.pipelineInput;
|
|
1089
|
+
}
|
|
1090
|
+
if (opts.description)
|
|
1091
|
+
input.description = opts.description;
|
|
1092
|
+
step.inputs.push(input);
|
|
1093
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1094
|
+
if (opts.sourceStep) {
|
|
1095
|
+
const srcStep = findStepById(t.definition, opts.sourceStep);
|
|
1096
|
+
console.log(chalk.green(`Added input ← ${opts.name} ← ${srcStep.name}.${opts.sourceOutput} to ${step.name}`));
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
console.log(chalk.green(`Added input ← ${opts.name} ← pipeline.${opts.pipelineInput} to ${step.name}`));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
console.error(chalk.red(err.message));
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
templates
|
|
1108
|
+
.command('remove-input')
|
|
1109
|
+
.description('Remove an input declaration from a step')
|
|
1110
|
+
.argument('<templateId>', 'Template ID')
|
|
1111
|
+
.requiredOption('--step <stepId>', 'Step ID')
|
|
1112
|
+
.requiredOption('--name <name>', 'Input name')
|
|
1113
|
+
.action(async (templateId, opts) => {
|
|
1114
|
+
try {
|
|
1115
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1116
|
+
const t = data.template;
|
|
1117
|
+
requireDraftStatus(t);
|
|
1118
|
+
const step = findStepById(t.definition, opts.step);
|
|
1119
|
+
if (!step) {
|
|
1120
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
const idx = (step.inputs || []).findIndex((i) => i.name === opts.name);
|
|
1124
|
+
if (idx === -1) {
|
|
1125
|
+
console.error(chalk.red(`Input "${opts.name}" not found on step "${step.name}"`));
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
step.inputs.splice(idx, 1);
|
|
1129
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1130
|
+
console.log(chalk.green(`Removed input ← ${opts.name} from ${step.name}`));
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
console.error(chalk.red(err.message));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
templates
|
|
1138
|
+
.command('add-pipeline-input')
|
|
1139
|
+
.description('Add a pipeline-level input to the template')
|
|
1140
|
+
.argument('<templateId>', 'Template ID')
|
|
1141
|
+
.requiredOption('--name <name>', 'Input name')
|
|
1142
|
+
.requiredOption('--type <type>', `Input type: ${VALID_IO_TYPES.join(', ')}`)
|
|
1143
|
+
.option('--description <desc>', 'Description')
|
|
1144
|
+
.option('--optional', 'Not required at instance creation')
|
|
1145
|
+
.option('--default <value>', 'Default value')
|
|
1146
|
+
.action(async (templateId, opts) => {
|
|
1147
|
+
try {
|
|
1148
|
+
if (!VALID_IO_TYPES.includes(opts.type)) {
|
|
1149
|
+
console.error(chalk.red(`Invalid type "${opts.type}". Valid: ${VALID_IO_TYPES.join(', ')}`));
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1153
|
+
const t = data.template;
|
|
1154
|
+
requireDraftStatus(t);
|
|
1155
|
+
if (!t.definition.inputs)
|
|
1156
|
+
t.definition.inputs = [];
|
|
1157
|
+
if (t.definition.inputs.find((i) => i.name === opts.name)) {
|
|
1158
|
+
console.error(chalk.red(`Pipeline input "${opts.name}" already exists`));
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
const input = { name: opts.name, type: opts.type, required: !opts.optional };
|
|
1162
|
+
if (opts.description)
|
|
1163
|
+
input.description = opts.description;
|
|
1164
|
+
if (opts.default != null)
|
|
1165
|
+
input.default_value = opts.default;
|
|
1166
|
+
t.definition.inputs.push(input);
|
|
1167
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1168
|
+
console.log(chalk.green(`Added pipeline input: ${opts.name} (${opts.type})`));
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
console.error(chalk.red(err.message));
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
templates
|
|
1176
|
+
.command('remove-pipeline-input')
|
|
1177
|
+
.description('Remove a pipeline-level input from the template')
|
|
1178
|
+
.argument('<templateId>', 'Template ID')
|
|
1179
|
+
.requiredOption('--name <name>', 'Input name')
|
|
1180
|
+
.option('--force', 'Remove even if steps reference this input')
|
|
1181
|
+
.action(async (templateId, opts) => {
|
|
1182
|
+
try {
|
|
1183
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1184
|
+
const t = data.template;
|
|
1185
|
+
requireDraftStatus(t);
|
|
1186
|
+
const inputs = t.definition.inputs || [];
|
|
1187
|
+
const idx = inputs.findIndex((i) => i.name === opts.name);
|
|
1188
|
+
if (idx === -1) {
|
|
1189
|
+
console.error(chalk.red(`Pipeline input "${opts.name}" not found`));
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
// Block if steps reference it unless --force
|
|
1193
|
+
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => inp.pipeline_input === opts.name));
|
|
1194
|
+
if (refs.length && !opts.force) {
|
|
1195
|
+
console.error(chalk.yellow(`Pipeline input "${opts.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
|
|
1196
|
+
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
}
|
|
1199
|
+
inputs.splice(idx, 1);
|
|
1200
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1201
|
+
console.log(chalk.green(`Removed pipeline input: ${opts.name}`));
|
|
1202
|
+
}
|
|
1203
|
+
catch (err) {
|
|
1204
|
+
console.error(chalk.red(err.message));
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
templates
|
|
1209
|
+
.command('validate')
|
|
1210
|
+
.description('Validate template I/O contracts')
|
|
1211
|
+
.argument('<id>', 'Template ID')
|
|
1212
|
+
.action(async (id) => {
|
|
1213
|
+
try {
|
|
1214
|
+
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
1215
|
+
const t = data.template;
|
|
1216
|
+
const steps = allSteps(t.definition);
|
|
1217
|
+
const pipelineInputs = t.definition.inputs || [];
|
|
1218
|
+
console.log(`Validating: ${chalk.cyan(t.name)} (${id})\n`);
|
|
1219
|
+
let passed = 0, warnings = 0, infos = 0, errors = 0;
|
|
1220
|
+
// Check for duplicate input names per step
|
|
1221
|
+
let hasDupes = false;
|
|
1222
|
+
for (const step of steps) {
|
|
1223
|
+
const names = (step.inputs || []).map((i) => i.name);
|
|
1224
|
+
const dupes = names.filter((n, i) => names.indexOf(n) !== i);
|
|
1225
|
+
if (dupes.length) {
|
|
1226
|
+
console.log(chalk.red(` ✗ Step "${step.name}" has duplicate input names: ${[...new Set(dupes)].join(', ')}`));
|
|
1227
|
+
hasDupes = true;
|
|
1228
|
+
errors++;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (!hasDupes) {
|
|
1232
|
+
console.log(chalk.green(' ✓ No duplicate input names'));
|
|
1233
|
+
passed++;
|
|
1234
|
+
}
|
|
1235
|
+
// Check self-references
|
|
1236
|
+
let hasSelfRef = false;
|
|
1237
|
+
for (const step of steps) {
|
|
1238
|
+
for (const inp of step.inputs || []) {
|
|
1239
|
+
if (inp.source_step_id === step.id) {
|
|
1240
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references itself`));
|
|
1241
|
+
hasSelfRef = true;
|
|
1242
|
+
errors++;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (!hasSelfRef) {
|
|
1247
|
+
console.log(chalk.green(' ✓ No self-references'));
|
|
1248
|
+
passed++;
|
|
1249
|
+
}
|
|
1250
|
+
// Check all input references resolve
|
|
1251
|
+
let hasUnresolved = false;
|
|
1252
|
+
for (const step of steps) {
|
|
1253
|
+
for (const inp of step.inputs || []) {
|
|
1254
|
+
if (inp.source_step_id) {
|
|
1255
|
+
const src = findStepById(t.definition, inp.source_step_id);
|
|
1256
|
+
if (!src) {
|
|
1257
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references missing step "${inp.source_step_id}"`));
|
|
1258
|
+
hasUnresolved = true;
|
|
1259
|
+
errors++;
|
|
1260
|
+
}
|
|
1261
|
+
else if (!(src.outputs || []).find((o) => o.name === inp.source_output_name)) {
|
|
1262
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references missing output "${inp.source_output_name}" on step "${src.name}"`));
|
|
1263
|
+
hasUnresolved = true;
|
|
1264
|
+
errors++;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (inp.pipeline_input) {
|
|
1268
|
+
if (!pipelineInputs.find((p) => p.name === inp.pipeline_input)) {
|
|
1269
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references missing pipeline input "${inp.pipeline_input}"`));
|
|
1270
|
+
hasUnresolved = true;
|
|
1271
|
+
errors++;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (!hasUnresolved) {
|
|
1277
|
+
console.log(chalk.green(' ✓ All input references resolve'));
|
|
1278
|
+
passed++;
|
|
1279
|
+
}
|
|
1280
|
+
// Cycle detection via topological sort (Kahn's algorithm)
|
|
1281
|
+
const stepIds = steps.map((s) => s.id);
|
|
1282
|
+
const adj = new Map();
|
|
1283
|
+
const inDeg = new Map();
|
|
1284
|
+
for (const sid of stepIds) {
|
|
1285
|
+
adj.set(sid, []);
|
|
1286
|
+
inDeg.set(sid, 0);
|
|
1287
|
+
}
|
|
1288
|
+
for (const step of steps) {
|
|
1289
|
+
for (const inp of step.inputs || []) {
|
|
1290
|
+
if (inp.source_step_id && stepIds.includes(inp.source_step_id)) {
|
|
1291
|
+
adj.get(inp.source_step_id).push(step.id);
|
|
1292
|
+
inDeg.set(step.id, (inDeg.get(step.id) || 0) + 1);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const queue = [];
|
|
1297
|
+
for (const [id, deg] of inDeg) {
|
|
1298
|
+
if (deg === 0)
|
|
1299
|
+
queue.push(id);
|
|
1300
|
+
}
|
|
1301
|
+
let visited = 0;
|
|
1302
|
+
while (queue.length) {
|
|
1303
|
+
const cur = queue.shift();
|
|
1304
|
+
visited++;
|
|
1305
|
+
for (const next of adj.get(cur) || []) {
|
|
1306
|
+
const nd = inDeg.get(next) - 1;
|
|
1307
|
+
inDeg.set(next, nd);
|
|
1308
|
+
if (nd === 0)
|
|
1309
|
+
queue.push(next);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (visited < stepIds.length) {
|
|
1313
|
+
console.log(chalk.red(' ✗ Cycle detected in step dependencies'));
|
|
1314
|
+
errors++;
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
console.log(chalk.green(' ✓ No cycles detected'));
|
|
1318
|
+
passed++;
|
|
1319
|
+
}
|
|
1320
|
+
// Unreferenced outputs (warning)
|
|
1321
|
+
for (const step of steps) {
|
|
1322
|
+
for (const output of step.outputs || []) {
|
|
1323
|
+
const referenced = steps.some((s) => (s.inputs || []).some((inp) => inp.source_step_id === step.id && inp.source_output_name === output.name));
|
|
1324
|
+
if (!referenced) {
|
|
1325
|
+
console.log(chalk.yellow(` ⚠ Step "${step.name}" output "${output.name}" is not consumed by any downstream step`));
|
|
1326
|
+
warnings++;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Steps with no outputs (info)
|
|
1331
|
+
for (const step of steps) {
|
|
1332
|
+
if (!step.outputs?.length) {
|
|
1333
|
+
console.log(chalk.dim(` ℹ Step "${step.name}" has no outputs declared`));
|
|
1334
|
+
infos++;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
console.log(`\n${passed} check${passed !== 1 ? 's' : ''} passed${errors ? `, ${errors} error${errors !== 1 ? 's' : ''}` : ''}${warnings ? `, ${warnings} warning${warnings !== 1 ? 's' : ''}` : ''}${infos ? `, ${infos} info` : ''}`);
|
|
1338
|
+
if (errors)
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
catch (err) {
|
|
1342
|
+
console.error(chalk.red(err.message));
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
838
1346
|
// ── Pipelines ──────────────────────────────────────────────
|
|
839
1347
|
const pipelines = program.command('pipelines').alias('pl').description('Pipeline instance commands');
|
|
840
1348
|
pipelines
|
|
@@ -1535,6 +2043,88 @@ executions
|
|
|
1535
2043
|
process.exit(1);
|
|
1536
2044
|
}
|
|
1537
2045
|
});
|
|
2046
|
+
executions
|
|
2047
|
+
.command('cloudwatch-logs')
|
|
2048
|
+
.alias('cw')
|
|
2049
|
+
.description('Show container logs from CloudWatch')
|
|
2050
|
+
.argument('<id>', 'Execution ID')
|
|
2051
|
+
.option('-l, --limit <n>', 'Max log lines', parseNumber, 100)
|
|
2052
|
+
.option('-f, --follow', 'Poll for new lines until execution completes')
|
|
2053
|
+
.option('--since <duration>', 'Show logs since (e.g., 5m, 1h)')
|
|
2054
|
+
.action(async (id, opts) => {
|
|
2055
|
+
try {
|
|
2056
|
+
const isTerminal = (status) => ['completed', 'failed', 'cancelled'].includes(status);
|
|
2057
|
+
const parseSince = (since) => {
|
|
2058
|
+
const match = since.match(/^(\d+)(m|h|d)$/);
|
|
2059
|
+
if (!match)
|
|
2060
|
+
throw new Error('Invalid --since format. Use e.g. 5m, 1h, 2d');
|
|
2061
|
+
const val = parseInt(match[1], 10);
|
|
2062
|
+
const unit = match[2];
|
|
2063
|
+
const multipliers = { m: 60000, h: 3600000, d: 86400000 };
|
|
2064
|
+
return Date.now() - val * multipliers[unit];
|
|
2065
|
+
};
|
|
2066
|
+
const startTime = opts.since ? String(parseSince(opts.since)) : undefined;
|
|
2067
|
+
const fetchLogs = async (nextToken) => {
|
|
2068
|
+
const params = new URLSearchParams();
|
|
2069
|
+
params.set('limit', String(opts.limit));
|
|
2070
|
+
if (startTime)
|
|
2071
|
+
params.set('startTime', startTime);
|
|
2072
|
+
if (nextToken)
|
|
2073
|
+
params.set('nextToken', nextToken);
|
|
2074
|
+
return await api('GET', `/api/executions/${id}/cloudwatch-logs?${params}`);
|
|
2075
|
+
};
|
|
2076
|
+
const formatLine = (event) => {
|
|
2077
|
+
const ts = new Date(event.timestamp);
|
|
2078
|
+
const hh = String(ts.getHours()).padStart(2, '0');
|
|
2079
|
+
const mm = String(ts.getMinutes()).padStart(2, '0');
|
|
2080
|
+
const ss = String(ts.getSeconds()).padStart(2, '0');
|
|
2081
|
+
return `${chalk.gray(`[${hh}:${mm}:${ss}]`)} ${event.message}`;
|
|
2082
|
+
};
|
|
2083
|
+
// Initial fetch
|
|
2084
|
+
let data = await fetchLogs();
|
|
2085
|
+
if (!data.logs?.length && !opts.follow) {
|
|
2086
|
+
console.log(chalk.yellow(data.message || 'No log events'));
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
for (const event of data.logs || []) {
|
|
2090
|
+
console.log(formatLine(event));
|
|
2091
|
+
}
|
|
2092
|
+
if (!opts.follow)
|
|
2093
|
+
return;
|
|
2094
|
+
// Poll loop
|
|
2095
|
+
let token = data.nextToken;
|
|
2096
|
+
while (true) {
|
|
2097
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2098
|
+
// Check execution status
|
|
2099
|
+
const exData = await api('GET', `/api/executions/${id}`);
|
|
2100
|
+
const status = exData.execution?.status;
|
|
2101
|
+
// Fetch new logs
|
|
2102
|
+
data = await fetchLogs(token);
|
|
2103
|
+
for (const event of data.logs || []) {
|
|
2104
|
+
console.log(formatLine(event));
|
|
2105
|
+
}
|
|
2106
|
+
token = data.nextToken;
|
|
2107
|
+
if (isTerminal(status)) {
|
|
2108
|
+
// Drain remaining log pages
|
|
2109
|
+
let prevToken;
|
|
2110
|
+
while (token && token !== prevToken) {
|
|
2111
|
+
prevToken = token;
|
|
2112
|
+
data = await fetchLogs(token);
|
|
2113
|
+
for (const event of data.logs || []) {
|
|
2114
|
+
console.log(formatLine(event));
|
|
2115
|
+
}
|
|
2116
|
+
token = data.nextToken;
|
|
2117
|
+
}
|
|
2118
|
+
console.log(chalk.gray(`\n[${status}]`));
|
|
2119
|
+
break;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
catch (err) {
|
|
2124
|
+
console.error(chalk.red(err.message));
|
|
2125
|
+
process.exit(1);
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
1538
2128
|
// ── My Work ─────────────────────────────────────────────────
|
|
1539
2129
|
program
|
|
1540
2130
|
.command('my-work')
|
|
@@ -1958,14 +2548,18 @@ agentsCmd
|
|
|
1958
2548
|
let skipped = 0;
|
|
1959
2549
|
const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
|
|
1960
2550
|
for (const file of files) {
|
|
1961
|
-
const
|
|
2551
|
+
const raw = readFileSync(resolve(resolvedDir, file), 'utf-8');
|
|
2552
|
+
// Extract name from frontmatter (name: field), fall back to filename
|
|
2553
|
+
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2554
|
+
const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';
|
|
2555
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
2556
|
+
const name = nameMatch ? nameMatch[1].trim() : basename(file, '.md');
|
|
1962
2557
|
const agent = agents.find((a) => norm(a.display_name) === norm(name));
|
|
1963
2558
|
if (!agent) {
|
|
1964
2559
|
console.log(chalk.yellow(` ⊘ ${name} — no matching agent`));
|
|
1965
2560
|
skipped++;
|
|
1966
2561
|
continue;
|
|
1967
2562
|
}
|
|
1968
|
-
const raw = readFileSync(resolve(resolvedDir, file), 'utf-8');
|
|
1969
2563
|
const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
|
|
1970
2564
|
await api('PATCH', `/api/members/${agent.id}/agent-config`, { systemPrompt: body });
|
|
1971
2565
|
console.log(chalk.green(` ✓ ${agent.display_name} (${body.length} chars)`));
|
|
@@ -2059,5 +2653,25 @@ apiKeys
|
|
|
2059
2653
|
process.exit(1);
|
|
2060
2654
|
}
|
|
2061
2655
|
});
|
|
2656
|
+
// ── Monitor ─────────────────────────────────────────────────
|
|
2657
|
+
program
|
|
2658
|
+
.command('monitor')
|
|
2659
|
+
.description('Launch web-based execution monitor (live logs, status)')
|
|
2660
|
+
.option('-p, --port <port>', 'Port to listen on', '4600')
|
|
2661
|
+
.action(async (opts) => {
|
|
2662
|
+
try {
|
|
2663
|
+
const port = parseInt(opts.port, 10);
|
|
2664
|
+
const { startMonitor } = await import('./monitor.js');
|
|
2665
|
+
await startMonitor(port);
|
|
2666
|
+
console.log(chalk.green(`\n ◆ Fazemos Execution Monitor`));
|
|
2667
|
+
console.log(` ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
2668
|
+
console.log(chalk.gray(`\n Proxying API calls with your current auth session`));
|
|
2669
|
+
console.log(chalk.gray(` Press Ctrl+C to stop\n`));
|
|
2670
|
+
}
|
|
2671
|
+
catch (err) {
|
|
2672
|
+
console.error(chalk.red(err.message));
|
|
2673
|
+
process.exit(1);
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2062
2676
|
program.parse();
|
|
2063
2677
|
//# sourceMappingURL=index.js.map
|