@unifan/pi-unifan-zh 1.0.0
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 +111 -0
- package/extensions/review/CHANGELOG.md +540 -0
- package/extensions/review/LICENSE +15 -0
- package/extensions/review/README.md +55 -0
- package/extensions/review/agents/bugbot.md +46 -0
- package/extensions/review/agents/claude-md-compliance.md +46 -0
- package/extensions/review/agents/code-comments.md +43 -0
- package/extensions/review/agents/conventions.md +41 -0
- package/extensions/review/agents/gate.md +70 -0
- package/extensions/review/agents/history-context.md +45 -0
- package/extensions/review/agents/lite-review.md +51 -0
- package/extensions/review/agents/security-review.md +45 -0
- package/extensions/review/index.ts +205 -0
- package/extensions/review/package.json +38 -0
- package/extensions/review/reference/README.md +20 -0
- package/extensions/review/reference/claude-code-review.md +133 -0
- package/extensions/review/reference/cursor-review-skills.md +72 -0
- package/extensions/review/reference/pi-review-roadmap.md +183 -0
- package/extensions/review/reference/structured-output.md +26 -0
- package/extensions/review/reference/v0.2-plan.md +268 -0
- package/extensions/review/src/cli-args.ts +105 -0
- package/extensions/review/src/config.ts +340 -0
- package/extensions/review/src/directive.ts +481 -0
- package/extensions/review/src/gate-enforce.ts +151 -0
- package/extensions/review/src/lean-agents.ts +105 -0
- package/extensions/review/src/pr-ref.ts +39 -0
- package/extensions/review/src/report-tool.ts +394 -0
- package/extensions/review/src/report.ts +399 -0
- package/extensions/review/src/review-report.ts +279 -0
- package/extensions/review/src/review-run.ts +568 -0
- package/extensions/review/src/target-workspace.ts +239 -0
- package/extensions/review/src/tool-wrapper.ts +75 -0
- package/extensions/review/src/tui-renderer.ts +92 -0
- package/extensions/review/src/types.ts +207 -0
- package/extensions/review/src/workflow-schemas.ts +172 -0
- package/extensions/sessions/README.md +41 -0
- package/extensions/sessions/index.ts +862 -0
- package/extensions/sessions/package.json +21 -0
- package/extensions/sessions/sessions.ts +264 -0
- package/package.json +36 -0
- package/skills/README.md +29 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# v0.2.0 implementation plan
|
|
2
|
+
|
|
3
|
+
> **Goal:** Skills MVP — user runs `/review` locally and gets a Claude-shaped pipeline report.
|
|
4
|
+
> **Baseline:** v0.1.0 (core libs + tests; no `index.ts`, no E2E).
|
|
5
|
+
> **Design refs:** [pi-review-roadmap.md](./pi-review-roadmap.md), [claude-code-review.md](./claude-code-review.md)
|
|
6
|
+
|
|
7
|
+
Target release: `@georgedong32/pi-review@0.2.0`
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Goals
|
|
12
|
+
|
|
13
|
+
- [x] Extension loads via `pi -e .` and registers `/review`, `/review-config`, `/review-agents`
|
|
14
|
+
- [x] `/review` runs full pipeline: **eligibility → prep → reviewers → gate → report**
|
|
15
|
+
- [x] Five default content reviewers with bundled prompts wired as system prompts
|
|
16
|
+
- [~] Subagent spawn + structured JSON capture works on real `pi` child processes (unit + args tests pass; live spawn E2E pending API key)
|
|
17
|
+
- [x] Default confidence threshold **8/10** (Claude 80/100 equivalent)
|
|
18
|
+
- [ ] Publish to npm
|
|
19
|
+
|
|
20
|
+
## Non-goals (v0.2)
|
|
21
|
+
|
|
22
|
+
- GitHub / `gh` integration (`pr-context` reviewer)
|
|
23
|
+
- Per-issue Haiku scorers (`--score-per-issue`)
|
|
24
|
+
- `registerTool` / agent auto-invoke
|
|
25
|
+
- Worktree isolation, auto-fix, retry loops
|
|
26
|
+
- Custom TUI message renderer (collapsible sections)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Task list
|
|
31
|
+
|
|
32
|
+
Status key: `[ ]` todo · `[~]` in progress · `[x]` done
|
|
33
|
+
|
|
34
|
+
### Track A — Extension entry (P0)
|
|
35
|
+
|
|
36
|
+
| ID | Task | Depends | Status |
|
|
37
|
+
|----|------|---------|--------|
|
|
38
|
+
| A1 | Create `index.ts` — default export `ExtensionAPI` factory | — | [x] |
|
|
39
|
+
| A2 | Register `/review` command with arg string passthrough | A1 | [x] |
|
|
40
|
+
| A3 | Register `/review-config` — ensure config dir, open editor, re-validate on save | A1 | [x] |
|
|
41
|
+
| A4 | Register `/review-agents` — list reviewers with resolved model/thinking/tools | A1 | [x] |
|
|
42
|
+
| A5 | Implement `src/cli-args.ts` — parse flags + path/`@file` from `/review` args | A1 | [x] |
|
|
43
|
+
| A6 | Wire orchestration in `index.ts`: load config → eligibility → diff → prep → review → gate → output | A2, A5, B*, C*, D* | [x] |
|
|
44
|
+
| A7 | Output: `renderReport` via `pi.sendMessage` (assistant-visible markdown) | A6 | [x] |
|
|
45
|
+
| A8 | Output: `pi.appendEntry("pi-review", report)` machine-readable payload | A6 | [x] |
|
|
46
|
+
| A9 | Long-run UX: `ctx.ui.setStatus("pi-review", …)` during spawn phases | A6 | [x] |
|
|
47
|
+
| A10 | Add `index.ts` to `package.json` `files`; verify `tsc` includes it | A1 | [x] |
|
|
48
|
+
|
|
49
|
+
**A3 notes:** Prefer `ctx.ui.editor()` with JSON text for in-TUI edit; fall back to `pi.exec($EDITOR, [configPath])` when `ctx.hasUI` and editor env set.
|
|
50
|
+
|
|
51
|
+
**A5 flags to parse:**
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
--threshold N
|
|
55
|
+
--reviewer <id> (repeatable)
|
|
56
|
+
--no-gate
|
|
57
|
+
--gate-model <id>
|
|
58
|
+
--no-spawn
|
|
59
|
+
[path | @path]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### Track B — Structured output / spawn (P0)
|
|
65
|
+
|
|
66
|
+
| ID | Task | Depends | Status |
|
|
67
|
+
|----|------|---------|--------|
|
|
68
|
+
| B1 | Add `src/structured-output-capture.ts` — minimal pi extension: register `structured_output` tool; read schema from env; write capture file; `terminate: true` | — | [x] |
|
|
69
|
+
| B2 | Document env contract in `reference/structured-output.md` (or inline in spawn.ts header) | B1 | [x] |
|
|
70
|
+
| B3 | Update `src/args.ts` — append `-e <capture-ext>` to child args (resolve path via `import.meta.url`) | B1 | [x] |
|
|
71
|
+
| B4 | Verify child still uses `--no-session --no-extensions --no-skills` **except** capture extension | B3 | [x] |
|
|
72
|
+
| B5 | Manual E2E: one reviewer spawn writes valid `output.json` | B3, C2 | [ ] |
|
|
73
|
+
| B6 | Integration test with fake spawn (existing pattern) updated for `-e` arg | B3 | [x] |
|
|
74
|
+
|
|
75
|
+
**B1 contract (unchanged env vars):**
|
|
76
|
+
|
|
77
|
+
- `PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA` → JSON Schema file path
|
|
78
|
+
- `PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE` → output JSON path
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Track C — Pipeline phases (P0–P1)
|
|
83
|
+
|
|
84
|
+
| ID | Task | Depends | Status |
|
|
85
|
+
|----|------|---------|--------|
|
|
86
|
+
| C1 | Add `src/eligibility.ts` — `checkEligibility(input): { ok, reason? }` | — | [x] |
|
|
87
|
+
| C2 | Eligibility rules: empty diff → skip; non-git without path → skip; optional trivial diff heuristic | C1 | [x] |
|
|
88
|
+
| C3 | Add `tests/eligibility.test.ts` | C1 | [x] |
|
|
89
|
+
| C4 | Add `src/prep.ts` — `prepareContext(cwd, diff): { rulePaths, summary }` | — | [x] |
|
|
90
|
+
| C5 | Rule discovery: `AGENTS.md`, `CLAUDE.md`, `.pi/rules`, `.pi/conventions.md` (paths only, like Claude step 2) | C4 | [x] |
|
|
91
|
+
| C6 | Diff summary: first N lines / hunk count / files touched (sync, no LLM required for v0.2) | C4 | [x] |
|
|
92
|
+
| C7 | Inject prep block into reviewer task preamble in `review.ts` | C4, D6 | [x] |
|
|
93
|
+
| C8 | Add `tests/prep.test.ts` | C4 | [x] |
|
|
94
|
+
| C9 | Light re-check before render: diff still non-empty (Claude phase 6 lite) | C1, A6 | [x] |
|
|
95
|
+
|
|
96
|
+
**Trivial diff heuristic (v0.2):** only lockfile / only formatting-only / < 3 lines — configurable `skipTrivial: true` in config later; hardcode simple rules OK for v0.2.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Track D — Reviewers & prompts (P1)
|
|
101
|
+
|
|
102
|
+
| ID | Task | Depends | Status |
|
|
103
|
+
|----|------|---------|--------|
|
|
104
|
+
| D1 | Add `src/paths.ts` — `packageRoot()` via `import.meta.url` | — | [x] |
|
|
105
|
+
| D2 | `resolveAgentPrompt(id)` → `agents/<id>.md` (strip YAML frontmatter body for system prompt) | D1 | [x] |
|
|
106
|
+
| D3 | `resolveGatePrompt()` → `prompts/gate.md` | D1 | [x] |
|
|
107
|
+
| D4 | Refactor `review.ts`: use bundled agent md as `--system-prompt`; task = prep + diff | D2, C7 | [x] |
|
|
108
|
+
| D5 | Refactor `gate.ts`: use `prompts/gate.md` as gate system prompt | D3 | [x] |
|
|
109
|
+
| D6 | Rename `agents/bug-detector.md` → `agents/bugbot.md`; update id in config defaults | — | [x] |
|
|
110
|
+
| D7 | Tighten `bugbot.md` prompt per [cursor-review-skills.md](./cursor-review-skills.md) | D6 | [x] |
|
|
111
|
+
| D8 | Add `agents/security-review.md` | — | [x] |
|
|
112
|
+
| D9 | Add `agents/code-comments.md` (Claude agent #5) | — | [x] |
|
|
113
|
+
| D10 | Update `src/config.ts` `DEFAULT_CONFIG.reviewers` roster (5 enabled, conventions disabled) | D6–D9 | [x] |
|
|
114
|
+
| D11 | Update `prompts/gate.md` — embed Claude 0–100 rubric mapped to 1–10; re-score not just filter | D5 | [x] |
|
|
115
|
+
| D12 | Change default `gate.threshold` from 3 → **8** in config + README | D10 | [x] |
|
|
116
|
+
| D13 | Keep `agents/conventions.md`; `enabled: false` in defaults | D10 | [x] |
|
|
117
|
+
|
|
118
|
+
**Default enabled reviewers (v0.2):**
|
|
119
|
+
|
|
120
|
+
1. `claude-md-compliance`
|
|
121
|
+
2. `bugbot`
|
|
122
|
+
3. `history-context`
|
|
123
|
+
4. `security-review`
|
|
124
|
+
5. `code-comments`
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Track E — Types, schema, report (P1)
|
|
129
|
+
|
|
130
|
+
| ID | Task | Depends | Status |
|
|
131
|
+
|----|------|---------|--------|
|
|
132
|
+
| E1 | Extend `ResolvedInput` / report if needed for prep metadata (`rulePaths`, `summary`) | C4 | [x] |
|
|
133
|
+
| E2 | `buildReport` — handle eligibility skip / no high-confidence issues messaging | A6 | [x] |
|
|
134
|
+
| E3 | `renderReport` — “No high-confidence issues” path (Claude: no comment posted) | E2 | [x] |
|
|
135
|
+
| E4 | Update `tests/report.test.ts` for new messages | E3 | [x] |
|
|
136
|
+
| E5 | Update `tests/config.test.ts` for new defaults + bugbot id | D10 | [x] |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### Track F — Tests & docs (P1)
|
|
141
|
+
|
|
142
|
+
| ID | Task | Depends | Status |
|
|
143
|
+
|----|------|---------|--------|
|
|
144
|
+
| F1 | Add `tests/cli-args.test.ts` | A5 | [x] |
|
|
145
|
+
| F2 | Update `tests/args.test.ts` if child argv shape changes (`-e`) | B3 | [x] |
|
|
146
|
+
| F3 | Update `tests/review.test.ts` for bundled prompt resolution (mock fs or inject paths) | D4 | [x] |
|
|
147
|
+
| F4 | Update README — pipeline diagram, 5 reviewers, threshold 8, reference links | * | [x] |
|
|
148
|
+
| F5 | CHANGELOG `[0.2.0]` section | * | [x] |
|
|
149
|
+
| F6 | Bump `package.json` version → `0.2.0` | * | [x] |
|
|
150
|
+
| F7 | `bun test` + `bun run check` green | * | [x] |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### Track G — Release (P1)
|
|
155
|
+
|
|
156
|
+
| ID | Task | Depends | Status |
|
|
157
|
+
|----|------|---------|--------|
|
|
158
|
+
| G1 | Manual smoke: `pi -e .` → `/review` on dirty repo | A6, B5 | [ ] |
|
|
159
|
+
| G2 | Manual smoke: `/review --no-gate`, `--reviewer bugbot`, `--no-spawn` | A5, A6 | [ ] |
|
|
160
|
+
| G3 | Manual smoke: `/review-config` round-trip | A3 | [ ] |
|
|
161
|
+
| G4 | `npm publish` (user-triggered) | G1–G3, F7 | [ ] |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Suggested implementation order
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
Week 1 — unblock E2E
|
|
169
|
+
D1 → B1 → B3 → D2/D3 → D4/D5 → A1 → A5 → C1/C4 → A6 (no-gate path first)
|
|
170
|
+
|
|
171
|
+
Week 2 — full pipeline
|
|
172
|
+
D6–D13 → C7 → D11/D12 → A7/A8/A9 → A3/A4 → tests (F*) → docs (F4/F5)
|
|
173
|
+
|
|
174
|
+
Week 3 — polish & release
|
|
175
|
+
C2/C9 trivial + re-check → G* smoke → version bump → publish
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Critical path:** `B1 → B3 → D4 → A6 → G1`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Acceptance criteria (release gate)
|
|
183
|
+
|
|
184
|
+
1. **Load:** `pi -e /path/to/pi-review` lists `/review` in commands.
|
|
185
|
+
2. **Happy path:** Dirty git repo → `/review` → markdown report with verdict + per-reviewer sections + gate section.
|
|
186
|
+
3. **Empty:** Clean repo, no commits ahead → eligibility message, no spawn storm.
|
|
187
|
+
4. **Filter:** Reviewer returns low-confidence issue → gate drops it; report says no high-confidence issues if all dropped.
|
|
188
|
+
5. **Flags:** `--no-gate` shows raw reviewer JSON sections; `--no-spawn` prints resolved plan; `--reviewer bugbot` runs one agent.
|
|
189
|
+
6. **Config:** `/review-config` edit + save → `/review-agents` reflects changes.
|
|
190
|
+
7. **CI:** `bun test` (≥ current count, new tests for eligibility/cli-args/prep) + `tsc --noEmit` pass.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## File manifest (expected diff)
|
|
195
|
+
|
|
196
|
+
### New files
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
index.ts
|
|
200
|
+
src/cli-args.ts
|
|
201
|
+
src/eligibility.ts
|
|
202
|
+
src/prep.ts
|
|
203
|
+
src/paths.ts
|
|
204
|
+
src/structured-output-capture.ts
|
|
205
|
+
agents/bugbot.md (from bug-detector)
|
|
206
|
+
agents/security-review.md
|
|
207
|
+
agents/code-comments.md
|
|
208
|
+
tests/cli-args.test.ts
|
|
209
|
+
tests/eligibility.test.ts
|
|
210
|
+
tests/prep.test.ts
|
|
211
|
+
reference/v0.2-plan.md (this file)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Modified files
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
src/review.ts
|
|
218
|
+
src/gate.ts
|
|
219
|
+
src/args.ts
|
|
220
|
+
src/config.ts
|
|
221
|
+
src/report.ts
|
|
222
|
+
src/types.ts (if prep metadata)
|
|
223
|
+
prompts/gate.md
|
|
224
|
+
README.md
|
|
225
|
+
CHANGELOG.md
|
|
226
|
+
package.json
|
|
227
|
+
reference/README.md
|
|
228
|
+
tests/args.test.ts
|
|
229
|
+
tests/config.test.ts
|
|
230
|
+
tests/report.test.ts
|
|
231
|
+
tests/review.test.ts
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Removed / renamed
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
agents/bug-detector.md → agents/bugbot.md
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Risk register
|
|
243
|
+
|
|
244
|
+
| Risk | Mitigation |
|
|
245
|
+
|------|------------|
|
|
246
|
+
| Stock `pi` rejects `-e` with `--no-extensions` | B4: confirm CLI allows single extension override; else use minimal inline prompt + `--tools` only |
|
|
247
|
+
| Long diff exceeds argv limits | Pass `@file` temp path for task text (already noted in args.ts) |
|
|
248
|
+
| 5 reviewers × spawn cost | Document expected latency; `--reviewer` subset; concurrency cap 4 |
|
|
249
|
+
| Gate re-score drift vs per-issue scorers | Document in reference; v0.3 adds `--score-per-issue` |
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Progress summary
|
|
254
|
+
|
|
255
|
+
| Track | Tasks | Done |
|
|
256
|
+
|-------|-------|------|
|
|
257
|
+
| A Extension | 10 | 10 |
|
|
258
|
+
| B Spawn | 6 | 5 |
|
|
259
|
+
| C Pipeline | 9 | 9 |
|
|
260
|
+
| D Reviewers | 13 | 13 |
|
|
261
|
+
| E Types/report | 5 | 5 |
|
|
262
|
+
| F Tests/docs | 7 | 7 |
|
|
263
|
+
| G Release | 4 | 0 |
|
|
264
|
+
| **Total** | **54** | **49** |
|
|
265
|
+
|
|
266
|
+
**Remaining:** B5 live spawn E2E; G1–G3 manual `pi` smoke; G4 `npm publish` (user-triggered).
|
|
267
|
+
|
|
268
|
+
Update the **Progress summary** table as tasks complete.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `/review` command arguments.
|
|
3
|
+
*
|
|
4
|
+
* Surface is intentionally minimal:
|
|
5
|
+
* - `--lite` fast single-agent review (no fan-out, no gate)
|
|
6
|
+
* - `--no-spawn` hidden dry-run (prints the resolved plan, no subprocess)
|
|
7
|
+
* - trailing freeform text → user review request (focus / requirements /
|
|
8
|
+
* PR url / context), injected into reviewers and gate via target.userContext
|
|
9
|
+
* (see src/prep.ts and src/git-input.ts)
|
|
10
|
+
*
|
|
11
|
+
* Removed flags (--threshold / --reviewer / --score-per-issue / --diff) are
|
|
12
|
+
* accepted-but-ignored for graceful degradation of old invocations: a valued
|
|
13
|
+
* legacy flag also consumes its next token so its value does not leak into
|
|
14
|
+
* `input`. Those capabilities now live in config.json (`/review-config`).
|
|
15
|
+
* `--gate-model <id>` remains an active per-run override.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface ParsedReviewArgs {
|
|
19
|
+
/** Freeform user request: review focus, PR url, or context. */
|
|
20
|
+
input?: string;
|
|
21
|
+
/** Dry-run: print resolved plan without spawning. */
|
|
22
|
+
noSpawn: boolean;
|
|
23
|
+
/** Single-agent fast mode: one reviewer, no gate. */
|
|
24
|
+
lite: boolean;
|
|
25
|
+
/** Override the gate model for this run (otherwise config.gate.model). */
|
|
26
|
+
gateModel?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Removed valued flags — silently skip flag + value to keep input clean. */
|
|
30
|
+
const LEGACY_VALUED_FLAGS = new Set([
|
|
31
|
+
"--threshold",
|
|
32
|
+
"--reviewer",
|
|
33
|
+
"--score-per-issue",
|
|
34
|
+
"--diff",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
38
|
+
const tokens = tokenize(raw);
|
|
39
|
+
const result: ParsedReviewArgs = { noSpawn: false, lite: false };
|
|
40
|
+
const inputParts: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
43
|
+
const t = tokens[i];
|
|
44
|
+
if (LEGACY_VALUED_FLAGS.has(t)) {
|
|
45
|
+
i++; // consume the value too
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (t === "--no-spawn") {
|
|
49
|
+
result.noSpawn = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (t === "--lite") {
|
|
53
|
+
result.lite = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (t === "--gate-model") {
|
|
57
|
+
const id = tokens[++i];
|
|
58
|
+
if (id) result.gateModel = id;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (t.startsWith("-")) {
|
|
62
|
+
continue; // ignore other legacy / unknown flags
|
|
63
|
+
}
|
|
64
|
+
inputParts.push(t);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const input = inputParts.join(" ").trim();
|
|
68
|
+
if (input.length > 0) {
|
|
69
|
+
result.input = input;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Split on whitespace preserving quoted segments. */
|
|
76
|
+
function tokenize(raw: string): string[] {
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
let cur = "";
|
|
79
|
+
let quote: "'" | '"' | null = null;
|
|
80
|
+
for (let i = 0; i < raw.length; i++) {
|
|
81
|
+
const c = raw[i];
|
|
82
|
+
if (quote) {
|
|
83
|
+
if (c === quote) {
|
|
84
|
+
quote = null;
|
|
85
|
+
} else {
|
|
86
|
+
cur += c;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === "'" || c === '"') {
|
|
91
|
+
quote = c;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (/\s/.test(c)) {
|
|
95
|
+
if (cur.length > 0) {
|
|
96
|
+
out.push(cur);
|
|
97
|
+
cur = "";
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
cur += c;
|
|
102
|
+
}
|
|
103
|
+
if (cur.length > 0) out.push(cur);
|
|
104
|
+
return out;
|
|
105
|
+
}
|