Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(dotnet:*)",
"Bash(git:*)",
"Bash(xargs grep:*)"
]
}
}
45 changes: 45 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Claude Code Review

on:
pull_request:
# No `synchronize`. That fires on every push to a PR, so a branch pushed five times gets
# five reviews of overlapping diffs. Once when it opens and once when it leaves draft is
# when a review is actually worth reading; push a fresh @claude comment for anything else.
types: [opened, ready_for_review, reopened]

jobs:
claude-review:
# Reviewable pull requests only. Three cases this cannot or should not review:
# head.repo.fork -- a FORK pull request. GitHub gives these a read-only token, no
# id-token: write and NO SECRETS, so ANTHROPIC_API_KEY arrives
# empty and the action dies on OIDC. Not fixable by permissions;
# pull_request_target would fix it by running fork code WITH the
# secrets, which is worse than no review.
# user.type -- the PR was OPENED by a bot (Copilot and friends).
# github.actor -- a human's PR that Claude then PUSHED to. The push fires
# `synchronize`, and Claude would review its own commit.
# All three are still reviewable on demand: comment @claude on the PR. That runs on
# issue_comment, which is a base-repo event and does get the secrets.
if: github.event.pull_request.head.repo.fork == false && github.event.pull_request.user.type != 'Bot' && github.actor != 'claude[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
# The Action does NOT read .claude/settings.json -- claude_args is what gates it.
claude_args: '--allowedTools "Bash(dotnet:*),Bash(gh pr edit:*),Bash(gh pr ready:*),Bash(gh pr view:*)"'
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
41 changes: 41 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
# The Action does NOT read .claude/settings.json -- claude_args is what gates it.
claude_args: '--allowedTools "Bash(dotnet:*),Bash(gh pr edit:*),Bash(gh pr ready:*),Bash(gh pr view:*)"'
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
147 changes: 143 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ CShell provides:
By maintaining the concept of a current folder all file and folder commands can be take absolute or
relative paths just like a normal shell.

CShell targets **net8.0** and depends only on
[MedallionShell](https://github.com/madelson/MedallionShell). JSON is read with the in-box
System.Text.Json.

### Properties
CShell exposes 3 properties which are the working environment of your script. The CurrentFolder is used to resolve relative paths for
most methods, so if you call **MoveFile(@"..\foo.txt", @"..\..\bar")** it will resolve the paths and execute just like a normal shell.
Expand All @@ -26,6 +30,8 @@ most methods, so if you call **MoveFile(@"..\foo.txt", @"..\..\bar")** it will r
| **FolderStack** | current stack from Push/Pop operations |
| **Echo** | Controls whether commands are echoed to output |
| **ThrowOnError** | Controls whether to throw exception when commands have non-sucess error code |
| **RichPrompts** | Whether the Ask methods use arrow keys or read a typed line. Null (the default) decides by asking whether standard input is redirected |
| **ReadKey** | Where the Ask methods get their keystrokes. Null reads the console |

### Folder Methods
CShell defines a number of methods which work relative to the current folder to make it easy
Expand Down Expand Up @@ -69,6 +75,111 @@ print("Hello world!");
error("ohoh!");
```

### Prompting the user
The **Ask**() family methods ask the user a question and return the answer.

| Method | Description |
|------------------|--------------------------------------------------------------------------------------------------|
| **AskText(question)** | read a line of text, trimmed |
| **AskSecret(question)** | read without echoing anything, for tokens and passwords |
| **AskYesNo(question)** | a yes/no question, returning bool |
| **AskYesNo(question, default)** | the same, where enter accepts the default |
| **AskNumber(question)** | a whole number |
| **AskNumber(question, min, max)** | a whole number held inside a range |
| **AskChoice(question, options, label)** | pick one from a list; returns the option itself |
| **AskChoice(question, style, options, label)** | the same, choosing how the options are labelled |
| **AskMultiChoice(question, options, label)** | pick any number of them; returns an array |
| **AskMultiChoice(question, style, options, label)** | the same, choosing how the options are labelled |

```CSharp
var name = AskText("What should I call you?");
var token = AskSecret("Paste a token:"); // nothing appears as it is typed
var retries = AskNumber("How many retries?", 1, 5);
var push = AskYesNo("Push straight to main?", false);

string[] fruits = ["apple", "banana", "cherry"];
var fruit = AskChoice("Pick a fruit:", fruits); // returns "banana", not 2
var repo = AskChoice("Pick a repo:", repos, r => r.Name); // returns the Repo itself
var extra = AskMultiChoice("Choose your toppings:", toppings);
```

* **AskChoice** and **AskMultiChoice** are generic and return the option itself, not its position.
The optional selector says what to show for each; without one they use `ToString()`.
* With a console they draw arrow-key prompts; with input redirected they read a typed line.
`RichPrompts` forces either mode, `ReadKey` supplies the keystrokes.
* An option's own text is matched before its position, so a list of `"3", "1", "2"` answers the
way it reads.
* At end of stream they throw, naming the question, rather than returning an empty answer.

`ChoiceStyle` sets the labels, and under `Letters` also what may be typed:

| Style | Renders | A typed answer may be |
|------------|--------------------|------------------------------------|
| **Auto** | nothing with arrow keys, numbers when typed | the option's text, or its number |
| **Numbers**| `1) 2) 3)` | the option's text, or its number |
| **Letters**| `a) b) c)` | the option's text, or its letter |
| **None** | nothing | the option's text only |

See **askdemo.csx** for a guided tour that shows each call and then runs it.

### Command line
**Cli** declares what a script accepts and reads the command line against it. An **Argument** is a
positional, a **Switch** is on or off, an **Option** carries a value. The same three words read
the values back.

```CSharp
var cmd = Cli.For(Args)
.Description("Opens a repository in GitHub Desktop.")
.OptionalArgument("path", "the repository to open; defaults to the current directory")
.WhatIf()
.Option("source", "the feed to use; defaults to nuget.org")
.Parse();

var path = cmd.Argument("path") ?? Directory.GetCurrentDirectory();
var source = cmd.Option("source") ?? "https://api.nuget.org/v3/index.json";
var whatIf = cmd.WhatIf;
```

| Declare on Cli | Description |
|------------------|--------------------------------------------------------------------------------------------------|
| **Argument(name, help)** | a required positional, filled in declaration order |
| **OptionalArgument(name, help)** | a positional that may be left out; reads back null |
| **Rest(name, help)** | a tail collecting everything left, verbatim |
| **Switch(name, help)** | a switch that is on or off; aliases go in the name after a pipe: `"whatif\|n"` |
| **Option(name, help)** | a switch carrying a value, written attached: `-out:file` |
| **WhatIf()** | declares the conventional dry run: `-whatif`, also `--dry-run` or `-n` |
| **Description(text)** | the paragraph shown above the usage line |
| **Example(commandLine, help)** | a worked example for the bottom of the help |
| **Program(name)** | override the name in the usage line |
| **UsageWhenEmpty()** | print the usage when run with no arguments at all |
| **Parse()** | read the command line; prints and exits if it was not valid or help was asked for |
| **TryParse()** | the same, reported through ShouldExit rather than acted on |

| Read on CliResult | Description |
|------------------|--------------------------------------------------------------------------------------------------|
| **Argument(name)** | what was given for a positional, or null when an optional one was omitted |
| **Switch(name)** | whether a switch was given |
| **Option(name)** | the value given for an option, or null |
| **WhatIf** | whether the dry-run switch was given |
| **Rest** | everything the declared Rest collected |
| **Arguments** | every positional given, in order |
| **Error** | what was wrong with the command line, or null |
| **UsageText** | the generated help, whether or not it was shown |
| **ProgramName** | the name shown in the usage line |
| **ShouldExit** | *(TryParse only)* true when the script should stop |
| **ExitCode** | *(TryParse only)* 0 for help, 1 for a command line that was not valid |

* Anything undeclared is an error. Bare words are positionals, not unknown switches.
* Values attach: `-out:file` or `-out=file`, never `-out file`. Only the name is normalized, so
`-source:https://api.nuget.org/v3/index.json` arrives intact.
* `-whatif`, `--whatif` and `--what-if` are one switch -- dashes come off, inner hyphens and
underscores go, case is ignored. `/` is not a prefix.
* `-help`, `-h` and `-?` work without being declared, and the help is generated from the
declarations. The program name comes from the calling script's file name.
* `Parse()` exits on a bad command line, so what it returns is always usable. `TryParse()` reports
through `ShouldExit`/`ExitCode` instead, and every other value on it throws until you check.
* No subcommands, repeated options, typed binding, or separated values. For more complex cli support use something like [System.CommandLine](https://www.nuget.org/packages/System.CommandLine) directly.

### Process Methods
CShell is built using [MedallionShell](https://github.com/madelson/MedallionShell), which provides a great set of functionality for easily invoking
processes and piping data between them. CShell adds on location awareness and helper methods
Expand Down Expand Up @@ -112,7 +223,7 @@ CShell adds on helper methods to make it even easier to work with the result of
|------------------|------------------------------------------------------------------------------|
| **Execute(log)** | get the CommandResult (with stdout/stderr) of the last command |
| **AsString(log)** | get the standard out of the last command a string |
| **AsJson(log)** | JSON Deserialize the standard out of the last command into a JObject/dynamic |
| **AsJson(log)** | Parse the standard out of the last command as JSON: `json.owner.login`, `json["owner"]`, or assign it to a JsonObject |
| **AsJson\<T>(log)** | JSON Deserialize the standard out of the last command into a typed T |
| **AsXml\<T>(log)** | XML Deserialize the standard out of the last command intoa typed T |
| **AsFile()** | Write the stdout/stderr of the last command to a file |
Expand All @@ -123,6 +234,21 @@ To call a program you await on:
2. call any chaining commands
3. end with a result call like Execute()/AsJson()/AsString()/AsXml()etc.

`AsJson()` gives you a **JsonDynamic**, which reads whichever way suits the script:

```CSharp
var json = await Cmd("gh api repos/tomlm/CShell").AsJson();

Console.WriteLine(json.owner.login); // walk it with a dot
Console.WriteLine(json["stargazers_count"]);
foreach (var topic in json.topics) { } // arrays enumerate

JsonObject o = await Cmd("gh api repos/tomlm/CShell").AsJson(); // or take the typed API
```

A property that is not there reads as null, so `if (json.optional != null)` is how you test for
one. Use `AsJson<T>()` where the shape is known.

The result methods all take a log argument is passed set to true then the commands output will be written to standard out.

```CSharp
Expand Down Expand Up @@ -169,7 +295,7 @@ To invoke the template
> NOTE: If you want debug support from visual studio code simply run **dotnet script init** in the same folder.

```csharp
#r "nuget: CShell, 2.1.0"
#r "nuget: CShell, 3.0.0"
global using static CShellNet.Globals;
using CShellNet;

Expand Down Expand Up @@ -206,7 +332,7 @@ On Linux/Mac you can make a .csx file executable by

```bash
#!/usr/bin/env dotnet-script
#r "nuget: CShell, 1.5.0"
#r "nuget: CShell, 3.0.0"
global using static CShellNet.Globals;
using CShellNet;
```
Expand Down Expand Up @@ -235,9 +361,22 @@ chmod +x example.csx
```

## CHANGELOG
### v3.0.0
* Added **Cli**, a declarative command line parser with generated --help
* Argument()/OptionalArgument()/Rest() for positionals, Switch() for booleans, Option() for attached values
* anything undeclared is an error; Parse() exits on a bad command line, TryParse() reports instead
* Added the **Ask** methods: AskText, AskSecret, AskYesNo, AskNumber, AskChoice, AskMultiChoice
* AskChoice/AskMultiChoice are generic and return the option itself rather than its position
* each has an arrow-key mode and a typed-line mode, chosen from whether input is redirected
* Added **RichPrompts** and **ReadKey** to control how the Ask methods read input
* **BREAKING** now targets net8.0 rather than netstandard2.0. .NET Framework is no longer supported
* **BREAKING** replaced Newtonsoft.Json with System.Text.Json; MedallionShell is now the only dependency
* `AsJson()` returns a **JsonDynamic** as `dynamic`, so `json.owner.login` still works. It also indexes, enumerates, and converts to JsonNode/JsonObject/JsonArray. `AsJson<T>()` is unchanged
* property names still match case insensitively; trailing commas, comments and a byte order mark are tolerated

### v2.1.0
* Added Write/WriteLine/print/error methods for writing to standard out and standard error

### V1.5.2
* Added Exists() methods to global

Expand Down
Loading
Loading