nightralph 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -34
- package/dist/index.js +80 -22
- package/dist/index.js.map +2 -2
- package/dist/merge.js +194 -0
- package/dist/merge.js.map +7 -0
- package/dist/meta.json +189 -17
- package/dist/orchestrator.js +538 -45
- package/dist/orchestrator.js.map +2 -2
- package/dist/progress.js +164 -0
- package/dist/progress.js.map +7 -0
- package/dist/resolve.js +16 -8
- package/dist/resolve.js.map +2 -2
- package/dist/setup.js +33 -5
- package/dist/setup.js.map +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/merge.d.ts +25 -0
- package/dist/src/merge.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +29 -3
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +27 -0
- package/dist/src/progress.d.ts.map +1 -0
- package/dist/src/resolve.d.ts +2 -1
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/setup.d.ts +1 -0
- package/dist/src/setup.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +18 -0
- package/dist/src/worktree.d.ts.map +1 -0
- package/dist/worktree.js +139 -0
- package/dist/worktree.js.map +7 -0
- package/package.json +8 -4
- package/.claude/settings.local.json +0 -5
- package/.github/dependabot.yml +0 -28
- package/.github/workflows/nodejs.yml +0 -30
- package/AGENTS.md +0 -60
- package/CLAUDE.md +0 -1
- package/PROMPT.md +0 -95
- package/WIP.md +0 -5
- package/dist/test/integration.test.js +0 -848
- package/dist/test/integration.test.js.map +0 -7
- package/dist/test/orchestrator.test.js +0 -749
- package/dist/test/orchestrator.test.js.map +0 -7
- package/dist/test/setup.test.js +0 -574
- package/dist/test/setup.test.js.map +0 -7
- package/docs/README.md +0 -12
- package/docs/agents/domain.md +0 -36
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +0 -144
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +0 -292
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +0 -212
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +0 -203
- package/docs/skills.md +0 -58
- package/docs/test-plans/2026-08-27-issue-tracker-choice.md +0 -95
- package/eslint.config.js +0 -142
- package/lib.es5.d.ts +0 -19
- package/progress.log +0 -190
- package/ralph_claude.sh +0 -263
- package/scripts/clone-skills.sh +0 -52
- package/specs/prd.json +0 -172
- package/src/docs-templates/domain.md +0 -36
- package/src/docs-templates/issue-tracker-github.md +0 -228
- package/src/docs-templates/issue-tracker.md +0 -30
- package/src/docs-templates/triage-labels.md +0 -15
- package/src/index.ts +0 -184
- package/src/orchestrator.ts +0 -324
- package/src/resolve.ts +0 -73
- package/src/setup.ts +0 -157
- package/src/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/src/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/src/skills/domain-modeling/SKILL.md +0 -74
- package/src/skills/domain-modeling/agents/openai.yaml +0 -3
- package/src/skills/grill/SKILL.md +0 -7
- package/src/skills/grill/agents/openai.yaml +0 -5
- package/src/skills/grilling/SKILL.md +0 -28
- package/src/skills/grilling/agents/openai.yaml +0 -3
- package/src/skills/tdd/SKILL.md +0 -38
- package/src/skills/tdd/agents/openai.yaml +0 -3
- package/src/skills/tdd/mocking.md +0 -59
- package/src/skills/tdd/tests.md +0 -77
- package/src/skills/to-spec/SKILL.md +0 -75
- package/src/skills/to-spec/agents/openai.yaml +0 -5
- package/src/skills/to-tickets/SKILL.md +0 -105
- package/src/skills/to-tickets/agents/openai.yaml +0 -5
- package/src/skills/upstream.json +0 -5
- package/tasks/prd-integration-test-coverage.md +0 -243
- package/test/integration.test.ts +0 -636
- package/test/mock-agent-echo-args.js +0 -4
- package/test/mock-agent-fail.js +0 -3
- package/test/mock-agent-pass.js +0 -3
- package/test/orchestrator.test.ts +0 -609
- package/test/setup.test.ts +0 -417
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -31
package/README.md
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
This is a minimal Node-based AI orchestrator. It reads local markdown tickets
|
|
9
|
-
|
|
9
|
+
produced by
|
|
10
10
|
[`to-tickets` skill](https://github.com/mattpocock/skills/blob/main/skills/engineering/to-tickets/SKILL.md),
|
|
11
|
-
resolves their dependency graph, and runs agents in parallel
|
|
12
|
-
honoring
|
|
11
|
+
resolves their dependency graph, and runs agents in parallel,
|
|
12
|
+
honoring ticket dependencies. During setup you can choose a GitHub issue
|
|
13
|
+
tracker, which installs skill instructions that use `gh` CLI conventions,
|
|
14
|
+
but the ticket source is always local markdown files.
|
|
13
15
|
|
|
14
16
|
This is inspired by [nightshift](https://github.com/Shaurya-Sethi/nightshift/).
|
|
15
|
-
I made it because I noticed
|
|
17
|
+
I made it because I noticed the API in `nightshift` had already drifted
|
|
16
18
|
from the skill definitions. This module bundles its skill dependencies,
|
|
17
19
|
so they can't drift as long as you install the skills with this package.
|
|
18
20
|
(See [the section on the `setup` command](#install-dependency-skills).)
|
|
@@ -24,6 +26,12 @@ so they can't drift as long as you install the skills with this package.
|
|
|
24
26
|
- [Workflow](#workflow)
|
|
25
27
|
* [Create Issues](#create-issues)
|
|
26
28
|
* [Execute Tasks with `nightralph`](#execute-tasks-with-nightralph)
|
|
29
|
+
- [How It Works](#how-it-works)
|
|
30
|
+
* [Select a ticket](#select-a-ticket)
|
|
31
|
+
* [Worktrees](#worktrees)
|
|
32
|
+
* [Progress](#progress)
|
|
33
|
+
- [Keep Your Machine Awake](#keep-your-machine-awake)
|
|
34
|
+
* [Mac OS](#mac-os)
|
|
27
35
|
|
|
28
36
|
<!-- tocstop -->
|
|
29
37
|
|
|
@@ -35,7 +43,11 @@ npm i -S nightralph
|
|
|
35
43
|
|
|
36
44
|
### Install Dependency Skills
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
```sh
|
|
47
|
+
npx nightralph setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use the `setup` subcommand in this package to add the required
|
|
39
51
|
[Pocock skills](https://github.com/mattpocock/skills) to your repo.
|
|
40
52
|
This command will copy some files in this package to a specific directory
|
|
41
53
|
in your repo. The upstream
|
|
@@ -43,11 +55,6 @@ in your repo. The upstream
|
|
|
43
55
|
skill is exposed as `/grill`, along with its sub-skills `grilling`
|
|
44
56
|
and `domain-modeling`.
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
npx nightralph setup
|
|
49
|
-
```
|
|
50
|
-
|
|
51
58
|
The Pocock skills are bundled with this git repo, at `./skills`.
|
|
52
59
|
They are cloned with some metadata, visible in
|
|
53
60
|
[skill/upstream.json](skills/upstream.json).
|
|
@@ -56,24 +63,30 @@ That's how we prevent skill vs API drift.
|
|
|
56
63
|
|
|
57
64
|
## Workflow
|
|
58
65
|
|
|
59
|
-
First create issues.
|
|
60
|
-
|
|
61
|
-
This package bundles the skill files with the CLI, to
|
|
62
|
-
**prevent version drift**. After installing this package,
|
|
63
|
-
use [the `npx` command](#install-dependency-skills) to add the required
|
|
64
|
-
skills to your local `./claude/skills/` folder or `./agents.skills/` folder.
|
|
65
|
-
|
|
66
|
-
### Create Issues
|
|
67
|
-
|
|
68
|
-
Use the `to-tickets` skill to create issues in local markdown files.
|
|
69
|
-
|
|
70
66
|
**The expected flow is**
|
|
71
67
|
|
|
72
68
|
```
|
|
73
69
|
grill -> to-spec -> to-tickets -> tdd
|
|
74
70
|
```
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
>
|
|
73
|
+
> [!NOTE]
|
|
74
|
+
> The `grill` step can be changed to something else. Output from step 1
|
|
75
|
+
> needs to be a prose description.
|
|
76
|
+
>
|
|
77
|
+
|
|
78
|
+
First create issues.
|
|
79
|
+
|
|
80
|
+
After installing this package, use
|
|
81
|
+
[the `npx nightralph setup` command](#install-dependency-skills)
|
|
82
|
+
to add the required skills to your local `.agents/skills/` folder.
|
|
83
|
+
|
|
84
|
+
### Create Issues
|
|
85
|
+
|
|
86
|
+
Use the `to-tickets` skill to create issues in local markdown files from
|
|
87
|
+
a prose text description.
|
|
88
|
+
|
|
89
|
+
The `nightralph` CLI does the `tdd` part of the workflow.
|
|
77
90
|
|
|
78
91
|
>
|
|
79
92
|
> [!NOTE]
|
|
@@ -93,7 +106,7 @@ The `nightralph` CLI does the `tdd` part.
|
|
|
93
106
|
|
|
94
107
|
### Execute Tasks with `nightralph`
|
|
95
108
|
|
|
96
|
-
After you have the tickets,
|
|
109
|
+
After you have the tickets, use the `nightralph` loop to execute.
|
|
97
110
|
The `to-tickets` skill produces markdown files at
|
|
98
111
|
`.scratch/<feature>/issues/<NN>-<slug>.md`, with task dependency info.
|
|
99
112
|
|
|
@@ -102,28 +115,32 @@ The `to-tickets` skill produces markdown files at
|
|
|
102
115
|
npx nightralph claude -m claude-opus-4-6
|
|
103
116
|
|
|
104
117
|
# Dry run to inspect the dependency graph and prompt
|
|
105
|
-
npx nightralph
|
|
118
|
+
npx nightralph codex --dry-run
|
|
106
119
|
|
|
107
|
-
#
|
|
108
|
-
npx nightralph claude -m claude-opus-4-6
|
|
109
|
-
--dir .scratch/my-feature/issues \
|
|
110
|
-
--spec .scratch/my-feature/spec.md
|
|
120
|
+
# Specify a feature by name
|
|
121
|
+
npx nightralph claude -m claude-opus-4-6 --spec my-feature
|
|
111
122
|
```
|
|
112
123
|
|
|
124
|
+
#### `--spec <name>` option
|
|
125
|
+
|
|
126
|
+
A feature name that resolves to `.scratch/<name>/`.
|
|
127
|
+
The spec file is `.scratch/<name>/spec.md` and the issues directory
|
|
128
|
+
is `.scratch/<name>/issues/`.
|
|
129
|
+
|
|
113
130
|
Flags:
|
|
114
131
|
|
|
115
132
|
| Flag | Required | Default | Description |
|
|
116
133
|
|------|----------|---------|-------------|
|
|
117
134
|
| `<provider>` | yes | -- | Agent CLI to spawn (e.g. `claude`, `codex`) |
|
|
118
135
|
| `-m`, `--model` | no | -- | Model flag passed to the agent |
|
|
119
|
-
| `--
|
|
120
|
-
| `--spec` | no | auto | Path to the spec file |
|
|
136
|
+
| `--spec` | no | auto | Feature name (resolves `.scratch/<name>/`) |
|
|
121
137
|
| `--dry-run` | no | -- | Show wave order and prompt without running |
|
|
122
|
-
|
|
|
138
|
+
| `-X` | no | -- | Stop on merge conflict instead of re-spawning |
|
|
139
|
+
| `--timeout` | no | `300` | Kill agent after N seconds |
|
|
123
140
|
|
|
124
141
|
Notes:
|
|
125
142
|
|
|
126
|
-
* When `--
|
|
143
|
+
* When `--spec` is omitted, the CLI looks for
|
|
127
144
|
`.scratch/<feature>/issues/` directories. If multiple features exist,
|
|
128
145
|
**it prompts you to pick one** with an interactive selector.
|
|
129
146
|
* Tickets are selected by `**Status:** ready-for-agent` in the file body.
|
|
@@ -131,7 +148,69 @@ Notes:
|
|
|
131
148
|
skips tickets whose blockers haven't completed.
|
|
132
149
|
* Unblocked tickets run in parallel. After a wave finishes, newly unblocked
|
|
133
150
|
tickets start in the next wave.
|
|
134
|
-
*
|
|
135
|
-
|
|
151
|
+
* An agent run only counts as a success when the agent exits 0,
|
|
152
|
+
its worktree branch has new commits relative to the base branch,
|
|
153
|
+
and the worktree is clean (no uncommitted changes). On success,
|
|
154
|
+
the ticket's status is updated to `done` in-place. If the agent
|
|
155
|
+
exits 0 but made no commits or left uncommitted changes, the
|
|
156
|
+
ticket is left `ready-for-agent` for a retry. Failed tickets
|
|
157
|
+
(non-zero exit) keep their status and block dependents.
|
|
136
158
|
* Agent logs are written to `<dir>/../logs/<ticket>.log`.
|
|
159
|
+
* Provider commands are launched non-interactively with default flags
|
|
160
|
+
that grant the agent write access to the worktree, so tickets can
|
|
161
|
+
actually be implemented without a permission prompt:
|
|
162
|
+
* `claude`: `--print --dangerously-skip-permissions`
|
|
163
|
+
* `codex`: `exec --full-auto`
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## How It Works
|
|
167
|
+
|
|
168
|
+
All currently eligible tickets are grouped into a
|
|
169
|
+
"wave" and run in parallel. Each ticket gets a worktree named after the
|
|
170
|
+
ticket slug.
|
|
171
|
+
|
|
172
|
+
When the wave finishes, every agent that exited successfully has its ticket
|
|
173
|
+
marked `done`, which unlocks dependency tickets, which are grouped into the
|
|
174
|
+
next wave. This continues until all tickets are complete.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
### Select a ticket
|
|
178
|
+
|
|
179
|
+
* Build a ticket dependency graph with the `Blocked by:` fields
|
|
180
|
+
* A ticket is eligible for execution when its status is `ready-for-agent` and
|
|
181
|
+
every ticket listed in its `Blocked by:` field is completed.
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
### Worktrees
|
|
185
|
+
|
|
186
|
+
Each ticket gets its own git worktree. Agents work in
|
|
187
|
+
isolation so parallel tickets never conflict on disk. After a wave
|
|
188
|
+
completes, successful branches are merged back into the base branch.
|
|
189
|
+
The first branch merges directly, and subsequent branches rebase
|
|
190
|
+
onto the updated base before merging.
|
|
191
|
+
|
|
192
|
+
If a merge conflict occurs, the behavior depends on the `-X` flag: by
|
|
193
|
+
default the agent is re-spawned to resolve it; with `-X`,
|
|
194
|
+
the run stops immediately.
|
|
195
|
+
|
|
196
|
+
### Progress
|
|
197
|
+
|
|
198
|
+
A file `progress.md` is written to the `.scratch/<feature>/` directory
|
|
199
|
+
and committed after each ticket completes, so you can monitor the run from
|
|
200
|
+
another terminal by reading the file.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Keep Your Machine Awake
|
|
206
|
+
|
|
207
|
+
This is expected to run for a long time, like overnight.
|
|
208
|
+
|
|
209
|
+
### Mac OS
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
caffeinate -i npx nightralph claude -m claude-opus-4-6
|
|
213
|
+
```
|
|
137
214
|
|
|
215
|
+
That command will prevent your machine from sleeping, but the display can still
|
|
216
|
+
go dark.
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,17 @@ import { setup, checkGhAuth } from "./setup.js";
|
|
|
10
10
|
import {
|
|
11
11
|
scanTickets,
|
|
12
12
|
dryRun,
|
|
13
|
-
|
|
13
|
+
dryRunIsolated,
|
|
14
|
+
runWaves,
|
|
15
|
+
runWavesIsolated
|
|
14
16
|
} from "./orchestrator.js";
|
|
15
17
|
import {
|
|
16
18
|
findFeatureDirs,
|
|
17
|
-
resolveIssuesDir
|
|
19
|
+
resolveIssuesDir,
|
|
20
|
+
getScriptDir
|
|
18
21
|
} from "./resolve.js";
|
|
19
|
-
|
|
22
|
+
import { getRepoRoot, getRepoName } from "./worktree.js";
|
|
23
|
+
const scriptDir = getScriptDir(import.meta.url);
|
|
20
24
|
yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").command(
|
|
21
25
|
"setup",
|
|
22
26
|
"Install bundled skills and doc templates",
|
|
@@ -71,35 +75,55 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
71
75
|
alias: "model",
|
|
72
76
|
type: "string",
|
|
73
77
|
describe: "Model passed to the agent"
|
|
74
|
-
}).option("dir", {
|
|
75
|
-
type: "string",
|
|
76
|
-
describe: "Path to the issues directory"
|
|
77
78
|
}).option("spec", {
|
|
78
79
|
type: "string",
|
|
79
|
-
describe: "
|
|
80
|
+
describe: "Feature name (resolves .scratch/<name>/)"
|
|
80
81
|
}).option("dry-run", {
|
|
81
82
|
type: "boolean",
|
|
82
83
|
describe: "Show wave order without running",
|
|
83
84
|
default: false
|
|
84
|
-
}).option("timeout
|
|
85
|
+
}).option("timeout", {
|
|
85
86
|
type: "number",
|
|
86
87
|
describe: "Kill agent after N seconds",
|
|
87
88
|
default: 300
|
|
89
|
+
}).option("X", {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
describe: "Stop on merge conflict instead of re-spawning",
|
|
92
|
+
default: false
|
|
88
93
|
}),
|
|
89
94
|
async (argv) => {
|
|
90
95
|
await runExecute({
|
|
91
96
|
provider: argv.provider,
|
|
92
97
|
model: argv.model,
|
|
93
|
-
dir: argv.dir,
|
|
94
98
|
spec: argv.spec,
|
|
95
99
|
dryRun: argv.dryRun,
|
|
96
|
-
|
|
100
|
+
timeout: argv.timeout,
|
|
101
|
+
stopOnConflict: argv.X
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
).strict().demandCommand(1, "Specify a command or provider").help().parse();
|
|
105
|
+
function isGitRepo() {
|
|
106
|
+
try {
|
|
107
|
+
getRepoRoot();
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
__name(isGitRepo, "isGitRepo");
|
|
114
|
+
function extractFeatureName(spec) {
|
|
115
|
+
const parts = spec.split(
|
|
116
|
+
/[\\/]/
|
|
117
|
+
);
|
|
118
|
+
const scratchIndex = parts.indexOf(".scratch");
|
|
119
|
+
if (scratchIndex >= 0 && scratchIndex < parts.length - 1) {
|
|
120
|
+
return parts[scratchIndex + 1];
|
|
121
|
+
}
|
|
122
|
+
return parts[parts.length - 2] ?? parts[parts.length - 1] ?? "feature";
|
|
123
|
+
}
|
|
124
|
+
__name(extractFeatureName, "extractFeatureName");
|
|
100
125
|
async function runExecute(argv) {
|
|
101
126
|
const { dir, spec } = await resolveIssuesDir(
|
|
102
|
-
argv.dir,
|
|
103
127
|
argv.spec
|
|
104
128
|
);
|
|
105
129
|
if (!existsSync(dir)) {
|
|
@@ -131,20 +155,54 @@ async function runExecute(argv) {
|
|
|
131
155
|
(b) => knownNums.has(b)
|
|
132
156
|
);
|
|
133
157
|
}
|
|
158
|
+
const logsDir = join(dirname(dir), "logs");
|
|
159
|
+
const timeout = argv.timeout ?? 300;
|
|
160
|
+
const featureName = extractFeatureName(spec);
|
|
161
|
+
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
162
|
+
const progressPath = join(
|
|
163
|
+
dirname(dir),
|
|
164
|
+
"progress.md"
|
|
165
|
+
);
|
|
134
166
|
if (argv.dryRun) {
|
|
135
|
-
|
|
167
|
+
if (isGitRepo()) {
|
|
168
|
+
const repoName = getRepoName();
|
|
169
|
+
dryRunIsolated({
|
|
170
|
+
tickets,
|
|
171
|
+
specBody,
|
|
172
|
+
repoName,
|
|
173
|
+
conflictStrategy,
|
|
174
|
+
progressPath
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
dryRun(tickets, specBody);
|
|
178
|
+
}
|
|
136
179
|
return;
|
|
137
180
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
let result;
|
|
182
|
+
if (isGitRepo()) {
|
|
183
|
+
result = await runWavesIsolated({
|
|
184
|
+
tickets,
|
|
185
|
+
specBody,
|
|
186
|
+
agentCmd: argv.provider,
|
|
187
|
+
model: argv.model ?? null,
|
|
188
|
+
timeout,
|
|
189
|
+
logsDir,
|
|
190
|
+
featureName,
|
|
191
|
+
conflictStrategy
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
result = await runWaves(
|
|
195
|
+
tickets,
|
|
196
|
+
specBody,
|
|
197
|
+
argv.provider,
|
|
198
|
+
argv.model ?? null,
|
|
199
|
+
timeout,
|
|
200
|
+
logsDir
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!result.allDone) {
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
148
206
|
}
|
|
149
207
|
__name(runExecute, "runExecute");
|
|
150
208
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { select } from '@inquirer/prompts'\nimport { setup, checkGhAuth, type TrackerKind } from './setup.js'\nimport {\n scanTickets,\n dryRun,\n runWaves,\n} from './orchestrator.js'\nimport {\n findFeatureDirs,\n resolveIssuesDir,\n} from './resolve.js'\n\nexport { findFeatureDirs, resolveIssuesDir }\n\nconst scriptDir =
|
|
5
|
-
"mappings": ";;;AAEA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,YAAY;AAC9B,OAAO,WAAW;AAClB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,OAAO,mBAAqC;AACrD;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,OACG;
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { select } from '@inquirer/prompts'\nimport { setup, checkGhAuth, type TrackerKind } from './setup.js'\nimport {\n scanTickets,\n dryRun,\n dryRunIsolated,\n runWaves,\n runWavesIsolated,\n} from './orchestrator.js'\nimport {\n findFeatureDirs,\n resolveIssuesDir,\n getScriptDir,\n} from './resolve.js'\nimport { getRepoRoot, getRepoName } from './worktree.js'\n\nexport { findFeatureDirs, resolveIssuesDir }\n\nconst scriptDir = getScriptDir(import.meta.url)\n\nyargs(hideBin(process.argv))\n .scriptName('nightralph')\n .usage('$0 <command>')\n .command(\n 'setup',\n 'Install bundled skills and doc templates',\n y => y.option('claude', {\n type:'boolean',\n describe:'Also create .claude/skills/ symlinks',\n default:false,\n }),\n async argv => {\n const tracker = await select<TrackerKind>({\n message:'Issue tracker format',\n choices:[\n {\n name:'Local Markdown',\n value:'local' as const,\n description:'Issues as .md files in ' +\n '.scratch/',\n },\n {\n name:'GitHub Issues',\n value:'github' as const,\n description:'Issues via gh CLI',\n },\n ],\n })\n\n if (tracker === 'github') {\n if (!checkGhAuth()) {\n console.error(\n 'GitHub Issues requires the gh CLI, ' +\n 'authenticated.\\n' +\n 'Install: https://cli.github.com\\n' +\n 'Then run: gh auth login'\n )\n process.exit(2)\n }\n }\n\n setup({\n bundledSkillsDir:join(scriptDir, 'skills'),\n docsTemplatesDir:join(\n scriptDir, 'docs-templates'\n ),\n projectDir:process.cwd(),\n claude:argv.claude,\n tracker,\n })\n }\n )\n .command(\n '$0 <provider>',\n 'Execute tickets with the given provider',\n y => y\n .positional('provider', {\n type:'string',\n describe:'Agent CLI to spawn (e.g. claude, codex)',\n demandOption:true,\n })\n .option('m', {\n alias:'model',\n type:'string',\n describe:'Model passed to the agent',\n })\n .option('spec', {\n type:'string',\n describe:'Feature name ' +\n '(resolves .scratch/<name>/)',\n })\n .option('dry-run', {\n type:'boolean',\n describe:'Show wave order without running',\n default:false,\n })\n .option('timeout', {\n type:'number',\n describe:'Kill agent after N seconds',\n default:300,\n })\n .option('X', {\n type:'boolean',\n describe:'Stop on merge conflict instead of ' +\n 're-spawning',\n default:false,\n }),\n async argv => {\n await runExecute({\n provider:argv.provider as string,\n model:argv.model as string|undefined,\n spec:argv.spec as string|undefined,\n dryRun:argv.dryRun as boolean|undefined,\n timeout:argv.timeout as\n number|undefined,\n stopOnConflict:argv.X as\n boolean|undefined,\n })\n }\n )\n .strict()\n .demandCommand(1, 'Specify a command or provider')\n .help()\n .parse()\n\nfunction isGitRepo ():boolean {\n try {\n getRepoRoot()\n return true\n } catch {\n return false\n }\n}\n\nfunction extractFeatureName (spec:string):string {\n const parts = spec.split(\n /[\\\\/]/\n )\n const scratchIndex = parts.indexOf('.scratch')\n if (scratchIndex >= 0 && scratchIndex < parts.length - 1) {\n return parts[scratchIndex + 1]\n }\n return parts[parts.length - 2] ??\n parts[parts.length - 1] ?? 'feature'\n}\n\nasync function runExecute (argv:{\n provider:string\n model?:string\n spec?:string\n dryRun?:boolean\n timeout?:number\n stopOnConflict?:boolean\n}):Promise<void> {\n const { dir, spec } = await resolveIssuesDir(\n argv.spec\n )\n\n if (!existsSync(dir)) {\n console.error(`Directory not found: ${dir}`)\n process.exit(2)\n }\n if (!existsSync(spec)) {\n console.error(`Spec file not found: ${spec}`)\n process.exit(2)\n }\n\n const specBody = readFileSync(spec, 'utf8')\n const tickets = scanTickets(dir)\n\n if (tickets.length === 0) {\n console.log('No ticket files found. Exiting.')\n return\n }\n\n console.log(`Found ${tickets.length} tickets`)\n\n const knownNums = new Set(tickets.map(t => t.num))\n for (const t of tickets) {\n const unknown = t.blockers.filter(\n b => !knownNums.has(b)\n )\n if (unknown.length > 0) {\n console.warn(\n `${t.filename}: blockers ` +\n `[${unknown.join(', ')}] ` +\n 'not found in directory'\n )\n }\n t.blockers = t.blockers.filter(\n b => knownNums.has(b)\n )\n }\n\n const logsDir = join(dirname(dir), 'logs')\n const timeout = argv.timeout ?? 300\n const featureName = extractFeatureName(spec)\n const conflictStrategy = argv.stopOnConflict ?\n 'stop' :\n 'respawn'\n const progressPath = join(\n dirname(dir), 'progress.md'\n )\n\n if (argv.dryRun) {\n if (isGitRepo()) {\n const repoName = getRepoName()\n dryRunIsolated({\n tickets,\n specBody,\n repoName,\n conflictStrategy,\n progressPath,\n })\n } else {\n dryRun(tickets, specBody)\n }\n return\n }\n\n let result:{ allDone:boolean }\n\n if (isGitRepo()) {\n result = await runWavesIsolated({\n tickets,\n specBody,\n agentCmd:argv.provider,\n model:argv.model ?? null,\n timeout,\n logsDir,\n featureName,\n conflictStrategy,\n })\n } else {\n result = await runWaves(\n tickets, specBody,\n argv.provider, argv.model ?? null,\n timeout,\n logsDir,\n )\n }\n\n if (!result.allDone) {\n process.exit(1)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
async function mergeBranch(repoRoot, branch) {
|
|
7
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
8
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
9
|
+
}
|
|
10
|
+
if (!branch || typeof branch !== "string") {
|
|
11
|
+
throw new Error("branch must be a non-empty string");
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await execFileAsync(
|
|
15
|
+
"git",
|
|
16
|
+
["merge", branch, "--no-edit"],
|
|
17
|
+
{ cwd: repoRoot }
|
|
18
|
+
);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
try {
|
|
22
|
+
await execFileAsync(
|
|
23
|
+
"git",
|
|
24
|
+
["merge", "--abort"],
|
|
25
|
+
{ cwd: repoRoot }
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
__name(mergeBranch, "mergeBranch");
|
|
33
|
+
async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
|
|
34
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
35
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
36
|
+
}
|
|
37
|
+
if (!branch || typeof branch !== "string") {
|
|
38
|
+
throw new Error("branch must be a non-empty string");
|
|
39
|
+
}
|
|
40
|
+
if (!onto || typeof onto !== "string") {
|
|
41
|
+
throw new Error("onto must be a non-empty string");
|
|
42
|
+
}
|
|
43
|
+
if (worktreePath) {
|
|
44
|
+
try {
|
|
45
|
+
await execFileAsync(
|
|
46
|
+
"git",
|
|
47
|
+
["rebase", onto],
|
|
48
|
+
{ cwd: worktreePath }
|
|
49
|
+
);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync(
|
|
54
|
+
"git",
|
|
55
|
+
["rebase", "--abort"],
|
|
56
|
+
{ cwd: worktreePath }
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await execFileAsync(
|
|
65
|
+
"git",
|
|
66
|
+
["checkout", branch],
|
|
67
|
+
{ cwd: repoRoot }
|
|
68
|
+
);
|
|
69
|
+
await execFileAsync(
|
|
70
|
+
"git",
|
|
71
|
+
["rebase", onto],
|
|
72
|
+
{ cwd: repoRoot }
|
|
73
|
+
);
|
|
74
|
+
await execFileAsync(
|
|
75
|
+
"git",
|
|
76
|
+
["checkout", onto],
|
|
77
|
+
{ cwd: repoRoot }
|
|
78
|
+
);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
try {
|
|
82
|
+
await execFileAsync(
|
|
83
|
+
"git",
|
|
84
|
+
["rebase", "--abort"],
|
|
85
|
+
{ cwd: repoRoot }
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await execFileAsync(
|
|
91
|
+
"git",
|
|
92
|
+
["checkout", onto],
|
|
93
|
+
{ cwd: repoRoot }
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
__name(rebaseBranch, "rebaseBranch");
|
|
101
|
+
async function updateWorktreeFromBase(worktreePath, baseBranch) {
|
|
102
|
+
try {
|
|
103
|
+
await execFileAsync(
|
|
104
|
+
"git",
|
|
105
|
+
["merge", baseBranch, "--no-edit"],
|
|
106
|
+
{ cwd: worktreePath }
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
__name(updateWorktreeFromBase, "updateWorktreeFromBase");
|
|
112
|
+
async function mergeWave(opts) {
|
|
113
|
+
const { repoRoot, baseBranch, branches, strategy } = opts;
|
|
114
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
115
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
116
|
+
}
|
|
117
|
+
if (!baseBranch || typeof baseBranch !== "string") {
|
|
118
|
+
throw new Error("baseBranch must be a non-empty string");
|
|
119
|
+
}
|
|
120
|
+
const merged = [];
|
|
121
|
+
let stoppedAt = null;
|
|
122
|
+
for (let i = 0; i < branches.length; i++) {
|
|
123
|
+
const { branch, ticketNum, worktreePath } = branches[i];
|
|
124
|
+
const mergingResult = {
|
|
125
|
+
branch,
|
|
126
|
+
ticketNum,
|
|
127
|
+
merged: false,
|
|
128
|
+
conflicted: false
|
|
129
|
+
};
|
|
130
|
+
if (i === 0) {
|
|
131
|
+
const success = await mergeBranch(
|
|
132
|
+
repoRoot,
|
|
133
|
+
branch
|
|
134
|
+
);
|
|
135
|
+
mergingResult.merged = success;
|
|
136
|
+
mergingResult.conflicted = !success;
|
|
137
|
+
if (!success) {
|
|
138
|
+
if (strategy === "stop") {
|
|
139
|
+
stoppedAt = mergingResult;
|
|
140
|
+
merged.push(mergingResult);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
merged.push(mergingResult);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const rebaseSuccess = await rebaseBranch(
|
|
148
|
+
repoRoot,
|
|
149
|
+
branch,
|
|
150
|
+
baseBranch,
|
|
151
|
+
worktreePath
|
|
152
|
+
);
|
|
153
|
+
if (!rebaseSuccess) {
|
|
154
|
+
mergingResult.conflicted = true;
|
|
155
|
+
if (strategy === "stop") {
|
|
156
|
+
stoppedAt = mergingResult;
|
|
157
|
+
merged.push(mergingResult);
|
|
158
|
+
break;
|
|
159
|
+
} else {
|
|
160
|
+
merged.push(mergingResult);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const mergeSuccess = await mergeBranch(
|
|
165
|
+
repoRoot,
|
|
166
|
+
branch
|
|
167
|
+
);
|
|
168
|
+
mergingResult.merged = mergeSuccess;
|
|
169
|
+
mergingResult.conflicted = !mergeSuccess;
|
|
170
|
+
if (!mergeSuccess) {
|
|
171
|
+
if (strategy === "stop") {
|
|
172
|
+
stoppedAt = mergingResult;
|
|
173
|
+
merged.push(mergingResult);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
merged.push(mergingResult);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
merged.push(mergingResult);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
merged,
|
|
184
|
+
stoppedAt
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
__name(mergeWave, "mergeWave");
|
|
188
|
+
export {
|
|
189
|
+
mergeBranch,
|
|
190
|
+
mergeWave,
|
|
191
|
+
rebaseBranch,
|
|
192
|
+
updateWorktreeFromBase
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/merge.ts"],
|
|
4
|
+
"sourcesContent": ["import { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\n\nconst execFileAsync = promisify(execFile)\n\nexport type MergeConflictStrategy = 'respawn'|'stop'\n\nexport type MergeResult = {\n branch:string\n ticketNum:number\n merged:boolean\n conflicted:boolean\n}\n\nexport type WaveMergeResult = {\n merged:Array<MergeResult>\n stoppedAt:MergeResult|null\n}\n\nexport async function mergeBranch (\n repoRoot:string,\n branch:string\n):Promise<boolean> {\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!branch || typeof branch !== 'string') {\n throw new Error('branch must be a non-empty string')\n }\n\n try {\n await execFileAsync(\n 'git',\n ['merge', branch, '--no-edit'],\n { cwd:repoRoot }\n )\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['merge', '--abort'],\n { cwd:repoRoot }\n )\n } catch {\n // no merge in progress to abort\n }\n return false\n }\n}\n\nexport async function rebaseBranch (\n repoRoot:string,\n branch:string,\n onto:string,\n worktreePath?:string\n):Promise<boolean> {\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!branch || typeof branch !== 'string') {\n throw new Error('branch must be a non-empty string')\n }\n if (!onto || typeof onto !== 'string') {\n throw new Error('onto must be a non-empty string')\n }\n\n // When the branch has its own worktree, it is already checked\n // out there -- checking it out in repoRoot too would fail with\n // \"already used by worktree\", so rebase in place instead.\n if (worktreePath) {\n try {\n await execFileAsync(\n 'git',\n ['rebase', onto],\n { cwd:worktreePath }\n )\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['rebase', '--abort'],\n { cwd:worktreePath }\n )\n } catch {\n // ignore abort errors\n }\n return false\n }\n }\n\n try {\n await execFileAsync(\n 'git',\n ['checkout', branch],\n { cwd:repoRoot }\n )\n\n await execFileAsync(\n 'git',\n ['rebase', onto],\n { cwd:repoRoot }\n )\n\n await execFileAsync(\n 'git',\n ['checkout', onto],\n { cwd:repoRoot }\n )\n\n return true\n } catch {\n try {\n await execFileAsync(\n 'git',\n ['rebase', '--abort'],\n { cwd:repoRoot }\n )\n } catch {\n // ignore abort errors\n }\n try {\n await execFileAsync(\n 'git',\n ['checkout', onto],\n { cwd:repoRoot }\n )\n } catch {\n // ignore checkout errors\n }\n return false\n }\n}\n\n// Merges baseBranch into the worktree's branch so a\n// respawned agent sees the latest code. Conflicts are\n// left in place (not aborted) so the agent can resolve\n// and commit them.\nexport async function updateWorktreeFromBase (\n worktreePath:string,\n baseBranch:string\n):Promise<void> {\n try {\n await execFileAsync(\n 'git',\n ['merge', baseBranch, '--no-edit'],\n { cwd:worktreePath }\n )\n } catch {\n // conflict markers intentionally left for the\n // respawned agent to resolve\n }\n}\n\nexport async function mergeWave (\n opts:Readonly<{\n repoRoot:string\n baseBranch:string\n branches:ReadonlyArray<{\n branch:string\n ticketNum:number\n worktreePath?:string\n }>\n strategy:MergeConflictStrategy\n }>\n):Promise<WaveMergeResult> {\n const { repoRoot, baseBranch, branches, strategy } = opts\n\n if (!repoRoot || typeof repoRoot !== 'string') {\n throw new Error('repoRoot must be a non-empty string')\n }\n if (!baseBranch || typeof baseBranch !== 'string') {\n throw new Error('baseBranch must be a non-empty string')\n }\n\n const merged:Array<MergeResult> = []\n let stoppedAt:MergeResult|null = null\n\n for (let i = 0; i < branches.length; i++) {\n const { branch, ticketNum, worktreePath } = branches[i]\n const mergingResult:MergeResult = {\n branch,\n ticketNum,\n merged:false,\n conflicted:false,\n }\n\n if (i === 0) {\n const success = await mergeBranch(\n repoRoot,\n branch\n )\n mergingResult.merged = success\n mergingResult.conflicted = !success\n\n if (!success) {\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n }\n merged.push(mergingResult)\n continue\n }\n } else {\n const rebaseSuccess = await rebaseBranch(\n repoRoot,\n branch,\n baseBranch,\n worktreePath\n )\n\n if (!rebaseSuccess) {\n mergingResult.conflicted = true\n\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n } else {\n // strategy === 'respawn'\n merged.push(mergingResult)\n continue\n }\n }\n\n const mergeSuccess = await mergeBranch(\n repoRoot,\n branch\n )\n mergingResult.merged = mergeSuccess\n mergingResult.conflicted = !mergeSuccess\n\n if (!mergeSuccess) {\n if (strategy === 'stop') {\n stoppedAt = mergingResult\n merged.push(mergingResult)\n break\n }\n merged.push(mergingResult)\n continue\n }\n }\n\n merged.push(mergingResult)\n }\n\n return {\n merged,\n stoppedAt,\n }\n}\n"],
|
|
5
|
+
"mappings": ";;AAAA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAE1B,MAAM,gBAAgB,UAAU,QAAQ;AAgBxC,eAAsB,YAClB,UACA,QACe;AACf,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC3C,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACzD;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACvC,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACvD;AAEA,MAAI;AACA,UAAM;AAAA,MACF;AAAA,MACA,CAAC,SAAS,QAAQ,WAAW;AAAA,MAC7B,EAAE,KAAI,SAAS;AAAA,IACnB;AACA,WAAO;AAAA,EACX,QAAQ;AACJ,QAAI;AACA,YAAM;AAAA,QACF;AAAA,QACA,CAAC,SAAS,SAAS;AAAA,QACnB,EAAE,KAAI,SAAS;AAAA,MACnB;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACX;AACJ;AA9BsB;AAgCtB,eAAsB,aAClB,UACA,QACA,MACA,cACe;AACf,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC3C,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACzD;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACvC,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACvD;AACA,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACnC,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACrD;AAKA,MAAI,cAAc;AACd,QAAI;AACA,YAAM;AAAA,QACF;AAAA,QACA,CAAC,UAAU,IAAI;AAAA,QACf,EAAE,KAAI,aAAa;AAAA,MACvB;AACA,aAAO;AAAA,IACX,QAAQ;AACJ,UAAI;AACA,cAAM;AAAA,UACF;AAAA,UACA,CAAC,UAAU,SAAS;AAAA,UACpB,EAAE,KAAI,aAAa;AAAA,QACvB;AAAA,MACJ,QAAQ;AAAA,MAER;AACA,aAAO;AAAA,IACX;AAAA,EACJ;AAEA,MAAI;AACA,UAAM;AAAA,MACF;AAAA,MACA,CAAC,YAAY,MAAM;AAAA,MACnB,EAAE,KAAI,SAAS;AAAA,IACnB;AAEA,UAAM;AAAA,MACF;AAAA,MACA,CAAC,UAAU,IAAI;AAAA,MACf,EAAE,KAAI,SAAS;AAAA,IACnB;AAEA,UAAM;AAAA,MACF;AAAA,MACA,CAAC,YAAY,IAAI;AAAA,MACjB,EAAE,KAAI,SAAS;AAAA,IACnB;AAEA,WAAO;AAAA,EACX,QAAQ;AACJ,QAAI;AACA,YAAM;AAAA,QACF;AAAA,QACA,CAAC,UAAU,SAAS;AAAA,QACpB,EAAE,KAAI,SAAS;AAAA,MACnB;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,QAAI;AACA,YAAM;AAAA,QACF;AAAA,QACA,CAAC,YAAY,IAAI;AAAA,QACjB,EAAE,KAAI,SAAS;AAAA,MACnB;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACX;AACJ;AAlFsB;AAwFtB,eAAsB,uBAClB,cACA,YACY;AACZ,MAAI;AACA,UAAM;AAAA,MACF;AAAA,MACA,CAAC,SAAS,YAAY,WAAW;AAAA,MACjC,EAAE,KAAI,aAAa;AAAA,IACvB;AAAA,EACJ,QAAQ;AAAA,EAGR;AACJ;AAdsB;AAgBtB,eAAsB,UAClB,MAUuB;AACvB,QAAM,EAAE,UAAU,YAAY,UAAU,SAAS,IAAI;AAErD,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC3C,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACzD;AACA,MAAI,CAAC,cAAc,OAAO,eAAe,UAAU;AAC/C,UAAM,IAAI,MAAM,uCAAuC;AAAA,EAC3D;AAEA,QAAM,SAA4B,CAAC;AACnC,MAAI,YAA6B;AAEjC,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACtC,UAAM,EAAE,QAAQ,WAAW,aAAa,IAAI,SAAS,CAAC;AACtD,UAAM,gBAA4B;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,QAAO;AAAA,MACP,YAAW;AAAA,IACf;AAEA,QAAI,MAAM,GAAG;AACT,YAAM,UAAU,MAAM;AAAA,QAClB;AAAA,QACA;AAAA,MACJ;AACA,oBAAc,SAAS;AACvB,oBAAc,aAAa,CAAC;AAE5B,UAAI,CAAC,SAAS;AACV,YAAI,aAAa,QAAQ;AACrB,sBAAY;AACZ,iBAAO,KAAK,aAAa;AACzB;AAAA,QACJ;AACA,eAAO,KAAK,aAAa;AACzB;AAAA,MACJ;AAAA,IACJ,OAAO;AACH,YAAM,gBAAgB,MAAM;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACJ;AAEA,UAAI,CAAC,eAAe;AAChB,sBAAc,aAAa;AAE3B,YAAI,aAAa,QAAQ;AACrB,sBAAY;AACZ,iBAAO,KAAK,aAAa;AACzB;AAAA,QACJ,OAAO;AAEH,iBAAO,KAAK,aAAa;AACzB;AAAA,QACJ;AAAA,MACJ;AAEA,YAAM,eAAe,MAAM;AAAA,QACvB;AAAA,QACA;AAAA,MACJ;AACA,oBAAc,SAAS;AACvB,oBAAc,aAAa,CAAC;AAE5B,UAAI,CAAC,cAAc;AACf,YAAI,aAAa,QAAQ;AACrB,sBAAY;AACZ,iBAAO,KAAK,aAAa;AACzB;AAAA,QACJ;AACA,eAAO,KAAK,aAAa;AACzB;AAAA,MACJ;AAAA,IACJ;AAEA,WAAO,KAAK,aAAa;AAAA,EAC7B;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;AAjGsB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|