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
4 changes: 4 additions & 0 deletions exercises/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module in the [walkthrough](../docs/walkthrough.md) and maps to a ready‑made G
[`seed-issues.md`](seed-issues.md). Reference solutions are in [`solutions.md`](solutions.md) —
don't peek until you've let the agent try.

If you want to reset the demo repository after a run-through, see
[`demo-reset.md`](demo-reset.md). It preserves the seeded practice issues and removes extra
issues and pull requests.

`main` ships **green** (8 passing tests). Every exercise asks the agent to add behavior *and* the
tests that prove it.

Expand Down
36 changes: 36 additions & 0 deletions exercises/demo-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Reset the demo state

Use this helper to clean up the repository after you have finished a walkthrough or want to start over with a fresh set of practice issues and pull requests.

## What it preserves

The script preserves the four seeded exercise issues from [`seed-issues.md`](seed-issues.md) by matching their titles exactly. Everything else is treated as demo clutter and reset.

## Prerequisites

- GitHub CLI installed and authenticated (`gh auth login`)
- Run the command from the repository you want to reset

## Preview the changes

```bash
node scripts/reset-demo.mjs
```

The script runs in dry-run mode by default and prints the issues and pull requests it would close or delete.

## Apply the reset

```bash
node scripts/reset-demo.mjs --apply
```

This will:

- preserve the seeded exercise issues
- close any extra issues that are still open
- delete extra issues
- close any extra pull requests that are still open
- delete extra pull requests

> The reset is destructive for extra issues and pull requests. Review the dry-run output carefully before using `--apply`.
89 changes: 89 additions & 0 deletions scripts/reset-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env node

import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';

const args = new Set(process.argv.slice(2));
const apply = args.has('--apply');
const repoRoot = process.cwd();
const gitRoot = runGitCommand(['rev-parse', '--show-toplevel'], repoRoot).trim();
const repo = runGhCommand(['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], gitRoot).trim();
const seededTitles = readSeededIssueTitles(resolve(gitRoot, 'exercises', 'seed-issues.md'));

console.log(`Repository: ${repo}`);
console.log(`Preserving seeded issues: ${seededTitles.join(', ')}`);
console.log(apply ? 'Applying changes.' : 'Dry run only. Re-run with --apply to make changes.');

const issues = listIssues(repo);
const pullRequests = listPullRequests(repo);

const extraIssues = issues.filter((issue) => !seededTitles.includes(issue.title));
const extraPullRequests = pullRequests;

if (extraIssues.length === 0 && extraPullRequests.length === 0) {
console.log('Nothing to reset.');
process.exit(0);
}

for (const issue of extraIssues) {
const action = issue.state === 'OPEN' ? `close issue #${issue.number}` : `delete issue #${issue.number}`;
if (apply) {
if (issue.state === 'OPEN') {
runGhCommand(['issue', 'close', String(issue.number), '--repo', repo]);
}
runGhCommand(['issue', 'delete', String(issue.number), '--repo', repo, '--yes']);
}
console.log(`${apply ? 'Applied' : 'Would'} ${action}: ${issue.title}`);
}

for (const pr of extraPullRequests) {
const action = pr.state === 'OPEN' ? `close pull request #${pr.number}` : `delete pull request #${pr.number}`;
if (apply) {
if (pr.state === 'OPEN') {
runGhCommand(['pr', 'close', String(pr.number), '--repo', repo]);
}
runGhCommand(['pr', 'delete', String(pr.number), '--repo', repo, '--yes']);
}
console.log(`${apply ? 'Applied' : 'Would'} ${action}: ${pr.title}`);
}

function runGitCommand(args, cwd = repoRoot) {
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
if (result.status !== 0) {
throw new Error(result.stderr.trim() || `git ${args.join(' ')} failed`);
}
return result.stdout;
}

function runGhCommand(args, cwd = repoRoot) {
const result = spawnSync('gh', args, { cwd, encoding: 'utf8' });
if (result.status !== 0) {
throw new Error(result.stderr.trim() || `gh ${args.join(' ')} failed`);
}
return result.stdout;
}

function readSeededIssueTitles(seedFilePath) {
if (!existsSync(seedFilePath)) {
throw new Error(`Seed issues file not found: ${seedFilePath}`);
}

const content = readFileSync(seedFilePath, 'utf8');
const titles = [...content.matchAll(/--title\s+"([^"]+)"/g)].map((match) => match[1]);
if (titles.length === 0) {
throw new Error(`No seeded issue titles found in ${seedFilePath}`);
}

return titles;
}

function listIssues(repo) {
const output = runGhCommand(['issue', 'list', '--state', 'all', '--limit', '1000', '--repo', repo, '--json', 'number,title,state']);
return JSON.parse(output);
}

function listPullRequests(repo) {
const output = runGhCommand(['pr', 'list', '--state', 'all', '--limit', '1000', '--repo', repo, '--json', 'number,title,state']);
return JSON.parse(output);
}
Loading