agileflow 2.90.6 → 2.91.0
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/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/codebase-indexer.js +810 -0
- package/lib/validate-names.js +3 -3
- package/package.json +4 -1
- package/scripts/obtain-context.js +238 -0
- package/scripts/precompact-context.sh +13 -1
- package/scripts/query-codebase.js +430 -0
- package/scripts/tui/blessed/data/watcher.js +175 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +95 -0
- package/scripts/tui/blessed/panels/sessions.js +143 -0
- package/scripts/tui/blessed/panels/trace.js +91 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +51 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -32
- package/scripts/tui/simple-tui.js +8 -5
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +212 -0
- package/scripts/validators/json-schema-validator.js +179 -0
- package/scripts/validators/markdown-validator.js +153 -0
- package/scripts/validators/migration-validator.js +117 -0
- package/scripts/validators/security-validator.js +276 -0
- package/scripts/validators/story-format-validator.js +176 -0
- package/scripts/validators/test-result-validator.js +99 -0
- package/scripts/validators/workflow-validator.js +240 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +237 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/epic.md +92 -1
- package/src/core/commands/help.md +1 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/status.md +126 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/tools/cli/commands/tui.js +40 -271
package/lib/validate-names.js
CHANGED
|
@@ -29,9 +29,9 @@ const PATTERNS = {
|
|
|
29
29
|
// Examples: default, my-profile, dev_config
|
|
30
30
|
profileName: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
|
31
31
|
|
|
32
|
-
// Command name: alphanumeric with hyphens/colons, starts with letter
|
|
33
|
-
// Examples: babysit, story:list, agileflow:configure
|
|
34
|
-
commandName: /^[a-zA-Z][a-zA-Z0-9
|
|
32
|
+
// Command name: alphanumeric with hyphens/colons/slashes, starts with letter
|
|
33
|
+
// Examples: babysit, story:list, agileflow:configure, research/ask
|
|
34
|
+
commandName: /^[a-zA-Z][a-zA-Z0-9:/-]*$/,
|
|
35
35
|
|
|
36
36
|
// Session nickname: alphanumeric with hyphens/underscores
|
|
37
37
|
// Examples: auth-work, feature_1, main
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agileflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.91.0",
|
|
4
4
|
"description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agile",
|
|
@@ -53,7 +53,10 @@
|
|
|
53
53
|
"test:coverage": "jest --coverage"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"blessed": "^0.1.81",
|
|
57
|
+
"blessed-contrib": "^4.11.0",
|
|
56
58
|
"chalk": "^4.1.2",
|
|
59
|
+
"chokidar": "^4.0.3",
|
|
57
60
|
"commander": "^12.1.0",
|
|
58
61
|
"fs-extra": "^11.2.0",
|
|
59
62
|
"ink": "^3.2.0",
|
|
@@ -21,14 +21,21 @@
|
|
|
21
21
|
* - File overlaps: Only load if parallel sessions are active
|
|
22
22
|
* - Configurable via features.lazyContext in agileflow-metadata.json
|
|
23
23
|
*
|
|
24
|
+
* QUERY MODE (US-0127):
|
|
25
|
+
* - When QUERY=<pattern> provided, uses codebase index for targeted search
|
|
26
|
+
* - Falls back to full context if query returns empty
|
|
27
|
+
* - Based on RLM pattern: programmatic search instead of loading everything
|
|
28
|
+
*
|
|
24
29
|
* Usage:
|
|
25
30
|
* node scripts/obtain-context.js # Just gather context
|
|
26
31
|
* node scripts/obtain-context.js babysit # Gather + register 'babysit'
|
|
32
|
+
* node scripts/obtain-context.js babysit QUERY="auth files" # Query mode
|
|
27
33
|
*/
|
|
28
34
|
|
|
29
35
|
const fs = require('fs');
|
|
30
36
|
const fsPromises = require('fs').promises;
|
|
31
37
|
const path = require('path');
|
|
38
|
+
const os = require('os');
|
|
32
39
|
const { execSync } = require('child_process');
|
|
33
40
|
const { c: C, box } = require('../lib/colors');
|
|
34
41
|
const { isValidCommandName } = require('../lib/validate');
|
|
@@ -77,6 +84,11 @@ function parseCommandArgs(args) {
|
|
|
77
84
|
activeSections.push('visual-e2e');
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
// Query mode: QUERY=<pattern> triggers targeted codebase search (US-0127)
|
|
88
|
+
if (params.QUERY) {
|
|
89
|
+
activeSections.push('query-mode');
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
// Check for multi-session environment
|
|
81
93
|
const registryPath = '.agileflow/sessions/registry.json';
|
|
82
94
|
if (fs.existsSync(registryPath)) {
|
|
@@ -99,6 +111,40 @@ const commandName = process.argv[2];
|
|
|
99
111
|
const commandArgs = process.argv.slice(3);
|
|
100
112
|
const { activeSections, params: commandParams } = parseCommandArgs(commandArgs);
|
|
101
113
|
|
|
114
|
+
// Helper to extract command type from frontmatter
|
|
115
|
+
function getCommandType(cmdName) {
|
|
116
|
+
// Handle nested command paths like "research/ask" -> "research/ask.md"
|
|
117
|
+
// The command name may contain "/" for nested commands
|
|
118
|
+
const cmdPath = cmdName.includes('/')
|
|
119
|
+
? `${cmdName.substring(0, cmdName.lastIndexOf('/'))}/${cmdName.substring(cmdName.lastIndexOf('/') + 1)}.md`
|
|
120
|
+
: `${cmdName}.md`;
|
|
121
|
+
|
|
122
|
+
// Try to find the command file and read its frontmatter type
|
|
123
|
+
const possiblePaths = [
|
|
124
|
+
`packages/cli/src/core/commands/${cmdPath}`,
|
|
125
|
+
`.agileflow/commands/${cmdPath}`,
|
|
126
|
+
`.claude/commands/agileflow/${cmdPath}`,
|
|
127
|
+
// Also try flat path for legacy commands
|
|
128
|
+
`packages/cli/src/core/commands/${cmdName.replace(/\//g, '-')}.md`,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const searchPath of possiblePaths) {
|
|
132
|
+
if (fs.existsSync(searchPath)) {
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(searchPath, 'utf8');
|
|
135
|
+
// Extract type from YAML frontmatter
|
|
136
|
+
const match = content.match(/^---\n[\s\S]*?type:\s*(\S+)/m);
|
|
137
|
+
if (match) {
|
|
138
|
+
return match[1].replace(/['"]/g, ''); // Remove quotes if any
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Continue to next path
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return 'interactive'; // Default to interactive
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
// Register command for PreCompact context preservation
|
|
103
149
|
if (commandName && isValidCommandName(commandName)) {
|
|
104
150
|
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
@@ -114,9 +160,13 @@ if (commandName && isValidCommandName(commandName)) {
|
|
|
114
160
|
// Remove any existing entry for this command (avoid duplicates)
|
|
115
161
|
state.active_commands = state.active_commands.filter(c => c.name !== commandName);
|
|
116
162
|
|
|
163
|
+
// Get command type from frontmatter (output-only vs interactive)
|
|
164
|
+
const commandType = getCommandType(commandName);
|
|
165
|
+
|
|
117
166
|
// Add the new command with active sections for progressive disclosure
|
|
118
167
|
state.active_commands.push({
|
|
119
168
|
name: commandName,
|
|
169
|
+
type: commandType, // Used by PreCompact to skip output-only commands
|
|
120
170
|
activated_at: new Date().toISOString(),
|
|
121
171
|
state: {},
|
|
122
172
|
active_sections: activeSections,
|
|
@@ -165,6 +215,121 @@ function safeExec(cmd) {
|
|
|
165
215
|
}
|
|
166
216
|
}
|
|
167
217
|
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Context Budget Tracking (GSD Integration)
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get current context usage percentage from Claude's session files.
|
|
224
|
+
* Reads token counts from the active session JSONL file.
|
|
225
|
+
*
|
|
226
|
+
* @returns {{ percent: number, tokens: number, max: number } | null}
|
|
227
|
+
*/
|
|
228
|
+
function getContextPercentage() {
|
|
229
|
+
try {
|
|
230
|
+
const homeDir = os.homedir();
|
|
231
|
+
const cwd = process.cwd();
|
|
232
|
+
|
|
233
|
+
// Convert current dir to Claude's session file path format
|
|
234
|
+
// e.g., /home/coder/AgileFlow -> home-coder-AgileFlow
|
|
235
|
+
const projectDir = cwd.replace(homeDir, '~').replace('~', homeDir).replace(/\//g, '-').replace(/^-/, '');
|
|
236
|
+
const sessionDir = path.join(homeDir, '.claude', 'projects', `-${projectDir}`);
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(sessionDir)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Find most recent .jsonl session file
|
|
243
|
+
const files = fs.readdirSync(sessionDir)
|
|
244
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
245
|
+
.map(f => ({
|
|
246
|
+
name: f,
|
|
247
|
+
mtime: fs.statSync(path.join(sessionDir, f)).mtime.getTime(),
|
|
248
|
+
}))
|
|
249
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
250
|
+
|
|
251
|
+
if (files.length === 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const sessionFile = path.join(sessionDir, files[0].name);
|
|
256
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
257
|
+
const lines = content.trim().split('\n').slice(-20); // Last 20 lines
|
|
258
|
+
|
|
259
|
+
// Find latest usage entry
|
|
260
|
+
let latestTokens = 0;
|
|
261
|
+
for (const line of lines.reverse()) {
|
|
262
|
+
try {
|
|
263
|
+
const entry = JSON.parse(line);
|
|
264
|
+
if (entry?.message?.usage) {
|
|
265
|
+
const usage = entry.message.usage;
|
|
266
|
+
latestTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
267
|
+
if (latestTokens > 0) break;
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Skip malformed lines
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (latestTokens === 0) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Default to 200K context for modern Claude models
|
|
279
|
+
const maxContext = 200000;
|
|
280
|
+
const percent = Math.min(100, Math.round((latestTokens * 100) / maxContext));
|
|
281
|
+
|
|
282
|
+
return { percent, tokens: latestTokens, max: maxContext };
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Generate context budget warning box if usage exceeds threshold.
|
|
290
|
+
* Based on GSD research: 50% is where quality starts degrading.
|
|
291
|
+
*
|
|
292
|
+
* @param {number} percent - Context usage percentage
|
|
293
|
+
* @returns {string} Warning box or empty string
|
|
294
|
+
*/
|
|
295
|
+
function generateContextWarning(percent) {
|
|
296
|
+
if (percent < 50) {
|
|
297
|
+
return ''; // No warning needed
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const width = 60;
|
|
301
|
+
const topLine = `┏${'━'.repeat(width - 2)}┓`;
|
|
302
|
+
const bottomLine = `┗${'━'.repeat(width - 2)}┛`;
|
|
303
|
+
|
|
304
|
+
let color, icon, message, suggestion;
|
|
305
|
+
|
|
306
|
+
if (percent >= 70) {
|
|
307
|
+
// Critical: Dumb Zone
|
|
308
|
+
color = C.coral;
|
|
309
|
+
icon = '🔴';
|
|
310
|
+
message = `Context usage: ${percent}% (in degradation zone)`;
|
|
311
|
+
suggestion = 'Strongly recommend: compact conversation or delegate to sub-agent';
|
|
312
|
+
} else {
|
|
313
|
+
// Warning: Approaching limit (50-69%)
|
|
314
|
+
color = C.amber;
|
|
315
|
+
icon = '⚠️';
|
|
316
|
+
message = `Context usage: ${percent}% (approaching 50% threshold)`;
|
|
317
|
+
suggestion = 'Consider: delegate to sub-agent or compact conversation';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Pad messages to fit width
|
|
321
|
+
const msgPadded = ` ${icon} ${message}`.padEnd(width - 3) + '┃';
|
|
322
|
+
const sugPadded = ` → ${suggestion}`.padEnd(width - 3) + '┃';
|
|
323
|
+
|
|
324
|
+
return [
|
|
325
|
+
`${color}${C.bold}${topLine}${C.reset}`,
|
|
326
|
+
`${color}${C.bold}┃${msgPadded}${C.reset}`,
|
|
327
|
+
`${color}${C.bold}┃${sugPadded}${C.reset}`,
|
|
328
|
+
`${color}${C.bold}${bottomLine}${C.reset}`,
|
|
329
|
+
'',
|
|
330
|
+
].join('\n');
|
|
331
|
+
}
|
|
332
|
+
|
|
168
333
|
// =============================================================================
|
|
169
334
|
// Lazy Evaluation Configuration (US-0093)
|
|
170
335
|
// =============================================================================
|
|
@@ -679,6 +844,13 @@ function generateFullContent(prefetched = null) {
|
|
|
679
844
|
content += `${C.dim}Balance: Use at natural pause points. Don't ask permission for routine work.${C.reset}\n\n`;
|
|
680
845
|
}
|
|
681
846
|
|
|
847
|
+
// 0.6 CONTEXT BUDGET WARNING (GSD Integration)
|
|
848
|
+
// Show warning when context usage approaches 50% threshold
|
|
849
|
+
const contextUsage = getContextPercentage();
|
|
850
|
+
if (contextUsage && contextUsage.percent >= 50) {
|
|
851
|
+
content += generateContextWarning(contextUsage.percent);
|
|
852
|
+
}
|
|
853
|
+
|
|
682
854
|
// 0. PROGRESSIVE DISCLOSURE (section activation)
|
|
683
855
|
if (activeSections.length > 0) {
|
|
684
856
|
content += `\n${C.cyan}${C.bold}═══ 📖 Progressive Disclosure: Active Sections ═══${C.reset}\n`;
|
|
@@ -1081,6 +1253,54 @@ function generateFullContent(prefetched = null) {
|
|
|
1081
1253
|
return content;
|
|
1082
1254
|
}
|
|
1083
1255
|
|
|
1256
|
+
// ============================================
|
|
1257
|
+
// QUERY MODE: Targeted codebase search (US-0127)
|
|
1258
|
+
// ============================================
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Execute query mode using codebase index for targeted search.
|
|
1262
|
+
* Falls back to full context if query returns no results.
|
|
1263
|
+
*
|
|
1264
|
+
* @param {string} query - Natural language or pattern query
|
|
1265
|
+
* @returns {Object|null} Query results or null to fall back to full context
|
|
1266
|
+
*/
|
|
1267
|
+
function executeQueryMode(query) {
|
|
1268
|
+
const queryScript = path.join(__dirname, 'query-codebase.js');
|
|
1269
|
+
|
|
1270
|
+
// Check if query script exists
|
|
1271
|
+
if (!fs.existsSync(queryScript)) {
|
|
1272
|
+
console.error('Query mode unavailable: query-codebase.js not found');
|
|
1273
|
+
return null; // Fall back to full context
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
try {
|
|
1277
|
+
// Execute query and capture output
|
|
1278
|
+
const result = execSync(`node "${queryScript}" --query="${query}" --budget=15000`, {
|
|
1279
|
+
encoding: 'utf8',
|
|
1280
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// Check if we got results
|
|
1284
|
+
if (result.includes('No files found') || result.trim() === '') {
|
|
1285
|
+
return null; // Fall back to full context
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
mode: 'query',
|
|
1290
|
+
query: query,
|
|
1291
|
+
results: result.trim(),
|
|
1292
|
+
};
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
// Exit code 2 = no results, fall back to full context
|
|
1295
|
+
if (err.status === 2) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
// Exit code 1 = error, report but fall back
|
|
1299
|
+
console.error(`Query error: ${err.message}`);
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1084
1304
|
// ============================================
|
|
1085
1305
|
// MAIN: Output with smart summary positioning
|
|
1086
1306
|
// ============================================
|
|
@@ -1089,6 +1309,24 @@ function generateFullContent(prefetched = null) {
|
|
|
1089
1309
|
* Main execution function using parallel pre-fetching for optimal performance.
|
|
1090
1310
|
*/
|
|
1091
1311
|
async function main() {
|
|
1312
|
+
// Check for query mode first (US-0127)
|
|
1313
|
+
if (activeSections.includes('query-mode') && commandParams.QUERY) {
|
|
1314
|
+
const queryResult = executeQueryMode(commandParams.QUERY);
|
|
1315
|
+
|
|
1316
|
+
if (queryResult) {
|
|
1317
|
+
// Output query results instead of full context
|
|
1318
|
+
console.log(`=== QUERY MODE ===`);
|
|
1319
|
+
console.log(`Query: "${queryResult.query}"`);
|
|
1320
|
+
console.log(`---`);
|
|
1321
|
+
console.log(queryResult.results);
|
|
1322
|
+
console.log(`---`);
|
|
1323
|
+
console.log(`[Query mode: targeted search. Run without QUERY= for full context]`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
// Fall through to full context if query returned no results
|
|
1327
|
+
console.log(`[Query "${commandParams.QUERY}" returned no results, loading full context...]`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1092
1330
|
// Check for multi-session environment before prefetching
|
|
1093
1331
|
const registryPath = '.agileflow/sessions/registry.json';
|
|
1094
1332
|
let isMultiSession = false;
|
|
@@ -42,11 +42,23 @@ if [ -d "docs/05-epics" ]; then
|
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
# Detect active commands and extract their Compact Summaries
|
|
45
|
+
# IMPORTANT: Skip "output-only" commands like research:ask, research:list, research:view
|
|
46
|
+
# These commands generate output for the user to copy - they're not ongoing tasks.
|
|
47
|
+
# If we include them in PreCompact, Claude will try to re-execute them after compact.
|
|
45
48
|
COMMAND_SUMMARIES=""
|
|
46
49
|
if [ -f "docs/09-agents/session-state.json" ]; then
|
|
50
|
+
# Output-only commands that should NOT be preserved during compact
|
|
51
|
+
# These commands generate output once and are done - no ongoing state
|
|
52
|
+
# Note: Commands with type: output-only in frontmatter are also filtered
|
|
53
|
+
OUTPUT_ONLY_COMMANDS="research/ask research/list research/view help metrics board"
|
|
54
|
+
|
|
47
55
|
ACTIVE_COMMANDS=$(node -p "
|
|
48
56
|
const s = require('./docs/09-agents/session-state.json');
|
|
49
|
-
|
|
57
|
+
const outputOnly = '$OUTPUT_ONLY_COMMANDS'.split(' ');
|
|
58
|
+
(s.active_commands || [])
|
|
59
|
+
.filter(c => !outputOnly.includes(c.name) && c.type !== 'output-only')
|
|
60
|
+
.map(c => c.name)
|
|
61
|
+
.join(' ');
|
|
50
62
|
" 2>/dev/null || echo "")
|
|
51
63
|
|
|
52
64
|
for ACTIVE_COMMAND in $ACTIVE_COMMANDS; do
|