flowmind 1.5.4 → 1.5.6

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.
@@ -0,0 +1,74 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ function compareVersions(currentVersion, latestVersion) {
4
+ const current = parseVersion(currentVersion);
5
+ const latest = parseVersion(latestVersion);
6
+
7
+ for (let index = 0; index < 3; index += 1) {
8
+ if (latest[index] > current[index]) return -1;
9
+ if (latest[index] < current[index]) return 1;
10
+ }
11
+
12
+ return 0;
13
+ }
14
+
15
+ function parseVersion(version) {
16
+ return String(version || '0.0.0')
17
+ .split('.')
18
+ .slice(0, 3)
19
+ .map((part) => Number.parseInt(part, 10) || 0);
20
+ }
21
+
22
+ function getLatestVersion(packageName = 'flowmind') {
23
+ return execSync(`npm view ${packageName} version`, { encoding: 'utf-8' }).trim();
24
+ }
25
+
26
+ function isGlobalInstall(packageJsonPath, packageName = 'flowmind') {
27
+ try {
28
+ const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
29
+ return String(packageJsonPath || '').startsWith(globalRoot) || String(packageJsonPath || '').includes(`${globalRoot}/${packageName}`);
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function buildInstallCommand({ packageName = 'flowmind', version, globalInstall = false }) {
36
+ if (!version) {
37
+ return globalInstall ? `npm install -g ${packageName}@latest` : `npm install ${packageName}@latest`;
38
+ }
39
+
40
+ return globalInstall
41
+ ? `npm install -g ${packageName}@${version}`
42
+ : `npm install ${packageName}@${version}`;
43
+ }
44
+
45
+ function getUpdateStatus({
46
+ packageName = 'flowmind',
47
+ currentVersion = '0.0.0',
48
+ packageJsonPath = ''
49
+ } = {}) {
50
+ const latestVersion = getLatestVersion(packageName);
51
+ const comparison = compareVersions(currentVersion, latestVersion);
52
+ const globalInstall = isGlobalInstall(packageJsonPath, packageName);
53
+
54
+ return {
55
+ packageName,
56
+ currentVersion,
57
+ latestVersion,
58
+ comparison,
59
+ globalInstall,
60
+ installCommand: buildInstallCommand({
61
+ packageName,
62
+ version: latestVersion,
63
+ globalInstall
64
+ })
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ buildInstallCommand,
70
+ compareVersions,
71
+ getLatestVersion,
72
+ getUpdateStatus,
73
+ isGlobalInstall
74
+ };
package/dashboard/app.jsx CHANGED
@@ -1,27 +1,86 @@
1
1
  const React = require('react');
2
- const { Box, useApp, useInput } = require('ink');
2
+ const { Box, Text, useApp, useInput } = require('ink');
3
3
  const ActivityFeed = require('./components/ActivityFeed.jsx');
4
4
  const StatsRow = require('./components/StatsRow.jsx');
5
5
  const DragonPanel = require('./components/DragonPanel.jsx');
6
6
  const McpStatusBar = require('./components/McpStatusBar.jsx');
7
+ const { getTuiLayout, MIN_COLUMNS, MIN_ROWS } = require('../tui/layout');
8
+
9
+ class DashboardErrorBoundary extends React.Component {
10
+ constructor(props) {
11
+ super(props);
12
+ this.state = { error: null };
13
+ }
14
+
15
+ static getDerivedStateFromError(error) {
16
+ return { error };
17
+ }
18
+
19
+ render() {
20
+ if (this.state.error) {
21
+ return React.createElement(
22
+ Box,
23
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'red', paddingX: 1, paddingY: 1 },
24
+ React.createElement(Text, { bold: true, color: 'red' }, 'Dashboard render error'),
25
+ React.createElement(Text, { color: 'gray' }, this.state.error.message || 'Unknown render failure'),
26
+ React.createElement(Text, { color: 'gray' }, 'Resize the terminal or restart FlowMind.')
27
+ );
28
+ }
29
+ return this.props.children;
30
+ }
31
+ }
7
32
 
8
33
  function DashboardApp({ flowmind, eventBus, asciiMode = false }) {
9
34
  const { exit } = useApp();
35
+ const [terminalSize, setTerminalSize] = React.useState(() => ({
36
+ columns: Number(process.stdout?.columns) || 0,
37
+ rows: Number(process.stdout?.rows) || 0
38
+ }));
39
+
40
+ React.useEffect(() => {
41
+ const updateSize = () => {
42
+ setTerminalSize({
43
+ columns: Number(process.stdout?.columns) || 0,
44
+ rows: Number(process.stdout?.rows) || 0
45
+ });
46
+ };
47
+
48
+ updateSize();
49
+ process.stdout?.on?.('resize', updateSize);
50
+
51
+ return () => {
52
+ process.stdout?.removeListener?.('resize', updateSize);
53
+ };
54
+ }, []);
10
55
 
11
56
  useInput((input, key) => {
12
57
  if (key.ctrl && input === 'c') exit();
13
58
  });
14
59
 
60
+ const layout = getTuiLayout(terminalSize.columns, terminalSize.rows);
61
+
62
+ if (layout.tooSmall) {
63
+ return (
64
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'red', paddingX: 1, paddingY: 1 },
65
+ React.createElement(Text, { bold: true, color: 'red' }, 'Terminal too small'),
66
+ React.createElement(Text, { color: 'gray' }, `Need at least ${MIN_COLUMNS}x${MIN_ROWS}. Current size: ${layout.columns}x${layout.rows}.`),
67
+ React.createElement(Text, { color: 'gray' }, 'Resize the terminal to keep the dashboard stable.')
68
+ )
69
+ );
70
+ }
71
+
15
72
  return (
16
- React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
17
- React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
18
- React.createElement(ActivityFeed, { eventBus: eventBus, asciiMode: asciiMode }),
19
- React.createElement(Box, { flexDirection: 'column', width: '60%', flexGrow: 1 },
20
- React.createElement(StatsRow, { flowmind: flowmind, asciiMode: asciiMode }),
21
- React.createElement(DragonPanel, { flowmind: flowmind, asciiMode: asciiMode })
22
- )
23
- ),
24
- React.createElement(McpStatusBar, { eventBus: eventBus, asciiMode: asciiMode })
73
+ React.createElement(DashboardErrorBoundary, null,
74
+ React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
75
+ React.createElement(Box, { flexDirection: layout.columns < 120 ? 'column' : 'row', flexGrow: 1 },
76
+ React.createElement(ActivityFeed, { eventBus: eventBus, asciiMode: asciiMode, width: layout.columns, compact: layout.columns < 120 }),
77
+ React.createElement(Box, { flexDirection: 'column', width: layout.columns < 120 ? '100%' : '60%', flexGrow: 1 },
78
+ React.createElement(StatsRow, { flowmind: flowmind, asciiMode: asciiMode, width: layout.columns, compact: layout.columns < 120 }),
79
+ React.createElement(DragonPanel, { flowmind: flowmind, asciiMode: asciiMode, width: layout.columns, compact: layout.columns < 120 })
80
+ )
81
+ ),
82
+ React.createElement(McpStatusBar, { eventBus: eventBus, asciiMode: asciiMode, width: layout.columns, compact: layout.columns < 120 })
83
+ )
25
84
  )
26
85
  );
27
86
  }
@@ -38,7 +38,7 @@ function formatEvent(event, asciiMode) {
38
38
  }
39
39
  }
40
40
 
41
- function ActivityFeed({ eventBus, asciiMode = false }) {
41
+ function ActivityFeed({ eventBus, asciiMode = false, width = 0, compact = false }) {
42
42
  const [events, setEvents] = React.useState([]);
43
43
 
44
44
  React.useEffect(() => {
@@ -66,13 +66,14 @@ function ActivityFeed({ eventBus, asciiMode = false }) {
66
66
  };
67
67
  }, [eventBus]);
68
68
 
69
- const displayEvents = events.slice(-30);
69
+ const displayEvents = events.slice(-(compact ? 12 : 30));
70
+ const feedWidth = compact ? '100%' : '40%';
70
71
 
71
72
  return (
72
- React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'green', paddingX: 1, width: '40%' },
73
+ React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'green', paddingX: 1, width: feedWidth },
73
74
  React.createElement(Text, { bold: true, color: 'green' }, 'Activity Feed'),
74
75
  React.createElement(Box, { flexDirection: 'column', marginTop: 1, overflow: 'hidden' },
75
- displayEvents.length === 0 && React.createElement(Text, { color: 'gray' }, 'Waiting for events...'),
76
+ displayEvents.length === 0 && React.createElement(Text, { color: 'gray' }, compact ? 'Waiting...' : 'Waiting for events...'),
76
77
  displayEvents.map((event, i) =>
77
78
  React.createElement(Text, { key: i },
78
79
  React.createElement(Text, { color: 'gray' }, formatTime(event.timestamp) + ' '),
@@ -2,7 +2,7 @@ const React = require('react');
2
2
  const { Box, Text } = require('ink');
3
3
  const { LEVEL_COLORS, LEVEL_NAMES, LEVEL_STATES, getBorderStyle, getDragonArt } = require('../../tui/ui');
4
4
 
5
- function DragonPanel({ flowmind, asciiMode = false }) {
5
+ function DragonPanel({ flowmind, asciiMode = false, width = 0, compact = false }) {
6
6
  const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
7
7
 
8
8
  React.useEffect(() => {
@@ -23,6 +23,22 @@ function DragonPanel({ flowmind, asciiMode = false }) {
23
23
  const nextLevelPoints = [1, 10, 30, 60, 100];
24
24
  const nextPoints = nextLevelPoints[level] || null;
25
25
  const pointsToNext = nextPoints !== null ? nextPoints - honorData.points : 0;
26
+ const superCompact = compact || width < 120;
27
+
28
+ if (superCompact) {
29
+ return (
30
+ React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'cyan', paddingX: 1, flexGrow: 1 },
31
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
32
+ React.createElement(Text, null,
33
+ React.createElement(Text, { color: 'yellow', bold: true }, 'Lv' + level),
34
+ React.createElement(Text, { color: 'white' }, ' ' + levelName)
35
+ ),
36
+ React.createElement(Text, { color: 'gray' }, 'State: ' + state),
37
+ React.createElement(Text, { color: 'gray' }, honorData.points + ' pts'),
38
+ pointsToNext > 0 && React.createElement(Text, { color: 'gray' }, pointsToNext + ' to next')
39
+ )
40
+ );
41
+ }
26
42
 
27
43
  return (
28
44
  React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'cyan', paddingX: 1, flexGrow: 1 },
@@ -2,10 +2,11 @@ const React = require('react');
2
2
  const { Box, Text } = require('ink');
3
3
  const { getBorderStyle } = require('../../tui/ui');
4
4
 
5
- function McpStatusBar({ eventBus, asciiMode = false }) {
5
+ function McpStatusBar({ eventBus, asciiMode = false, width = 0, compact = false }) {
6
6
  const [toolCount, setToolCount] = React.useState(0);
7
7
  const [lastCall, setLastCall] = React.useState(null);
8
8
  const [serverState, setServerState] = React.useState('running');
9
+ const isCompact = compact || width < 120;
9
10
 
10
11
  React.useEffect(() => {
11
12
  if (!eventBus) return;
@@ -19,6 +20,23 @@ function McpStatusBar({ eventBus, asciiMode = false }) {
19
20
 
20
21
  const formatTime = (ts) => ts ? new Date(ts).toTimeString().substring(0, 8) : 'none';
21
22
 
23
+ if (isCompact) {
24
+ return (
25
+ React.createElement(Box, { borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1, flexDirection: 'column' },
26
+ React.createElement(Text, null,
27
+ React.createElement(Text, { color: 'gray' }, 'MCP: '),
28
+ React.createElement(Text, { color: 'green' }, serverState),
29
+ React.createElement(Text, { color: 'gray' }, ' | Tools: '),
30
+ React.createElement(Text, { color: 'white' }, '' + toolCount)
31
+ ),
32
+ React.createElement(Text, null,
33
+ React.createElement(Text, { color: 'gray' }, 'Last: '),
34
+ React.createElement(Text, { color: 'white' }, formatTime(lastCall))
35
+ )
36
+ )
37
+ );
38
+ }
39
+
22
40
  return (
23
41
  React.createElement(Box, { borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
24
42
  React.createElement(Text, null,
@@ -2,7 +2,7 @@ const React = require('react');
2
2
  const { Box, Text } = require('ink');
3
3
  const { LEVEL_NAMES, getBorderStyle, getProgressBar } = require('../../tui/ui');
4
4
 
5
- function StatsRow({ flowmind, asciiMode = false }) {
5
+ function StatsRow({ flowmind, asciiMode = false, width = 0, compact = false }) {
6
6
  const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
7
7
  const [learningStats, setLearningStats] = React.useState({ totalRecords: 0, byType: {} });
8
8
  const [aiStatus, setAiStatus] = React.useState({ initialized: false, defaultProvider: 'none' });
@@ -19,10 +19,35 @@ function StatsRow({ flowmind, asciiMode = false }) {
19
19
  return () => clearInterval(interval);
20
20
  }, [flowmind]);
21
21
 
22
- const barWidth = 16;
22
+ const barWidth = compact ? Math.max(8, Math.floor(width / 10)) : 16;
23
23
  const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
24
24
  const progressBar = getProgressBar(barWidth, progress, asciiMode);
25
25
 
26
+ if (compact) {
27
+ return (
28
+ React.createElement(Box, { flexDirection: 'column' },
29
+ React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'yellow', paddingX: 1, width: '100%' },
30
+ React.createElement(Text, { bold: true, color: 'yellow' }, 'Honor'),
31
+ React.createElement(Text, { color: 'yellow' }, LEVEL_NAMES[honorData.level] || 'Egg'),
32
+ React.createElement(Text, { color: 'green' }, progressBar),
33
+ React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
34
+ ),
35
+ React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'cyan', paddingX: 1, width: '100%', marginTop: 1 },
36
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Learning'),
37
+ React.createElement(Text, { color: 'white' }, '' + (learningStats.totalRecords || 0) + ' records'),
38
+ React.createElement(Text, null,
39
+ React.createElement(Text, { color: 'gray' }, 'AI: '),
40
+ React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.initialized ? 'ok' : 'off')
41
+ )
42
+ ),
43
+ React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'blue', paddingX: 1, width: '100%', marginTop: 1 },
44
+ React.createElement(Text, { bold: true, color: 'blue' }, 'Components'),
45
+ React.createElement(Text, { color: 'gray' }, aiStatus.defaultProvider || 'none')
46
+ )
47
+ )
48
+ );
49
+ }
50
+
26
51
  return (
27
52
  React.createElement(Box, { flexDirection: 'row' },
28
53
  React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'yellow', paddingX: 1, width: '33%' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Memory and workflow automation for MCP, Codex, and Claude Code. Reuse repeatable developer operations through skills and explicit feedback.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "scripts": {
12
12
  "start": "node core/index.js",
13
13
  "mcp": "node mcp/server.js",
14
+ "check:update": "node scripts/check-update.js",
14
15
  "test": "jest",
15
16
  "test:coverage": "jest --coverage",
16
17
  "lint": "eslint .",
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ const chalk = require('chalk');
4
+ const packageJson = require('../package.json');
5
+ const {
6
+ getUpdateStatus
7
+ } = require('../core/update-notifier');
8
+
9
+ function printStatus(status) {
10
+ console.log(chalk.cyan(`\nCurrent version: ${status.currentVersion}`));
11
+ console.log(chalk.cyan(`Latest version: ${status.latestVersion}`));
12
+
13
+ if (status.comparison === 0) {
14
+ console.log(chalk.green('\n✓ You are already on the latest version!'));
15
+ return;
16
+ }
17
+
18
+ if (status.comparison > 0) {
19
+ console.log(chalk.green('\n✓ You are on a newer version than npm latest.'));
20
+ return;
21
+ }
22
+
23
+ console.log(chalk.yellow(`\n⬆ Update available: ${status.currentVersion} → ${status.latestVersion}`));
24
+ console.log(chalk.cyan('\nRun the following command to update:'));
25
+ console.log(chalk.white(` ${status.installCommand}`));
26
+ console.log(chalk.gray(' Or run `flowmind update` for the guided upgrade flow.'));
27
+ }
28
+
29
+ async function main() {
30
+ try {
31
+ const status = getUpdateStatus({
32
+ packageName: packageJson.name,
33
+ currentVersion: packageJson.version,
34
+ packageJsonPath: require.resolve('../package.json')
35
+ });
36
+
37
+ printStatus(status);
38
+ } catch (error) {
39
+ console.error(chalk.red('Failed to check for updates:'), error.message);
40
+ console.log(chalk.yellow('\nYou can manually update with:'));
41
+ console.log(chalk.white(` npm install -g ${packageJson.name}@latest`));
42
+ process.exitCode = 1;
43
+ }
44
+ }
45
+
46
+ if (require.main === module) {
47
+ main();
48
+ }
49
+
50
+ module.exports = {
51
+ main
52
+ };
@@ -669,34 +669,8 @@ async function executeDeploy(workflow, params, input, context) {
669
669
  };
670
670
  }
671
671
 
672
- const listParams = buildListPipelineParams(params);
673
- const lookup = await workflow.client.listPipelines(listParams);
674
- const batchParams = buildBatchRunParams(params);
675
- const execution = await workflow.client.startBatchPipelineRun(batchParams);
676
-
677
- return {
678
- type: 'result',
679
- skill: 'auto-flow',
680
- message: 'Workflow status: deployment submitted',
681
- data: {
682
- action: 'deploy',
683
- provider: workflow.provider,
684
- binding: workflow.binding,
685
- status: 'submitted',
686
- mcpServer: getWorkflowMcpServer(workflow),
687
- execution: summarizeDeployExecution(params, execution, {
688
- source: 'batch-fallback',
689
- lookup
690
- }),
691
- resolution: {
692
- source: 'batch-fallback',
693
- lookup
694
- }
695
- },
696
- input,
697
- timestamp: new Date().toISOString()
698
- };
699
- }
672
+ return buildUnresolvedDeployResult(workflow, params, input, context);
673
+ }
700
674
 
701
675
  async function executeStatus(workflow, params, input) {
702
676
  let execution;
@@ -894,7 +868,7 @@ function resolveLocalPipelineForService(serviceName, environment, context) {
894
868
  const entry = pipelineMap[serviceName];
895
869
  if (!entry) return null;
896
870
 
897
- const pipeline = selectPipelineFromEntry(entry, environment);
871
+ const pipeline = selectPipelineFromEntry(entry, environment, serviceName);
898
872
  if (!pipeline?.pipelineId) return null;
899
873
 
900
874
  return {
@@ -923,19 +897,13 @@ function readLocalPipelineMap(context) {
923
897
  return null;
924
898
  }
925
899
 
926
- function selectPipelineFromEntry(entry, environment) {
900
+ function selectPipelineFromEntry(entry, environment, serviceName) {
927
901
  const preferredKeys = getEnvLookupOrder(environment);
928
902
  for (const key of preferredKeys) {
929
903
  const candidate = normalizePipelineCollection(entry?.[key]);
930
- if (candidate.length > 0) {
931
- return candidate[0];
932
- }
933
- }
934
-
935
- for (const value of Object.values(entry || {})) {
936
- const candidate = normalizePipelineCollection(value);
937
- if (candidate.length > 0) {
938
- return candidate[0];
904
+ const matched = candidate.find((item) => matchesPipelineName(item.pipelineName, environment, serviceName));
905
+ if (matched) {
906
+ return matched;
939
907
  }
940
908
  }
941
909
 
@@ -1094,49 +1062,12 @@ function matchesRemoteService(fields, serviceName, environment) {
1094
1062
  }
1095
1063
 
1096
1064
  function selectPipelineFromFields(fields, environment, serviceName) {
1097
- const indexedCandidate = selectIndexedPipelineForService(fields, environment, serviceName);
1098
- if (indexedCandidate?.pipelineId) {
1099
- return indexedCandidate;
1100
- }
1101
-
1102
1065
  const candidates = collectFieldPipelineDescriptors(fields, environment);
1103
- if (candidates.length > 0) {
1104
- return candidates[0];
1105
- }
1106
-
1107
- const pipelineIds = String(fields.pipelineId || '')
1108
- .split(',')
1109
- .map((item) => item.trim())
1110
- .filter(Boolean);
1111
-
1112
- if (pipelineIds.length === 0) {
1113
- return null;
1114
- }
1115
-
1116
- return {
1117
- pipelineName: buildPrefixedPipelineName(
1118
- Array.isArray(fields.service) ? fields.service[0] : null,
1119
- environment
1120
- ),
1121
- pipelineId: pipelineIds[0]
1122
- };
1123
- }
1124
-
1125
- function selectIndexedPipelineForService(fields, environment, serviceName) {
1126
- if (!serviceName) return null;
1127
-
1128
- const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
1129
- const serviceIndex = services.indexOf(serviceName);
1130
- if (serviceIndex < 0) return null;
1131
-
1132
- for (const key of getFieldKeysForEnvironment(environment)) {
1133
- const values = Array.isArray(fields[key]) ? fields[key] : [];
1134
- const parsed = parsePipelineDescriptor(values[serviceIndex]);
1135
- if (parsed?.pipelineId) {
1136
- return parsed;
1066
+ for (const candidate of candidates) {
1067
+ if (candidate?.pipelineId && matchesPipelineName(candidate.pipelineName, environment, serviceName)) {
1068
+ return candidate;
1137
1069
  }
1138
1070
  }
1139
-
1140
1071
  return null;
1141
1072
  }
1142
1073
 
@@ -1177,10 +1108,11 @@ function parsePipelineDescriptor(value) {
1177
1108
  function getEnvLookupOrder(environment) {
1178
1109
  const normalized = normalizeEnvironment(environment || '');
1179
1110
  if (normalized === 'prod') return ['prod'];
1180
- if (normalized === 'gray') return ['gray', 'prod'];
1111
+ if (normalized === 'gray') return ['gray'];
1181
1112
  if (normalized === 'uat') return ['uat'];
1182
- if (normalized === 'test') return ['test', 'dev', 'uat'];
1183
- return ['uat', 'test', 'dev', 'gray', 'prod'];
1113
+ if (normalized === 'test') return ['test', 'dev'];
1114
+ if (normalized === 'dev') return ['dev'];
1115
+ return [];
1184
1116
  }
1185
1117
 
1186
1118
  function buildPrefixedPipelineName(serviceName, environment) {
@@ -1191,6 +1123,24 @@ function buildPrefixedPipelineName(serviceName, environment) {
1191
1123
  return `${prefix}-${serviceName}`;
1192
1124
  }
1193
1125
 
1126
+ function matchesPipelineName(pipelineName, environment, serviceName) {
1127
+ if (!pipelineName || !serviceName) return false;
1128
+
1129
+ const normalized = String(pipelineName).toLowerCase();
1130
+ const candidatePrefixes = getAllowedPipelinePrefixes(environment);
1131
+ return candidatePrefixes.some((prefix) => normalized === `${prefix}-${serviceName}`.toLowerCase());
1132
+ }
1133
+
1134
+ function getAllowedPipelinePrefixes(environment) {
1135
+ const normalized = normalizeEnvironment(environment || '');
1136
+ if (normalized === 'test') return ['test', 'dev'];
1137
+ if (normalized === 'dev') return ['dev'];
1138
+ if (normalized === 'uat') return ['uat'];
1139
+ if (normalized === 'gray') return ['gray'];
1140
+ if (normalized === 'prod') return ['prod'];
1141
+ return [];
1142
+ }
1143
+
1194
1144
  function inferEnvironmentFromPipelineName(pipelineName) {
1195
1145
  const match = String(pipelineName || '').match(/^(test|dev|uat|gray|prod)-/i);
1196
1146
  return match ? (ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1])) : null;
@@ -1219,6 +1169,32 @@ function collectNestedValues(payload, values = []) {
1219
1169
  return values;
1220
1170
  }
1221
1171
 
1172
+ function buildUnresolvedDeployResult(workflow, params, input, context) {
1173
+ const target = buildWorkflowTargetLabel(params) || buildDeployTargetLabel(context) || 'deployment';
1174
+ return {
1175
+ type: 'error',
1176
+ success: false,
1177
+ skill: 'auto-flow',
1178
+ message: `Unable to resolve deployment target for ${target}. Specify an exact environment or pipelineId.`,
1179
+ data: {
1180
+ action: 'deploy',
1181
+ provider: workflow.provider,
1182
+ binding: workflow.binding,
1183
+ status: 'not_resolved',
1184
+ mcpServer: getWorkflowMcpServer(workflow),
1185
+ target,
1186
+ environment: params.environment || null,
1187
+ resolution: {
1188
+ source: 'not-resolved',
1189
+ serviceNames: params.serviceNames || [],
1190
+ environment: params.environment || null
1191
+ }
1192
+ },
1193
+ input,
1194
+ timestamp: new Date().toISOString()
1195
+ };
1196
+ }
1197
+
1222
1198
  function compactObject(value) {
1223
1199
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
1224
1200
  }