Skip to content

Commit 45c769e

Browse files
committed
feat: Specific Rule Targeting for Logs resolve #67
1 parent c8043fe commit 45c769e

File tree

6 files changed

+132
-29
lines changed

6 files changed

+132
-29
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ temp
77
browser
88
coverage
99
.DS_store
10-
.windsurf
10+
.windsurf
11+
CLAUDE.md
12+
.claude

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
A command-line tool for managing YouTrack workflows with seamless local development experience.
44

5+
## [1.5.0] - 2026-01-12
6+
7+
### New Features
8+
9+
#### Specific Rule Targeting for Logs
10+
- **Direct rule targeting**: Added support for specifying specific rules directly using `workflow/rule` syntax in `ytw logs`
11+
- Example: `ytw logs my-workflow/my-rule` fetches logs for a specific rule without prompts
12+
- Supports multiple specific rules: `ytw logs workflow1/rule1 workflow2/rule2`
13+
- Can be mixed with workflow-only targets: `ytw logs workflow1/rule1 workflow2`
14+
- Works with all existing options (`--watch`, `--top`, `--all`)
15+
516
## [1.4.0] - 2025-08-29
617

718
### Improvements

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,16 +267,42 @@ module.exports = [
267267
### Logs
268268
269269
```bash
270-
ytw logs [workflow-name...] [--watch [ms]] [--all] [--top <number>]
270+
ytw logs [targets...] [--watch [ms]] [--all] [--top <number>]
271271
```
272272
273-
View logs for a selected workflow rules. This helps you monitor the behavior of your workflows in real-time.
273+
View logs for workflow rules. This helps you monitor the behavior of your workflows in real-time.
274274
275-
Options:
275+
**Arguments:**
276+
- `[targets...]` - Workflow names or workflow/rule paths. Supports two formats:
277+
- `workflow-name` - Target a workflow (will prompt for rule selection or use `--all`)
278+
- `workflow-name/rule-name` - Target a specific rule directly (no prompt)
279+
280+
**Options:**
276281
- `-w, --watch [ms]` - watch for new logs in YouTrack in real-time with interval in milliseconds, default: 5000ms.
277282
- `-a, --all` - fetch all logs for rules of all workflows in project
278283
- `-t, --top [number]` - number of logs to fetch per rule, default: 10
279-
- If workflow names are provided and `--all` is not specified, it will prompt you to select rules from provided workflows.
284+
285+
**Examples:**
286+
287+
```bash
288+
# View logs for a specific rule (no prompt)
289+
ytw logs my-workflow/my-rule
290+
291+
# View logs for multiple specific rules
292+
ytw logs workflow1/rule1 workflow2/rule2
293+
294+
# View logs for a workflow (prompts for rule selection)
295+
ytw logs my-workflow
296+
297+
# View all logs for a workflow
298+
ytw logs my-workflow --all
299+
300+
# Watch logs for a specific rule
301+
ytw logs my-workflow/my-rule --watch
302+
303+
# Mixed: specific rule + workflow selection
304+
ytw logs workflow1/rule1 workflow2
305+
```
280306
281307
### Types
282308

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "youtrack-workflow-cli",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "Youtrack workflow CLI",
55
"repository": {
66
"type": "git",

src/commands/logs.command.ts

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ const LOG_LEVEL_COLORS: Record<string, string> = {
1515
DEBUG: COLORS.FG.GREEN,
1616
}
1717

18+
type ParsedLogTarget = { type: "workflow"; name: string } | { type: "rule"; workflowName: string; ruleName: string }
19+
20+
/**
21+
* Parse a log target argument into either a workflow or workflow/rule target
22+
* @param arg The argument to parse (e.g., "my-workflow" or "my-workflow/my-rule")
23+
* @returns Parsed target object
24+
*/
25+
const parseLogTarget = (arg: string): ParsedLogTarget => {
26+
const slashIndex = arg.indexOf("/")
27+
if (slashIndex > 0 && slashIndex < arg.length - 1) {
28+
return {
29+
type: "rule",
30+
workflowName: arg.substring(0, slashIndex),
31+
ruleName: arg.substring(slashIndex + 1),
32+
}
33+
}
34+
return { type: "workflow", name: arg }
35+
}
36+
1837
/**
1938
* Print logs for a workflow rule
2039
* @param workflowName Name of the workflow
@@ -47,7 +66,7 @@ const printLogs = (workflowName: string, ruleName: string, logs: RuleLog[]) => {
4766
* Logs command implementation
4867
*/
4968
export const logsCommand = async (
50-
workflowNames: string[],
69+
targets: string[],
5170
{ host, token, top, watch, all }: { host: string; token: string; top: number; watch?: number; all: boolean },
5271
): Promise<void> => {
5372
const youtrackService = new YoutrackService(host, token)
@@ -66,19 +85,50 @@ export const logsCommand = async (
6685

6786
const workflows = serverWorkflows.filter((w) => isManifestExists(w.name))
6887

69-
if (workflowNames.length === 0) {
70-
// Validate specified workflows
71-
const invalidWorkflows = workflowNames.filter((name) => !workflows.some((w) => w.name === name))
88+
// Parse targets into workflow-only and rule-specific targets
89+
const parsedTargets = targets.map(parseLogTarget)
90+
const workflowTargets = parsedTargets.filter((t): t is { type: "workflow"; name: string } => t.type === "workflow")
91+
const ruleTargets = parsedTargets.filter(
92+
(t): t is { type: "rule"; workflowName: string; ruleName: string } => t.type === "rule",
93+
)
94+
95+
// Resolve rule-specific targets directly
96+
const directRules: WorkflowRule[] = []
97+
for (const target of ruleTargets) {
98+
const workflow = workflows.find((w) => w.name === target.workflowName)
99+
if (!workflow) {
100+
spinner.fail(`Workflow '${target.workflowName}' not found`)
101+
return
102+
}
103+
const rule = workflow.rules.find((r) => r.name === target.ruleName)
104+
if (!rule) {
105+
spinner.fail(`Rule '${target.ruleName}' not found in workflow '${target.workflowName}'`)
106+
return
107+
}
108+
directRules.push({
109+
workflowId: workflow.id,
110+
ruleId: rule.id,
111+
workflowName: workflow.name,
112+
ruleName: rule.name,
113+
})
114+
}
115+
116+
// Handle workflow-only targets
117+
const workflowNames = workflowTargets.map((t) => t.name)
72118

119+
// Validate specified workflows
120+
if (workflowNames.length > 0) {
121+
const invalidWorkflows = workflowNames.filter((name) => !workflows.some((w) => w.name === name))
73122
if (invalidWorkflows.length > 0) {
74123
spinner.fail(`Invalid workflow names: ${invalidWorkflows.join(", ")}`)
75124
return
76125
}
77126
}
78127

79-
// If no workflows specified, show selection menu
80-
const workflowsToProcess = workflowNames.length ? workflows.filter((w) => workflowNames.includes(w.name)) : workflows
81-
const workflowRules = workflowsToProcess.reduce(
128+
// Build workflow rules for workflow-only targets (used for --all or prompt)
129+
const workflowsToProcess =
130+
workflowNames.length > 0 ? workflows.filter((w) => workflowNames.includes(w.name)) : workflows
131+
const workflowRulesForPrompt = workflowsToProcess.reduce(
82132
(res, workflow) => {
83133
res.push(
84134
...workflow.rules.map((r) => ({
@@ -96,27 +146,41 @@ export const logsCommand = async (
96146
[] as { name: string; value: WorkflowRule }[],
97147
)
98148

99-
if (workflowRules.length === 0) {
149+
// If we have direct rules and no workflow targets, skip prompt/all logic
150+
const hasWorkflowTargets = workflowNames.length > 0 || targets.length === 0
151+
const needsSelection = hasWorkflowTargets && !all && workflowRulesForPrompt.length > 0
152+
153+
if (workflowRulesForPrompt.length === 0 && directRules.length === 0) {
100154
spinner.fail("No workflows found. Add workflows first.")
101155
return
102156
}
103157

104158
spinner.stop()
105159

106-
const selectedRules: WorkflowRule[] = []
107-
if (all) {
108-
selectedRules.push(...workflowRules.map(({ value }) => value))
109-
} else {
110-
const { selectedRules: selectedRulesFromPrompt } = await inquirer.prompt<{ selectedRules: WorkflowRule[] }>([
111-
{
112-
type: "checkbox",
113-
name: "selectedRules",
114-
message: "Select rules to view logs for:",
115-
choices: workflowRules,
116-
validate: (input) => (input.length > 0 ? true : "Please select at least one rule"),
117-
},
118-
])
119-
selectedRules.push(...selectedRulesFromPrompt)
160+
const selectedRules: WorkflowRule[] = [...directRules]
161+
162+
if (hasWorkflowTargets) {
163+
if (all) {
164+
// Add all rules from workflow targets
165+
selectedRules.push(...workflowRulesForPrompt.map(({ value }) => value))
166+
} else if (needsSelection) {
167+
// Prompt for selection from workflow targets
168+
const { selectedRules: selectedRulesFromPrompt } = await inquirer.prompt<{ selectedRules: WorkflowRule[] }>([
169+
{
170+
type: "checkbox",
171+
name: "selectedRules",
172+
message: "Select rules to view logs for:",
173+
choices: workflowRulesForPrompt,
174+
validate: (input) => (input.length > 0 || directRules.length > 0 ? true : "Please select at least one rule"),
175+
},
176+
])
177+
selectedRules.push(...selectedRulesFromPrompt)
178+
}
179+
}
180+
181+
if (selectedRules.length === 0) {
182+
spinner.fail("No rules selected")
183+
return
120184
}
121185

122186
spinner.start("Fetching logs...")

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ program
123123
program
124124
.command("logs")
125125
.description("Fetch and display workflow logs")
126-
.argument("[workflows...]", "Workflow names to fetch logs for")
126+
.argument("[targets...]", "Workflow names or workflow/rule paths (e.g., my-workflow/my-rule)")
127127
.option("-t, --top <number>", "Number of logs to fetch per rule", Number.parseInt, 10)
128128
.option("-w, --watch [ms]", "Watch for new logs", Number.parseInt)
129129
.option("-a, --all", "Fetch all logs for rules of all workflows in project")

0 commit comments

Comments
 (0)