mirror of
https://github.com/actions/runner.git
synced 2026-01-22 20:44:30 +08:00
651 lines
20 KiB
Markdown
651 lines
20 KiB
Markdown
# Plan: Simplify Step Commands to Use REPL Format
|
|
|
|
**Status:** Ready for Implementation
|
|
**Date:** January 2026
|
|
**Prerequisites:** dap-step-manipulation.md (Chunks 1-9 completed)
|
|
|
|
## Overview
|
|
|
|
Remove the JSON API for step commands and use a single REPL command format (`steps <command>`) for both human input and browser extension UI. Add `--output` flag for controlling response format.
|
|
|
|
## Problem
|
|
|
|
Currently the step command system has two input formats:
|
|
1. REPL format: `!step list` (for humans typing in console)
|
|
2. JSON format: `{"cmd":"step.list"}` (for browser extension UI)
|
|
|
|
This causes issues:
|
|
- The `!` prefix is awkward for humans typing commands
|
|
- The JSON API is unnecessary complexity (browser extension is just another DAP client)
|
|
- Debugging is harder because UI sends different format than humans would type
|
|
- Two code paths to maintain and test
|
|
|
|
## Goals
|
|
|
|
1. Replace `!step` prefix with `steps` (more ergonomic, no special character)
|
|
2. Remove JSON command parsing (unnecessary complexity)
|
|
3. Add `--output` flag for response format control (`text` or `json`)
|
|
4. Browser extension sends same command strings a human would type
|
|
5. Single code path for all step command input
|
|
|
|
## Progress Checklist
|
|
|
|
- [ ] **Chunk 1:** Update StepCommandParser - `steps` prefix, `--output` flag, remove JSON parsing
|
|
- [ ] **Chunk 2:** Update StepCommandHandler - format responses based on OutputFormat
|
|
- [ ] **Chunk 3:** Update Browser Extension - build REPL command strings
|
|
- [ ] **Chunk 4:** Update REPL context detection in browser extension
|
|
- [ ] **Chunk 5:** Update/remove tests
|
|
- [ ] **Chunk 6:** Update plan documentation
|
|
|
|
---
|
|
|
|
## Implementation Chunks
|
|
|
|
### Chunk 1: Update StepCommandParser to Use `steps` Prefix
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
|
|
|
|
**Changes:**
|
|
|
|
1. **Add `OutputFormat` enum and update `StepCommand` base class:**
|
|
```csharp
|
|
public enum OutputFormat
|
|
{
|
|
Text,
|
|
Json
|
|
}
|
|
|
|
public abstract class StepCommand
|
|
{
|
|
/// <summary>
|
|
/// Output format for the command response.
|
|
/// </summary>
|
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
|
}
|
|
```
|
|
|
|
Remove the `WasJsonInput` property (replaced by `OutputFormat`).
|
|
|
|
2. **Update `IsStepCommand()`** - recognize `steps` prefix, remove JSON detection:
|
|
```csharp
|
|
public bool IsStepCommand(string input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
return false;
|
|
|
|
var trimmed = input.Trim();
|
|
|
|
// Command format: steps ...
|
|
if (trimmed.StartsWith("steps ", StringComparison.OrdinalIgnoreCase) ||
|
|
trimmed.Equals("steps", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
```
|
|
|
|
3. **Update `Parse()`** - remove JSON branch:
|
|
```csharp
|
|
public StepCommand Parse(string input)
|
|
{
|
|
var trimmed = input?.Trim() ?? "";
|
|
return ParseReplCommand(trimmed);
|
|
}
|
|
```
|
|
|
|
4. **Update `ParseReplCommand()`** - expect `steps` as first token:
|
|
```csharp
|
|
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Invalid command format. Expected: steps <command> [args...]");
|
|
}
|
|
```
|
|
|
|
5. **Add `--output` flag parsing** - create a helper method and call it in each Parse*Command method:
|
|
```csharp
|
|
private OutputFormat ParseOutputFlag(List<string> tokens, ref int index)
|
|
{
|
|
// Look for --output, --output=json, --output=text, -o json, -o text
|
|
for (int i = index; 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); // Remove flag
|
|
tokens.RemoveAt(i); // Remove value
|
|
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;
|
|
}
|
|
```
|
|
|
|
Apply to each command parser before processing other flags.
|
|
|
|
6. **Delete all JSON parsing methods:**
|
|
- `ParseJsonCommand()`
|
|
- `ParseJsonListCommand()`
|
|
- `ParseJsonAddCommand()`
|
|
- `ParseJsonEditCommand()`
|
|
- `ParseJsonRemoveCommand()`
|
|
- `ParseJsonMoveCommand()`
|
|
- `ParseJsonExportCommand()`
|
|
- `ParseJsonPosition()`
|
|
- `ParseJsonDictionary()`
|
|
- `ParseJsonStringList()`
|
|
|
|
7. **Update error messages** to reference `steps <command>` format.
|
|
|
|
**Estimated effort:** Small-medium
|
|
|
|
---
|
|
|
|
### Chunk 2: Update StepCommandHandler Response Format
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
|
|
|
**Changes:**
|
|
|
|
1. **Update each command handler** to format response based on `command.Output`:
|
|
|
|
For `ListCommand`:
|
|
```csharp
|
|
if (command.Output == OutputFormat.Json)
|
|
{
|
|
return new StepCommandResult
|
|
{
|
|
Success = true,
|
|
Message = JsonConvert.SerializeObject(new { Success = true, Result = steps }),
|
|
Result = steps
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new StepCommandResult
|
|
{
|
|
Success = true,
|
|
Message = FormatStepListAsText(steps),
|
|
Result = steps
|
|
};
|
|
}
|
|
```
|
|
|
|
2. **Add text formatting helpers:**
|
|
```csharp
|
|
private string FormatStepListAsText(IReadOnlyList<StepInfo> steps)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("Steps:");
|
|
foreach (var step in steps)
|
|
{
|
|
var statusIcon = step.Status switch
|
|
{
|
|
StepStatus.Completed => "✓",
|
|
StepStatus.Current => "▶",
|
|
_ => " "
|
|
};
|
|
var changeBadge = step.Change.HasValue ? $"[{step.Change}]" : "";
|
|
sb.AppendLine($" {statusIcon} {step.Index}. {step.Name,-30} {changeBadge,-12} {step.Type,-5} {step.TypeDetail}");
|
|
}
|
|
sb.AppendLine();
|
|
sb.AppendLine("Legend: ✓ = completed, ▶ = current, [ADDED] = new, [MODIFIED] = edited");
|
|
return sb.ToString();
|
|
}
|
|
```
|
|
|
|
3. **Update error responses** to also respect output format.
|
|
|
|
4. **Remove `WasJsonInput` checks** throughout the handler.
|
|
|
|
**Estimated effort:** Small
|
|
|
|
---
|
|
|
|
### Chunk 3: Update Browser Extension - Build Command Strings
|
|
|
|
**Files to modify:**
|
|
- `browser-ext/content/content.js`
|
|
|
|
**Changes:**
|
|
|
|
1. **Replace `sendStepCommand()` implementation:**
|
|
|
|
```javascript
|
|
/**
|
|
* Send step command via REPL format
|
|
*/
|
|
async function sendStepCommand(action, options = {}) {
|
|
const expression = buildStepCommand(action, options);
|
|
try {
|
|
const response = await sendDapRequest('evaluate', {
|
|
expression,
|
|
frameId: currentFrameId,
|
|
context: 'repl',
|
|
});
|
|
|
|
if (response.result) {
|
|
try {
|
|
return JSON.parse(response.result);
|
|
} catch (e) {
|
|
// Response might be plain text for non-JSON output
|
|
return { Success: true, Message: response.result };
|
|
}
|
|
}
|
|
return { Success: false, Error: 'NO_RESPONSE', Message: 'No response from server' };
|
|
} catch (error) {
|
|
return { Success: false, Error: 'REQUEST_FAILED', Message: error.message };
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Add `buildStepCommand()` function:**
|
|
|
|
```javascript
|
|
/**
|
|
* Build REPL command string from action and options
|
|
*/
|
|
function buildStepCommand(action, options) {
|
|
let cmd;
|
|
switch (action) {
|
|
case 'step.list':
|
|
cmd = options.verbose ? 'steps list --verbose' : 'steps list';
|
|
break;
|
|
case 'step.add':
|
|
cmd = buildAddStepCommand(options);
|
|
break;
|
|
case 'step.edit':
|
|
cmd = buildEditStepCommand(options);
|
|
break;
|
|
case 'step.remove':
|
|
cmd = `steps remove ${options.index}`;
|
|
break;
|
|
case 'step.move':
|
|
cmd = buildMoveStepCommand(options);
|
|
break;
|
|
case 'step.export':
|
|
cmd = buildExportCommand(options);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown step command: ${action}`);
|
|
}
|
|
// Always request JSON output for programmatic use
|
|
return cmd + ' --output json';
|
|
}
|
|
```
|
|
|
|
3. **Add command builder helpers:**
|
|
|
|
```javascript
|
|
function buildAddStepCommand(options) {
|
|
let cmd = 'steps add';
|
|
|
|
if (options.type === 'run') {
|
|
cmd += ` run ${quoteString(options.script)}`;
|
|
if (options.shell) cmd += ` --shell ${options.shell}`;
|
|
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
|
|
} else if (options.type === 'uses') {
|
|
cmd += ` uses ${options.action}`;
|
|
if (options.with) {
|
|
for (const [key, value] of Object.entries(options.with)) {
|
|
cmd += ` --with ${key}=${value}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
|
if (options.env) {
|
|
for (const [key, value] of Object.entries(options.env)) {
|
|
cmd += ` --env ${key}=${value}`;
|
|
}
|
|
}
|
|
if (options.continueOnError) cmd += ' --continue-on-error';
|
|
if (options.timeout) cmd += ` --timeout ${options.timeout}`;
|
|
|
|
// Position
|
|
if (options.position) {
|
|
if (options.position.after) cmd += ` --after ${options.position.after}`;
|
|
else if (options.position.before) cmd += ` --before ${options.position.before}`;
|
|
else if (options.position.at) cmd += ` --at ${options.position.at}`;
|
|
else if (options.position.first) cmd += ' --first';
|
|
// --last is default, no need to specify
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
function buildEditStepCommand(options) {
|
|
let cmd = `steps edit ${options.index}`;
|
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
|
if (options.script) cmd += ` --script ${quoteString(options.script)}`;
|
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
|
if (options.shell) cmd += ` --shell ${options.shell}`;
|
|
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
|
|
return cmd;
|
|
}
|
|
|
|
function buildMoveStepCommand(options) {
|
|
let cmd = `steps move ${options.from}`;
|
|
const pos = options.position;
|
|
if (pos.after) cmd += ` --after ${pos.after}`;
|
|
else if (pos.before) cmd += ` --before ${pos.before}`;
|
|
else if (pos.at) cmd += ` --to ${pos.at}`;
|
|
else if (pos.first) cmd += ' --first';
|
|
else if (pos.last) cmd += ' --last';
|
|
return cmd;
|
|
}
|
|
|
|
function buildExportCommand(options) {
|
|
let cmd = 'steps export';
|
|
if (options.changesOnly) cmd += ' --changes-only';
|
|
if (options.withComments) cmd += ' --with-comments';
|
|
return cmd;
|
|
}
|
|
|
|
/**
|
|
* Quote a string for use in command, escaping as needed
|
|
*/
|
|
function quoteString(str) {
|
|
// Escape backslashes and quotes, wrap in quotes
|
|
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
}
|
|
```
|
|
|
|
4. **Update `loadSteps()`:**
|
|
```javascript
|
|
async function loadSteps() {
|
|
try {
|
|
const response = await sendDapRequest('evaluate', {
|
|
expression: 'steps list --output json',
|
|
frameId: currentFrameId,
|
|
context: 'repl',
|
|
});
|
|
// ... rest of parsing logic unchanged
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estimated effort:** Medium
|
|
|
|
---
|
|
|
|
### Chunk 4: Update REPL Context Detection
|
|
|
|
**Files to modify:**
|
|
- `browser-ext/content/content.js`
|
|
|
|
**Changes:**
|
|
|
|
Update `handleReplKeydown()` to set context to 'repl' for `steps` commands:
|
|
|
|
```javascript
|
|
async function handleReplKeydown(e) {
|
|
const input = e.target;
|
|
|
|
if (e.key === 'Enter') {
|
|
const command = input.value.trim();
|
|
if (!command) return;
|
|
|
|
replHistory.push(command);
|
|
replHistoryIndex = replHistory.length;
|
|
input.value = '';
|
|
|
|
// Show command
|
|
appendOutput(`> ${command}`, 'input');
|
|
|
|
// Send to DAP
|
|
try {
|
|
const response = await sendDapRequest('evaluate', {
|
|
expression: command,
|
|
frameId: currentFrameId,
|
|
// Use 'repl' context for shell commands (!) and step commands
|
|
context: (command.startsWith('!') || command.startsWith('steps')) ? 'repl' : 'watch',
|
|
});
|
|
// ... rest unchanged
|
|
}
|
|
}
|
|
// ... arrow key handling unchanged
|
|
}
|
|
```
|
|
|
|
**Estimated effort:** Trivial
|
|
|
|
---
|
|
|
|
### Chunk 5: Update/Remove Tests
|
|
|
|
**Files to modify:**
|
|
- `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserJsonL0.cs` - **Delete**
|
|
- `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs` - Modify
|
|
|
|
**Changes:**
|
|
|
|
1. **Delete `StepCommandParserJsonL0.cs`** entirely (JSON parsing tests no longer needed)
|
|
|
|
2. **Update `StepCommandParserL0.cs`:**
|
|
|
|
a. Update `IsStepCommand` tests:
|
|
```csharp
|
|
[Fact]
|
|
public void IsStepCommand_DetectsStepsPrefix()
|
|
{
|
|
Assert.True(_parser.IsStepCommand("steps list"));
|
|
Assert.True(_parser.IsStepCommand("steps add run \"test\""));
|
|
Assert.True(_parser.IsStepCommand("STEPS LIST")); // case insensitive
|
|
Assert.True(_parser.IsStepCommand(" steps list ")); // whitespace
|
|
}
|
|
|
|
[Fact]
|
|
public void IsStepCommand_RejectsInvalid()
|
|
{
|
|
Assert.False(_parser.IsStepCommand("step list")); // missing 's'
|
|
Assert.False(_parser.IsStepCommand("!step list")); // old format
|
|
Assert.False(_parser.IsStepCommand("stepslist")); // no space
|
|
Assert.False(_parser.IsStepCommand(""));
|
|
Assert.False(_parser.IsStepCommand(null));
|
|
}
|
|
```
|
|
|
|
b. Change all `!step` to `steps` in existing test cases:
|
|
```csharp
|
|
// Before:
|
|
var cmd = _parser.Parse("!step list --verbose");
|
|
|
|
// After:
|
|
var cmd = _parser.Parse("steps list --verbose");
|
|
```
|
|
|
|
c. Add tests for `--output` flag:
|
|
```csharp
|
|
[Fact]
|
|
public void Parse_ListCommand_WithOutputJson()
|
|
{
|
|
var cmd = _parser.Parse("steps list --output json") as ListCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Json, cmd.Output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ListCommand_WithOutputText()
|
|
{
|
|
var cmd = _parser.Parse("steps list --output text") as ListCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Text, cmd.Output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ListCommand_DefaultOutputIsText()
|
|
{
|
|
var cmd = _parser.Parse("steps list") as ListCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Text, cmd.Output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_AddCommand_WithOutputFlag()
|
|
{
|
|
var cmd = _parser.Parse("steps add run \"echo test\" --output json") as AddRunCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Json, cmd.Output);
|
|
Assert.Equal("echo test", cmd.Script);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_OutputFlag_ShortForm()
|
|
{
|
|
var cmd = _parser.Parse("steps list -o json") as ListCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Json, cmd.Output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_OutputFlag_EqualsForm()
|
|
{
|
|
var cmd = _parser.Parse("steps list --output=json") as ListCommand;
|
|
Assert.NotNull(cmd);
|
|
Assert.Equal(OutputFormat.Json, cmd.Output);
|
|
}
|
|
```
|
|
|
|
d. Update error message expectations to reference `steps` format.
|
|
|
|
**Estimated effort:** Small
|
|
|
|
---
|
|
|
|
### Chunk 6: Update Plan Documentation
|
|
|
|
**Files to modify:**
|
|
- `.opencode/plans/dap-step-manipulation.md`
|
|
|
|
**Changes:**
|
|
|
|
1. **Update command format documentation** - change all `!step` references to `steps`
|
|
|
|
2. **Document `--output` flag** in command reference:
|
|
```
|
|
### 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`
|
|
```
|
|
|
|
3. **Update Chunk 8 description** - note that JSON API was replaced with `--output` flag
|
|
|
|
4. **Update command reference table:**
|
|
```
|
|
| 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` |
|
|
```
|
|
|
|
**Estimated effort:** Trivial
|
|
|
|
---
|
|
|
|
## File Summary
|
|
|
|
| File | Action | Chunk | Description |
|
|
|------|--------|-------|-------------|
|
|
| `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` | Modify | 1 | Change prefix to `steps`, add `--output` flag, remove JSON parsing |
|
|
| `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` | Modify | 2 | Format responses based on `OutputFormat` |
|
|
| `browser-ext/content/content.js` | Modify | 3, 4 | Build REPL command strings, update context detection |
|
|
| `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserJsonL0.cs` | Delete | 5 | No longer needed |
|
|
| `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs` | Modify | 5 | Update for `steps` prefix, add `--output` tests |
|
|
| `.opencode/plans/dap-step-manipulation.md` | Modify | 6 | Update documentation |
|
|
|
|
---
|
|
|
|
## Command Reference (After Changes)
|
|
|
|
### Human Usage (text output, default)
|
|
|
|
| Action | Command |
|
|
|--------|---------|
|
|
| List steps | `steps list` |
|
|
| List verbose | `steps list --verbose` |
|
|
| Add run step | `steps add run "echo hello"` |
|
|
| Add run with options | `steps add run "npm test" --name "Run tests" --shell bash` |
|
|
| Add uses step | `steps add uses actions/checkout@v4` |
|
|
| Add uses with inputs | `steps add uses actions/setup-node@v4 --with node-version=20` |
|
|
| Edit step | `steps edit 4 --name "New name" --script "new script"` |
|
|
| Remove step | `steps remove 5` |
|
|
| Move step | `steps move 5 --after 2` |
|
|
| Export | `steps export` |
|
|
| Export with options | `steps export --changes-only --with-comments` |
|
|
|
|
### Browser Extension (JSON output)
|
|
|
|
The browser extension appends `--output json` to all commands:
|
|
|
|
| Action | Command Sent |
|
|
|--------|--------------|
|
|
| List steps | `steps list --output json` |
|
|
| Add step | `steps add uses actions/checkout@v4 --output json` |
|
|
| Remove step | `steps remove 5 --output json` |
|
|
|
|
---
|
|
|
|
## Output Format Examples
|
|
|
|
**`steps list` (text, default):**
|
|
```
|
|
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
|
|
|
|
Legend: ✓ = completed, ▶ = current, [ADDED] = new, [MODIFIED] = edited
|
|
```
|
|
|
|
**`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 "echo hello" --name "Greeting"` (text):**
|
|
```
|
|
Step added at position 6: Greeting
|
|
```
|
|
|
|
**`steps add run "echo hello" --name "Greeting" --output json`:**
|
|
```json
|
|
{
|
|
"Success": true,
|
|
"Message": "Step added at position 6",
|
|
"Result": {"index": 6, "name": "Greeting", "type": "run", "typeDetail": "echo hello", "status": "pending", "change": "ADDED"}
|
|
}
|
|
```
|