Files
runner/.opencode/plans/dap-step-manipulation.md
Francesco Renzi d334ab3f0a simplify
2026-01-22 00:12:27 +00:00

1267 lines
37 KiB
Markdown

# Dynamic Step Manipulation & Workflow Export
**Status:** Draft
**Author:** GitHub Actions Team
**Date:** January 2026
**Prerequisites:** dap-debugging.md, dap-step-backwards.md (completed)
## Progress Checklist
- [x] **Chunk 1:** Command Parser & Infrastructure
- [x] **Chunk 2:** Step Serializer (ActionStep → YAML)
- [x] **Chunk 3:** Step Factory (Create new steps)
- [x] **Chunk 4:** Step Manipulator (Queue operations)
- [x] **Chunk 5:** REPL Commands (steps list, steps add run, steps edit, steps remove, steps move)
- [x] **Chunk 6:** Action Download Integration (steps add uses)
- [x] **Chunk 7:** Export Command (steps export)
- [x] **Chunk 8:** Output Format Flag (--output text|json for programmatic use)
- [x] **Chunk 9:** Browser Extension UI
## Overview
This plan extends the DAP debugger with the ability to dynamically manipulate job steps during a debug session: add new steps, edit pending steps, remove steps, and reorder them. At the end of a session, users can export the modified steps as YAML to paste into their workflow file.
This transforms the debugger from a "read-only inspection tool" into an **interactive workflow editor** — the key differentiator of this prototype.
## Goals
- **Primary:** Enable add/edit/move/delete of job steps during debug session
- **Primary:** Support both `run` and `uses` step types
- **Primary:** Export modified steps as valid YAML
- **Secondary:** Provide `--output` flag for text/JSON response format
- **Non-goal:** Full workflow file reconstruction (steps section only)
- **Non-goal:** Production action restriction enforcement (noted for later)
## Command API Specification
### Grammar
```
steps <command> [target] [options] [--output text|json]
```
### Output Format
All commands support the `--output` flag to control response format:
- `--output text` (default) - Human-readable text output
- `--output json` - JSON output for programmatic use
- Short form: `-o json`, `-o text`
- Equals form: `--output=json`, `--output=text`
### Index Reference
- **1-based indexing** for user-friendliness
- Completed steps are shown but read-only
- Cannot modify currently executing step (except via step-back)
### Commands Summary
| Command | Purpose | Example |
|---------|---------|---------|
| `steps list` | Show all steps | `steps list --verbose` |
| `steps add` | Add new step | `steps add run "npm test" --after 3` |
| `steps edit` | Modify step | `steps edit 4 --script "npm run test:ci"` |
| `steps remove` | Delete step | `steps remove 5` |
| `steps move` | Reorder step | `steps move 5 --after 2` |
| `steps export` | Generate YAML | `steps export --with-comments` |
### Position Modifiers
For `steps add` and `steps move`:
- `--at <index>` — Insert at specific position
- `--after <index>` — Insert after step
- `--before <index>` — Insert before step
- `--first` — Insert at first pending position
- `--last` — Insert at end (default)
### Full Command Reference
See "Command API Full Reference" section at end of document.
---
## Implementation Chunks
### Chunk 1: Command Parser & Infrastructure
**Goal:** Create the foundation for parsing and dispatching step commands.
**Files to create:**
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
- `src/Runner.Worker/Dap/StepCommands/StepCommandResult.cs`
**Files to modify:**
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add command dispatch in `HandleEvaluate()`
**Details:**
1. **StepCommandParser** — Parse REPL command strings into structured commands:
```csharp
public interface IStepCommandParser
{
StepCommand Parse(string input); // "steps add run \"echo hello\" --after 3"
bool IsStepCommand(string input); // Starts with "steps " or equals "steps"
}
public enum OutputFormat { Text, Json }
public abstract class StepCommand
{
public OutputFormat Output { get; set; } = OutputFormat.Text;
}
public class ListCommand : StepCommand { public bool Verbose; }
public class AddRunCommand : StepCommand {
public string Script;
public string Name;
public string Shell;
public StepPosition Position;
// ...
}
public class AddUsesCommand : StepCommand { /* ... */ }
public class EditCommand : StepCommand { /* ... */ }
public class RemoveCommand : StepCommand { public int Index; }
public class MoveCommand : StepCommand { public int FromIndex; public StepPosition Position; }
public class ExportCommand : StepCommand { public bool ChangesOnly; public bool WithComments; }
```
2. **StepPosition** — Represent insertion position:
```csharp
public class StepPosition
{
public PositionType Type { get; set; } // At, After, Before, First, Last
public int? Index { get; set; } // For At, After, Before
}
```
3. **StepCommandResult** — Standardized response:
```csharp
public class StepCommandResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string Error { get; set; } // Error code
public object Result { get; set; } // Command-specific data
}
```
4. **Integration in DapDebugSession.HandleEvaluate():**
```csharp
// After checking for !debug command
if (_stepCommandParser.IsStepCommand(expression))
{
return await HandleStepCommandAsync(expression, executionContext);
}
```
**Testing:**
- Unit tests for command parsing
- Test various edge cases (quoted strings, escapes, missing args)
**Estimated effort:** Small-medium
---
### Chunk 2: Step Serializer (ActionStep → YAML)
**Goal:** Convert `Pipelines.ActionStep` objects to YAML string representation.
**Files to create:**
- `src/Runner.Worker/Dap/StepCommands/StepSerializer.cs`
**Details:**
1. **IStepSerializer interface:**
```csharp
public interface IStepSerializer
{
string ToYaml(Pipelines.ActionStep step);
string ToYaml(IEnumerable<StepInfo> steps, bool withComments = false);
}
```
2. **Handle both step types:**
For `run` steps (ScriptReference):
```yaml
- name: Run Tests
run: |
npm ci
npm test
shell: bash
working-directory: src
env:
NODE_ENV: test
if: success()
continue-on-error: false
timeout-minutes: 10
```
For `uses` steps (RepositoryPathReference):
```yaml
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
env:
NODE_OPTIONS: --max-old-space-size=4096
if: success()
```
3. **Extract data from ActionStep:**
- `Reference` type determines run vs uses
- `Inputs` TemplateToken contains script (for run) or with values (for uses)
- `Environment` TemplateToken contains env vars
- `Condition` string contains if expression
- `DisplayName` or `DisplayNameToken` for name
4. **Use existing YAML infrastructure:**
- Consider using `YamlObjectWriter` from WorkflowParser
- Or use a simple string builder for more control
**Testing:**
- Round-trip tests: create step → serialize → verify YAML
- Test all step properties
- Test multi-line scripts
**Estimated effort:** Medium
---
### Chunk 3: Step Factory (Create new steps)
**Goal:** Create `Pipelines.ActionStep` and `IActionRunner` objects at runtime.
**Files to create:**
- `src/Runner.Worker/Dap/StepCommands/StepFactory.cs`
**Details:**
1. **IStepFactory interface:**
```csharp
public interface IStepFactory : IRunnerService
{
Pipelines.ActionStep CreateRunStep(
string script,
string name = null,
string shell = null,
string workingDirectory = null,
Dictionary<string, string> env = null,
string condition = null,
bool continueOnError = false,
int? timeoutMinutes = null);
Pipelines.ActionStep CreateUsesStep(
string actionReference, // "owner/repo@ref"
string name = null,
Dictionary<string, string> with = null,
Dictionary<string, string> env = null,
string condition = null,
bool continueOnError = false,
int? timeoutMinutes = null);
IActionRunner WrapInRunner(
Pipelines.ActionStep step,
IExecutionContext jobContext,
ActionRunStage stage = ActionRunStage.Main);
}
```
2. **CreateRunStep implementation:**
```csharp
public Pipelines.ActionStep CreateRunStep(...)
{
var step = new Pipelines.ActionStep
{
Id = Guid.NewGuid(),
Name = $"_dynamic_{Guid.NewGuid():N}",
DisplayName = name ?? "Run script",
Reference = new ScriptReference(),
Condition = condition ?? "success()",
ContinueOnError = CreateBoolToken(continueOnError),
TimeoutInMinutes = CreateIntToken(timeoutMinutes)
};
// Build Inputs mapping with script, shell, working-directory
step.Inputs = CreateRunInputs(script, shell, workingDirectory);
// Build Environment mapping
if (env?.Count > 0)
step.Environment = CreateEnvToken(env);
return step;
}
```
3. **CreateUsesStep implementation:**
```csharp
public Pipelines.ActionStep CreateUsesStep(string actionReference, ...)
{
var (name, ref_, path) = ParseActionReference(actionReference);
var step = new Pipelines.ActionStep
{
Id = Guid.NewGuid(),
Name = $"_dynamic_{Guid.NewGuid():N}",
DisplayName = displayName ?? actionReference,
Reference = new RepositoryPathReference
{
Name = name, // "actions/checkout"
Ref = ref_, // "v4"
Path = path, // null or "subdir"
RepositoryType = "GitHub"
},
Condition = condition ?? "success()"
};
// Build with inputs
if (with?.Count > 0)
step.Inputs = CreateWithInputs(with);
return step;
}
```
4. **ParseActionReference helper:**
- `actions/checkout@v4` → name=`actions/checkout`, ref=`v4`, path=null
- `actions/setup-node@v4` → name=`actions/setup-node`, ref=`v4`
- `owner/repo/subdir@ref` → name=`owner/repo`, ref=`ref`, path=`subdir`
- `docker://alpine:latest` → ContainerRegistryReference instead
5. **WrapInRunner:** Create IActionRunner from ActionStep (copy pattern from JobExtension.cs):
```csharp
public IActionRunner WrapInRunner(Pipelines.ActionStep step, IExecutionContext jobContext, ActionRunStage stage)
{
var runner = HostContext.CreateService<IActionRunner>();
runner.Action = step;
runner.Stage = stage;
runner.Condition = step.Condition;
runner.ExecutionContext = jobContext.CreateChild(
Guid.NewGuid(),
step.DisplayName,
step.Name
);
return runner;
}
```
**Testing:**
- Create run step → verify all properties
- Create uses step → verify reference parsing
- Test edge cases (missing shell, local actions, docker actions)
**Estimated effort:** Medium
---
### Chunk 4: Step Manipulator (Queue operations)
**Goal:** Manipulate the job step queue (add, remove, move, track changes).
**Files to create:**
- `src/Runner.Worker/Dap/StepCommands/StepManipulator.cs`
- `src/Runner.Worker/Dap/StepCommands/StepInfo.cs`
- `src/Runner.Worker/Dap/StepCommands/StepChange.cs`
**Details:**
1. **StepInfo** — Unified step representation:
```csharp
public class StepInfo
{
public int Index { get; set; }
public string Name { get; set; }
public string Type { get; set; } // "run" or "uses"
public string TypeDetail { get; set; } // action ref or script preview
public StepStatus Status { get; set; } // Completed, Current, Pending
public ChangeType? Change { get; set; } // Added, Modified, null
public Pipelines.ActionStep Action { get; set; }
public IStep Step { get; set; }
}
public enum StepStatus { Completed, Current, Pending }
public enum ChangeType { Added, Modified, Removed, Moved }
```
2. **StepChange** — Track modifications:
```csharp
public class StepChange
{
public ChangeType Type { get; set; }
public int OriginalIndex { get; set; }
public StepInfo OriginalStep { get; set; }
public StepInfo ModifiedStep { get; set; }
}
```
3. **IStepManipulator interface:**
```csharp
public interface IStepManipulator : IRunnerService
{
// Initialize with job context
void Initialize(IExecutionContext jobContext, int currentStepIndex);
void UpdateCurrentIndex(int index);
// Query
IReadOnlyList<StepInfo> GetAllSteps();
StepInfo GetStep(int index);
int GetPendingCount();
int GetFirstPendingIndex();
// Mutate
int InsertStep(IStep step, StepPosition position);
void RemoveStep(int index);
void MoveStep(int fromIndex, StepPosition position);
void EditStep(int index, Action<Pipelines.ActionStep> edit);
// Change tracking
IReadOnlyList<StepChange> GetChanges();
void RecordOriginalState(); // Call at session start
}
```
4. **Implementation details:**
- Maintain list of completed steps (from checkpoints/history)
- Access `jobContext.JobSteps` queue for pending steps
- Track all modifications for export diff
- Validate indices before operations
5. **Queue manipulation:**
```csharp
public int InsertStep(IStep step, StepPosition position)
{
// Convert queue to list
var pending = _jobContext.JobSteps.ToList();
_jobContext.JobSteps.Clear();
// Calculate insertion index
int insertAt = CalculateInsertIndex(position, pending.Count);
// Insert
pending.Insert(insertAt, step);
// Re-queue
foreach (var s in pending)
_jobContext.JobSteps.Enqueue(s);
// Track change
_changes.Add(new StepChange { Type = ChangeType.Added, ... });
return _currentIndex + insertAt + 1; // Return 1-based index
}
```
**Testing:**
- Insert at various positions
- Remove and verify queue state
- Move operations
- Change tracking accuracy
**Estimated effort:** Medium
---
### Chunk 5: REPL Commands (run steps)
**Goal:** Implement `steps list`, `steps add run`, `steps edit`, `steps remove`, `steps move`.
**Files to modify:**
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add `HandleStepCommandAsync()`
**Files to create:**
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
**Details:**
1. **IStepCommandHandler interface:**
```csharp
public interface IStepCommandHandler : IRunnerService
{
Task<StepCommandResult> HandleAsync(StepCommand command, IExecutionContext context);
}
```
2. **Command implementations:**
**List:**
```csharp
case ListCommand list:
var steps = _manipulator.GetAllSteps();
var output = FormatStepList(steps, list.Verbose);
return new StepCommandResult { Success = true, Result = steps, Message = output };
```
**Add run:**
```csharp
case AddRunCommand add:
var actionStep = _factory.CreateRunStep(
add.Script, add.Name, add.Shell, add.WorkingDirectory,
add.Env, add.Condition, add.ContinueOnError, add.Timeout);
var runner = _factory.WrapInRunner(actionStep, context);
var index = _manipulator.InsertStep(runner, add.Position);
return new StepCommandResult {
Success = true,
Message = $"Step added at position {index}",
Result = new { index, step = GetStepInfo(runner) }
};
```
**Edit:**
```csharp
case EditCommand edit:
ValidatePendingIndex(edit.Index);
_manipulator.EditStep(edit.Index, step => {
if (edit.Script != null) UpdateScript(step, edit.Script);
if (edit.Name != null) step.DisplayName = edit.Name;
if (edit.Condition != null) step.Condition = edit.Condition;
// ... other fields
});
return new StepCommandResult { Success = true, Message = $"Step {edit.Index} updated" };
```
**Remove:**
```csharp
case RemoveCommand remove:
ValidatePendingIndex(remove.Index);
_manipulator.RemoveStep(remove.Index);
return new StepCommandResult { Success = true, Message = $"Step {remove.Index} removed" };
```
**Move:**
```csharp
case MoveCommand move:
ValidatePendingIndex(move.FromIndex);
var newIndex = _manipulator.MoveStep(move.FromIndex, move.Position);
return new StepCommandResult { Success = true, Message = $"Step moved to position {newIndex}" };
```
3. **Integration in DapDebugSession:**
```csharp
private async Task<Response> HandleStepCommandAsync(string expression, IExecutionContext context)
{
try
{
var command = _commandParser.Parse(expression);
var result = await _commandHandler.HandleAsync(command, context);
if (result.Success)
return CreateSuccessResponse(new EvaluateResponseBody
{
Result = result.Message,
Type = "string"
});
else
return CreateErrorResponse($"{result.Error}: {result.Message}");
}
catch (StepCommandException ex)
{
return CreateErrorResponse(ex.Message);
}
}
```
**Testing:**
- End-to-end: add step → list → verify
- Edit various properties
- Remove and verify indices shift
- Move operations
**Estimated effort:** Medium-large
---
### Chunk 6: Action Download Integration (steps add uses)
**Goal:** Support `steps add uses` with full action download.
**Files to modify:**
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
**Details:**
1. **Add uses command handling:**
```csharp
case AddUsesCommand add:
// Create the step
var actionStep = _factory.CreateUsesStep(
add.Action, add.Name, add.With, add.Env,
add.Condition, add.ContinueOnError, add.Timeout);
// Download the action (this is the key difference from run steps)
var actionManager = HostContext.GetService<IActionManager>();
var prepareResult = await actionManager.PrepareActionsAsync(
context,
new[] { actionStep }
);
// Check for pre-steps (some actions have setup steps)
if (prepareResult.PreStepTracker.TryGetValue(actionStep.Id, out var preStep))
{
// Insert pre-step before main step
var preRunner = _factory.WrapInRunner(preStep.Action, context, ActionRunStage.Pre);
_manipulator.InsertStep(preRunner, add.Position);
}
// Wrap and insert main step
var runner = _factory.WrapInRunner(actionStep, context);
var index = _manipulator.InsertStep(runner, CalculateMainPosition(add.Position, hasPreStep));
// Handle post-steps (cleanup)
// These go to PostJobSteps stack, handled by existing infrastructure
return new StepCommandResult {
Success = true,
Message = $"Action '{add.Action}' added at position {index}",
Result = new { index, step = GetStepInfo(runner) }
};
```
2. **Error handling:**
- Action not found → clear error message with action name
- Network failure → suggest retry
- Invalid action reference format → parse error
3. **Production note (for future):**
```csharp
// TODO: Before production release, add action restriction checks:
// - Verify action is in organization's allowed list
// - Check verified creator requirements
// - Enforce enterprise policies
// For now, allow all actions in prototype
```
**Testing:**
- Add common actions (checkout, setup-node)
- Test actions with pre/post steps
- Test local actions (./.github/actions/...)
- Test docker actions (docker://...)
- Error cases: invalid action, network failure
**Estimated effort:** Medium
---
### Chunk 7: Export Command (steps export)
**Goal:** Generate YAML output for modified steps.
**Files to modify:**
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
**Details:**
1. **Export command handling:**
```csharp
case ExportCommand export:
var steps = _manipulator.GetAllSteps();
var changes = _manipulator.GetChanges();
IEnumerable<StepInfo> toExport;
if (export.ChangesOnly)
{
toExport = steps.Where(s => s.Change != null);
}
else
{
toExport = steps;
}
var yaml = _serializer.ToYaml(toExport, export.WithComments);
return new StepCommandResult
{
Success = true,
Message = yaml,
Result = new {
yaml,
totalSteps = steps.Count,
addedCount = changes.Count(c => c.Type == ChangeType.Added),
modifiedCount = changes.Count(c => c.Type == ChangeType.Modified)
}
};
```
2. **YAML output format:**
```yaml
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node # ADDED
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run Tests # MODIFIED
run: |
npm ci
npm test
shell: bash
```
3. **Change comments (when --with-comments):**
- `# ADDED` for new steps
- `# MODIFIED` for edited steps
- Removed steps listed at bottom as comments (optional)
**Testing:**
- Export with no changes → valid YAML
- Export with additions → ADDED comments
- Export with modifications → MODIFIED comments
- Verify YAML is valid and can be pasted into workflow
**Estimated effort:** Small
---
### Chunk 8: Output Format Flag for Browser Extension
**Goal:** Add `--output` flag support for programmatic access (replaces separate JSON API).
**Files to modify:**
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
**Details:**
1. **Add OutputFormat enum and property to base StepCommand:**
```csharp
public enum OutputFormat { Text, Json }
public abstract class StepCommand
{
public OutputFormat Output { get; set; } = OutputFormat.Text;
}
```
2. **Parse `--output` flag in command parser:**
```csharp
// Recognize --output json, --output text, -o json, -o text, --output=json
private OutputFormat ParseOutputFlag(List<string> tokens)
{
for (int i = 0; i < tokens.Count; i++)
{
var token = tokens[i].ToLower();
if (token == "--output" || token == "-o")
{
if (i + 1 < tokens.Count)
{
var format = tokens[i + 1].ToLower();
tokens.RemoveAt(i); tokens.RemoveAt(i);
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
}
}
else if (token.StartsWith("--output="))
{
var format = token.Substring("--output=".Length);
tokens.RemoveAt(i);
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
}
}
return OutputFormat.Text;
}
```
3. **Format responses based on Output property in handler:**
```csharp
if (command.Output == OutputFormat.Json)
{
return new StepCommandResult
{
Success = true,
Message = JsonConvert.SerializeObject(new { Success = true, Result = data })
};
}
else
{
return new StepCommandResult
{
Success = true,
Message = FormatAsText(data)
};
}
```
4. **Browser extension sends REPL commands with `--output json`:**
```javascript
// Browser extension builds command strings like:
// "steps list --output json"
// "steps add run \"echo test\" --after 3 --output json"
```
**Benefits over separate JSON API:**
- Single code path for parsing all commands
- Easier debugging (UI sends same commands humans would type)
- Commands can be copy-pasted from UI to console for testing
- Less code to maintain
**Testing:**
- All commands with `--output json` return valid JSON
- All commands with `--output text` (or default) return human-readable text
- Short form `-o json` works correctly
**Estimated effort:** Small
---
### Chunk 9: Browser Extension UI
**Goal:** Add step manipulation UI to the browser extension.
**Files to modify:**
- `browser-ext/content/content.js`
- `browser-ext/content/content.css`
- `browser-ext/background/background.js`
**Details:**
1. **Steps Panel in Debugger Pane:**
```html
<div class="dap-steps-panel">
<div class="dap-steps-header">
<span>Steps</span>
<button class="dap-add-step-btn">+ Add</button>
</div>
<div class="dap-steps-list">
<!-- Steps rendered here -->
</div>
<div class="dap-steps-footer">
<button class="dap-export-btn">Export Changes</button>
</div>
</div>
```
2. **Step List Rendering:**
```javascript
function renderSteps(steps) {
return steps.map(step => `
<div class="dap-step ${step.status}" data-index="${step.index}">
<span class="dap-step-status">${getStatusIcon(step.status)}</span>
<span class="dap-step-index">${step.index}.</span>
<span class="dap-step-name">${step.name}</span>
${step.change ? `<span class="dap-step-badge">[${step.change}]</span>` : ''}
<span class="dap-step-type">${step.type}</span>
${step.status === 'pending' ? renderStepActions(step) : ''}
</div>
`).join('');
}
```
3. **Add Step Dialog:**
```javascript
function showAddStepDialog() {
// Modal with:
// - Type selector: run / uses
// - For run: script textarea, shell dropdown
// - For uses: action input with autocomplete
// - Common: name, if condition, env vars
// - Position: dropdown (after current, at end, at position)
}
```
4. **Step Context Menu:**
```javascript
function showStepContextMenu(stepIndex, event) {
// Edit, Move Up, Move Down, Delete
}
```
5. **Export Modal:**
```javascript
function showExportModal(yaml) {
// Code view with syntax highlighting
// Copy to clipboard button
}
```
6. **Send commands via REPL format with `--output json`:**
```javascript
async function addStep(type, options) {
const cmd = buildStepCommand('step.add', { type, ...options });
const response = await sendEvaluate(cmd); // e.g., "steps add run \"echo test\" --output json"
refreshStepList();
}
async function loadSteps() {
const response = await sendEvaluate('steps list --output json');
// Parse JSON response
}
```
**Testing:**
- Manual testing in Chrome
- All UI operations work correctly
- Responsive layout
**Estimated effort:** Medium-large
---
## File Summary
### New Files
| File | Chunk | Purpose |
|------|-------|---------|
| `StepCommands/StepCommandParser.cs` | 1 | Parse REPL and JSON commands |
| `StepCommands/StepCommandResult.cs` | 1 | Standardized command responses |
| `StepCommands/StepSerializer.cs` | 2 | ActionStep → YAML conversion |
| `StepCommands/StepFactory.cs` | 3 | Create new ActionStep objects |
| `StepCommands/StepManipulator.cs` | 4 | Queue operations & change tracking |
| `StepCommands/StepInfo.cs` | 4 | Step info data structure |
| `StepCommands/StepChange.cs` | 4 | Change tracking data structure |
| `StepCommands/StepCommandHandler.cs` | 5 | Command execution logic |
### Modified Files
| File | Chunk | Changes |
|------|-------|---------|
| `DapDebugSession.cs` | 1, 5 | Add command dispatch, wire up services |
| `StepCommandParser.cs` | 8 | Add `--output` flag parsing |
| `StepCommandHandler.cs` | 8 | Format responses based on OutputFormat |
| `content.js` | 9 | Steps panel, dialogs, export modal, build REPL commands |
| `content.css` | 9 | Styling for new UI elements |
| `background.js` | 9 | Helper functions if needed |
---
## Command API Full Reference
### Output Format Flag
All commands support the `--output` flag:
```
steps <command> ... --output text # Human-readable output (default)
steps <command> ... --output json # JSON output for programmatic use
steps <command> ... -o json # Short form
steps <command> ... --output=json # Equals form
```
The browser extension uses `--output json` for all commands to receive structured responses.
### steps list
```
steps list [--verbose] [--output text|json]
```
Show all steps with their indices, status, and modification state.
**Output format:**
```
Steps:
✓ 1. Checkout uses actions/checkout@v4
✓ 2. Setup Node uses actions/setup-node@v4
▶ 3. Install deps run npm ci
4. Run tests [MODIFIED] run npm test
5. Build [ADDED] run npm run build
6. Deploy run ./deploy.sh
Legend: ✓ = completed, ▶ = current/paused, [ADDED] = new, [MODIFIED] = edited
```
**JSON output (`steps list --output json`):**
```json
{
"Success": true,
"Result": [
{"index": 1, "name": "Checkout", "type": "uses", "typeDetail": "actions/checkout@v4", "status": "completed"},
{"index": 2, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "completed"},
{"index": 3, "name": "Install deps", "type": "run", "typeDetail": "npm ci", "status": "current"},
{"index": 4, "name": "Run tests", "type": "run", "typeDetail": "npm test", "status": "pending", "change": "MODIFIED"},
{"index": 5, "name": "Build", "type": "run", "typeDetail": "npm run build", "status": "pending", "change": "ADDED"}
]
}
```
### steps add
**Run step:**
```
steps add run "<script>" [options]
Options:
--name "<name>" Display name
--shell <shell> Shell (bash, sh, pwsh, etc.)
--working-directory <path> Working directory
--if "<condition>" Condition expression
--env KEY=value Environment variable (repeatable)
--continue-on-error Don't fail job on step failure
--timeout <minutes> Step timeout
--at <index> Insert at position
--after <index> Insert after step
--before <index> Insert before step
--first Insert at first pending position
--last Insert at end (default)
--output text|json Response format (default: text)
```
**Uses step:**
```
steps add uses <action> [options]
Options:
--name "<name>" Display name
--with key=value Input parameter (repeatable)
--if "<condition>" Condition expression
--env KEY=value Environment variable (repeatable)
--continue-on-error Don't fail job on step failure
--timeout <minutes> Step timeout
[position options same as run]
--output text|json Response format (default: text)
```
**Examples:**
```bash
# Human usage (text output)
steps add run "npm test" --name "Run Tests" --after 3
# Browser extension (JSON output)
steps add uses actions/setup-node@v4 --with node-version=20 --output json
```
**JSON response (`--output json`):**
```json
{
"Success": true,
"Message": "Step added at position 4",
"Result": {"index": 4, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "pending", "change": "ADDED"}
}
```
**Position options:**
- `--at 3` — Insert at position 3
- `--after 2` — Insert after step 2
- `--before 4` — Insert before step 4
- `--first` — Insert at first pending position
- `--last` — Insert at end (default if omitted)
### steps edit
```
steps edit <index> [modifications]
Modifications:
--name "<name>" Change display name
--script "<script>" Change script (run only)
--action "<action>" Change action (uses only)
--shell <shell> Change shell (run only)
--working-directory <path> Change working directory
--if "<condition>" Change condition
--with key=value Set/update input (uses only)
--env KEY=value Set/update env var
--remove-with <key> Remove input
--remove-env <KEY> Remove env var
--continue-on-error Enable continue-on-error
--no-continue-on-error Disable continue-on-error
--timeout <minutes> Change timeout
--output text|json Response format (default: text)
```
**Examples:**
```bash
# Human usage
steps edit 4 --script "npm run test:ci" --name "CI Tests"
# Browser extension
steps edit 4 --script "npm run test:ci" --output json
```
**JSON response (`--output json`):**
```json
{
"Success": true,
"Message": "Step 4 updated",
"Result": {"index": 4, "name": "CI Tests", "type": "run", "typeDetail": "npm run test:ci", "status": "pending", "change": "MODIFIED"}
}
```
### steps remove
```
steps remove <index> [--output text|json]
```
**Examples:**
```bash
steps remove 5
steps remove 5 --output json
```
**JSON response (`--output json`):**
```json
{"Success": true, "Message": "Step 5 removed"}
```
### steps move
```
steps move <from> <position> [--output text|json]
Position (one required):
--to <index> Move to position
--after <index> Move after step
--before <index> Move before step
--first Move to first pending position
--last Move to end
```
**Examples:**
```bash
steps move 5 --after 2
steps move 5 --after 2 --output json
```
**JSON response (`--output json`):**
```json
{"Success": true, "Message": "Step moved to position 3"}
```
### steps export
```
steps export [--changes-only] [--with-comments] [--output text|json]
```
**Examples:**
```bash
steps export --with-comments
steps export --changes-only --output json
```
**JSON response (`--output json`):**
```json
{
"Success": true,
"Result": {
"yaml": "steps:\n - name: Checkout\n uses: actions/checkout@v4\n...",
"totalSteps": 5,
"addedCount": 1,
"modifiedCount": 1
}
}
```
!step edit <index> [modifications]
Modifications:
--name "<name>" Change display name
--script "<script>" Change script (run only)
--action "<action>" Change action (uses only)
--shell <shell> Change shell (run only)
--working-directory <path> Change working directory
--if "<condition>" Change condition
--with key=value Set/update input (uses only)
--env KEY=value Set/update env var
--remove-with <key> Remove input
--remove-env <KEY> Remove env var
--continue-on-error Enable continue-on-error
--no-continue-on-error Disable continue-on-error
--timeout <minutes> Change timeout
```
**JSON:**
```json
{
"cmd": "step.edit",
"index": 4,
"script": "npm run test:ci",
"name": "CI Tests",
"with": {"node-version": "22"},
"removeWith": ["cache"],
"env": {"CI": "true"},
"removeEnv": ["DEBUG"]
}
```
### !step remove
```
!step remove <index>
```
**JSON:**
```json
{"cmd": "step.remove", "index": 5}
```
### !step move
```
!step move <from> <position>
Position (one required):
--to <index> Move to position
--after <index> Move after step
--before <index> Move before step
--first Move to first pending position
--last Move to end
```
**JSON:**
```json
{"cmd": "step.move", "from": 5, "position": {"after": 2}}
```
### !step export
```
!step export [--changes-only] [--with-comments]
```
**JSON:**
```json
{"cmd": "step.export", "changesOnly": false, "withComments": true}
```
---
## Validation Rules
1. **Index bounds**: Must be 1 ≤ index ≤ total_steps
2. **Completed steps**: Cannot edit, remove, or move completed steps
3. **Current step**: Cannot remove or move the currently executing step (can edit for next run if step-back)
4. **Position conflicts**: `--at`, `--after`, `--before`, `--first`, `--last` are mutually exclusive
5. **Type-specific options**: `--script`, `--shell`, `--working-directory` only for run; `--action`, `--with` only for uses
6. **Required values**: `--at`, `--after`, `--before`, `--to` require an index value
---
## Error Codes
| Code | Description |
|------|-------------|
| `INVALID_INDEX` | Index out of range or refers to completed/current step |
| `INVALID_COMMAND` | Unknown command |
| `INVALID_OPTION` | Unknown or conflicting options |
| `INVALID_TYPE` | Invalid step type (not "run" or "uses") |
| `ACTION_DOWNLOAD_FAILED` | Failed to download action for uses step |
| `PARSE_ERROR` | Failed to parse command/JSON |
---
## Future Commands (Reserved)
These command names are reserved for future implementation:
```
steps duplicate <index> # Clone a step
steps enable <index> # Re-enable a disabled step
steps disable <index> # Skip step without removing
steps inspect <index> # Show detailed step info
steps reset <index> # Revert modifications
steps import # Add steps from YAML
```
---
## Notes for Production
1. **Action Restrictions:** Before production, must integrate with organization/enterprise action policies (allowed lists, verified creators, etc.)
2. **Security:** Dynamic step execution has security implications - ensure proper sandboxing and audit logging
3. **Persistence:** Consider whether modifications should persist across step-back operations
4. **Undo:** Consider adding `!step undo` for reverting last operation