@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 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} (${step.stepType || 'human'}) — role: ${step.role || 'unassigned'}`);
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 steps = raw.steps || [];
782
- // Group sequential steps into a single phase for now
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: steps.map((s, i) => ({
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
- outputs: [],
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} (${steps.length} steps)`));
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 name = basename(file, '.md');
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