@dzhechkov/p-replicator 1.5.18 → 1.6.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/.dz-manifest.json +126 -46
- package/CHANGELOG.md +135 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +6 -2
- package/README/eng/README.md +1 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +36 -15
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +245 -45
- package/src/utils.js +2 -0
- package/templates/.claude/agents/doc-validator.md +2 -1
- package/templates/.claude/agents/product-discoverer.md +1 -1
- package/templates/.claude/commands/next.md +16 -0
- package/templates/.claude/commands/replicate.md +126 -8
- package/templates/.claude/commands/start.md +19 -1
- package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
- package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
- package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
- package/templates/.claude/hooks/check-ports.cjs +232 -0
- package/templates/.claude/hooks/session-insights.cjs +13 -1
- package/templates/.claude/hooks/state-update.cjs +13 -1
- package/templates/.claude/hooks/statusline.cjs +145 -18
- package/templates/.claude/rules/docker-ports.md +123 -0
- package/templates/.claude/rules/replicate-pipeline.md +5 -2
- package/templates/.claude/settings.json +5 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
- package/templates/.claude/skills/requirements-validator/SKILL.md +7 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -0
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +143 -1
- package/tests/e2e/lifecycle.test.js +21 -10
- package/tests/snapshot/baseline.json +29 -27
- package/tests/snapshot/update-baseline.js +2 -1
- package/tests/unit/adr-decision-coverage.test.js +137 -0
- package/tests/unit/adr-scanner-contract.test.js +108 -0
- package/tests/unit/autocommit-deletion.test.js +242 -0
- package/tests/unit/check-ports.test.js +184 -0
- package/tests/unit/db-port-rule.test.js +216 -0
- package/tests/unit/detect-parse-anchor.test.js +109 -0
- package/tests/unit/external-dependency-check.test.js +209 -0
- package/tests/unit/growth-module-b2b-gate.test.js +104 -0
- package/tests/unit/hooks-project-anchored.test.js +223 -0
- package/tests/unit/hooks-report-failures.test.js +207 -0
- package/tests/unit/pipeline-file-ownership.test.js +95 -0
- package/tests/unit/roadmap-one-schema.test.js +179 -0
- package/tests/unit/sparc-reconciliation.test.js +117 -0
- package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
- package/tests/unit/statusline-honest-labels.test.js +178 -0
- package/tests/unit/statusline-two-roots.test.js +237 -0
- package/tests/unit/sync-templates-guard.test.js +209 -0
- package/tests/unit/utils.test.js +2 -2
- package/tests/unit/validation-gate-teeth.test.js +158 -0
- package/LICENSE +0 -21
|
@@ -166,7 +166,20 @@ Read the skill: `.claude/skills/reverse-engineering-unicorn/SKILL.md`
|
|
|
166
166
|
| M2: Product & Customers | Always | JTBD, Value Prop, segments |
|
|
167
167
|
| M3: Market & Competition | Always | TAM/SAM, competitors, Blue Ocean |
|
|
168
168
|
| M4: Business & Finance | If monetization | Unit economics |
|
|
169
|
-
| M5: Growth Engine | If
|
|
169
|
+
| M5: Growth Engine | If acquisition/adoption in scope (incl. B2B) | Channels, integrations |
|
|
170
|
+
|
|
171
|
+
M5 is no longer gated on PRODUCT TYPE. The module branches on type itself — *«Если B2B → sales-led
|
|
172
|
+
growth, не product-led»* — so gating it outside disabled the one branch it declares, and every B2B
|
|
173
|
+
project got an empty `### Growth Channels` slot.
|
|
174
|
+
|
|
175
|
+
The condition is APPLICABILITY, not type, and the difference matters in both directions: `Always`
|
|
176
|
+
would over-promise, because M5's outputs are CAC, channels and loops, and an internal tool with no
|
|
177
|
+
acquisition objective has nothing to put in them. So: run it whenever acquisition or adoption is in
|
|
178
|
+
scope — which includes B2B — and skip it when neither is. The cost is visible rather than
|
|
179
|
+
discovered: a B2B run now spends M5's time.
|
|
180
|
+
|
|
181
|
+
Honest limit: line 62 of the module says to *switch* to a sales-led framework; it does not define
|
|
182
|
+
one. A B2B run gets the type-appropriate instruction, not a type-appropriate playbook.
|
|
170
183
|
|
|
171
184
|
**Output:** Product Discovery Brief → passed as pre-filled context to Phase 1
|
|
172
185
|
|
|
@@ -255,6 +268,10 @@ Read the skill: `.claude/skills/requirements-validator/SKILL.md`
|
|
|
255
268
|
| `validator-architecture` | Architecture.md | Target constraints, completeness |
|
|
256
269
|
| `validator-pseudocode` | Pseudocode.md | Story coverage, implementability |
|
|
257
270
|
| `validator-coherence` | Cross-document | Consistency, no contradictions |
|
|
271
|
+
| `validator-dependencies` | `Architecture.md` → `## External Dependencies` | Every external capability a requirement relies on has a verdict and, where CONFIRMED, evidence that names that capability |
|
|
272
|
+
|
|
273
|
+
The sixth lens is the only one that looks OUTSIDE the documents. The other five compare our own
|
|
274
|
+
output with our own output, which cannot discover that a service does not do what we assumed.
|
|
258
275
|
|
|
259
276
|
**Process (iterative, max 3 iterations):**
|
|
260
277
|
|
|
@@ -270,7 +287,16 @@ Read the skill: `.claude/skills/requirements-validator/SKILL.md`
|
|
|
270
287
|
- Happy path (1-2), Error handling (2-3), Edge cases (1-2), Security
|
|
271
288
|
- Save as `docs/test-scenarios.md`
|
|
272
289
|
|
|
273
|
-
**Save validation report:** `docs/validation-report.md
|
|
290
|
+
**Save validation report:** `docs/validation-report.md`. Its **first line** must be exactly one of
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
**Verdict:** 🟢 READY
|
|
294
|
+
**Verdict:** 🟡 CAVEATS
|
|
295
|
+
**Verdict:** 🔴 NEEDS WORK
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
and no other line in the file may begin with `**Verdict:**`. Phase 3 reads that one line and nothing
|
|
299
|
+
else — an unanchored verdict is a verdict Phase 3 can find in an example or a quoted history.
|
|
274
300
|
|
|
275
301
|
Git commit: `docs: validation report and BDD scenarios`
|
|
276
302
|
|
|
@@ -278,9 +304,85 @@ Git commit: `docs: validation report and BDD scenarios`
|
|
|
278
304
|
|
|
279
305
|
| Verdict | Conditions | Action |
|
|
280
306
|
|---------|-----------|--------|
|
|
281
|
-
| 🟢 READY | All scores ≥50, average ≥70, no contradictions | → Phase 3 |
|
|
282
|
-
| 🟡 CAVEATS | Warnings exist, no blocked, limitations described | → Phase 3 with notes |
|
|
283
|
-
| 🔴 NEEDS WORK | Blocked items exist | → Return to Phase 1 |
|
|
307
|
+
| 🟢 READY | All scores ≥50, average ≥70, no contradictions, **no item on the blocking floor**, **no external dependency `UNCONFIRMED` or `CONTRADICTED`** | → Phase 3 |
|
|
308
|
+
| 🟡 CAVEATS | Warnings exist, no blocked, limitations described, **every `UNCONFIRMED` dependency NAMED row by row** | → Phase 3 with notes |
|
|
309
|
+
| 🔴 NEEDS WORK | Blocked items exist, **or any item has `Testable = 0` or `Completeness = 0`**, **or any external dependency is `CONTRADICTED`** | → Return to Phase 1 |
|
|
310
|
+
|
|
311
|
+
**The blocking floor** (`skills/requirements-validator/references/scoring-system.md` → "Blocking
|
|
312
|
+
floor"): the weakest link decides, never the average. An item with no acceptance criteria totals
|
|
313
|
+
72/100 and would otherwise read as READY.
|
|
314
|
+
|
|
315
|
+
**Шаг 2.9 — ПОКРЫТИЕ РЕШЕНИЙ (обязательный, до чекпойнта).**
|
|
316
|
+
|
|
317
|
+
Every decision in `docs/ADR.md` carries an id `ADR-<nnn>` — three digits, assigned in order, never
|
|
318
|
+
reused even after a decision is superseded.
|
|
319
|
+
|
|
320
|
+
**Where to look, named file by file.** «Across the docs» is not an instruction. Search EXACTLY these,
|
|
321
|
+
and no others:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
docs/PRD.md · docs/Solution_Strategy.md · docs/Specification.md · docs/Pseudocode.md
|
|
325
|
+
docs/Architecture.md · docs/Refinement.md · docs/Completion.md · docs/C4_Diagrams.md
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Two files are EXCLUDED, and the first exclusion is the one that makes this check work at all:**
|
|
329
|
+
|
|
330
|
+
- `docs/ADR.md` itself. Its own headings contain every id, so counting them would make every decision
|
|
331
|
+
appear named and the check would pass by construction — always, on any project.
|
|
332
|
+
- `docs/validation-report.md`. This step WRITES into it. Counting it would let the previous run's
|
|
333
|
+
output satisfy the next run: the check would start proving itself.
|
|
334
|
+
|
|
335
|
+
**What counts as a mention.** The exact token `ADR-<nnn>`, case-sensitive, in one of the files above.
|
|
336
|
+
Not a title, not a paraphrase, not a link whose text merely resembles it. One occurrence is enough;
|
|
337
|
+
repeats are not counted twice.
|
|
338
|
+
|
|
339
|
+
**A superseded decision needs no current mention.** If a decision's own entry says it is superseded,
|
|
340
|
+
list it in a third column rather than as a gap — it was replaced, not forgotten.
|
|
341
|
+
|
|
342
|
+
Write a `## Decision Coverage` block into `docs/validation-report.md` — **in every case**, including
|
|
343
|
+
the one where everything is covered, because an absent block and a block saying "all covered" are
|
|
344
|
+
indistinguishable to the next reader:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
## Decision Coverage
|
|
348
|
+
|
|
349
|
+
Decisions in docs/ADR.md: [N] · named downstream: [M] · superseded: [S]
|
|
350
|
+
|
|
351
|
+
Recorded but named nowhere:
|
|
352
|
+
| Decision | Title |
|
|
353
|
+
|---|---|
|
|
354
|
+
| ADR-… | … |
|
|
355
|
+
|
|
356
|
+
Named downstream but absent from docs/ADR.md:
|
|
357
|
+
| Reference | Where |
|
|
358
|
+
|---|---|
|
|
359
|
+
| ADR-… | docs/… |
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Both tables are required, and both may be the single word `none`.** A one-way check is half a
|
|
363
|
+
check: without the second table, a document referring to `ADR-009` that nobody ever wrote reads
|
|
364
|
+
exactly like coverage. `none` is written out rather than left blank, because an empty table and a
|
|
365
|
+
forgotten table look identical.
|
|
366
|
+
|
|
367
|
+
**Three states of the ADR file, and each has its own line — the block is written in all three:**
|
|
368
|
+
|
|
369
|
+
| State | What to write in the block |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `docs/ADR.md` absent | *"docs/ADR.md is absent, so no decision ids were collected from it. The second table below still applies."* |
|
|
372
|
+
| present but containing no `ADR-<nnn>` id | *"docs/ADR.md exists but records no decision ids."* |
|
|
373
|
+
| present with ids | the counts and the two tables above |
|
|
374
|
+
|
|
375
|
+
Note what the first line does NOT say. It says the FILE is absent — not that the project recorded no
|
|
376
|
+
architectural decisions. Decisions may live somewhere this pipeline does not look, and claiming
|
|
377
|
+
otherwise would be asserting something this step cannot see. **In all three states the second table
|
|
378
|
+
still runs**: a downstream reference to a decision that does not exist is a defect whether or not an
|
|
379
|
+
ADR file was ever written.
|
|
380
|
+
|
|
381
|
+
**What this establishes, and what it does not.** It establishes that a decision is NAMED somewhere
|
|
382
|
+
downstream. It does NOT establish that the decision was implemented — no comparison of identifiers
|
|
383
|
+
can. So it catches *"the decision was written down and then forgotten"*; it does not catch *"someone
|
|
384
|
+
mentioned it in a sentence and built something else"*. Say so here rather than letting a later reader
|
|
385
|
+
assume the stronger thing.
|
|
284
386
|
|
|
285
387
|
**Checkpoint:**
|
|
286
388
|
```
|
|
@@ -295,6 +397,18 @@ Iterations: N/3
|
|
|
295
397
|
|
|
296
398
|
### Phase 3: TOOLKIT GENERATION
|
|
297
399
|
|
|
400
|
+
**Precondition — check it before reading anything else. The toolkit is built ON the validated docs,
|
|
401
|
+
so an unvalidated input is not a smaller toolkit, it is a wrong one:**
|
|
402
|
+
|
|
403
|
+
1. `docs/validation-report.md` must EXIST. If it is absent, Phase 2 did not run or did not finish —
|
|
404
|
+
do NOT generate anything; return to Phase 2 and say so.
|
|
405
|
+
2. Its **first line** must be `**Verdict:**` followed by 🟢 READY or 🟡 CAVEATS. Read ONLY that
|
|
406
|
+
line: a verdict word anywhere else in the document — an example, a quoted history, a summary —
|
|
407
|
+
is NOT the verdict. On 🔴 NEEDS WORK, on no such first line, or on more than one line starting
|
|
408
|
+
with `**Verdict:**`, do NOT generate anything; return to Phase 2 and say which of the four it was.
|
|
409
|
+
3. On 🟡, carry the report's stated limitations into the toolkit's own notes — a caveat that stops at
|
|
410
|
+
the phase boundary was never recorded.
|
|
411
|
+
|
|
298
412
|
Read the skill: `.claude/skills/cc-toolkit-generator-enhanced/SKILL.md`
|
|
299
413
|
|
|
300
414
|
**Goal:** Generate project-specific Claude Code instruments IN-PLACE.
|
|
@@ -305,7 +419,7 @@ Read the skill: `.claude/skills/cc-toolkit-generator-enhanced/SKILL.md`
|
|
|
305
419
|
- **Pre-shipped by `npx p-replicator init` — do NOT overwrite or regenerate:**
|
|
306
420
|
- All 10 skills in `.claude/skills/`
|
|
307
421
|
- All 11 commands: `/replicate`, `/harvest`, `/start`, `/plan`, `/feature`, `/go`, `/run`, `/next`, `/myinsights`, `/docs`, `/deploy`
|
|
308
|
-
- All
|
|
422
|
+
- All 6 rules: `replicate-pipeline`, `skill-interface-protocol`, `git-workflow`, `insights-capture`, `feature-lifecycle`, `docker-ports`
|
|
309
423
|
- All 4 pipeline agents: `replicate-coordinator`, `product-discoverer`, `doc-validator`, `harvest-coordinator`
|
|
310
424
|
- `.claude/settings.json` + cross-platform Node hook scripts in `.claude/hooks/`
|
|
311
425
|
- Phase 3 generates ONLY project-specific artifacts derived from SPARC docs (see below).
|
|
@@ -369,11 +483,15 @@ Git commit: `feat: Claude Code toolkit for [project-name]`
|
|
|
369
483
|
|
|
370
484
|
**Generate scaffold files:**
|
|
371
485
|
|
|
372
|
-
1. `docker-compose.yml` — from Architecture.md services
|
|
486
|
+
1. `docker-compose.yml` — from Architecture.md services, **if not exists** (a run over a tree that already has one must not discard it either — the guard is symmetric)
|
|
373
487
|
2. `Dockerfile` — from Architecture.md tech stack
|
|
374
488
|
3. `.gitignore` — if not exists
|
|
375
489
|
4. `docs/features/` — create empty directory for future features
|
|
376
490
|
|
|
491
|
+
> `docker-compose.yml`, `.gitignore` и `README.md` позже читает `/start` (Phase 1). Он обязан их СОХРАНИТЬ и
|
|
492
|
+
> менять только по названной причине — правило `if not exists` записано на его стороне. Оба конца
|
|
493
|
+
> рекомендованной последовательности договорены в тексте, а не совпадают по случайности.
|
|
494
|
+
|
|
377
495
|
**Git operations:**
|
|
378
496
|
```bash
|
|
379
497
|
git add .
|
|
@@ -467,7 +585,7 @@ PRINCIPLE: User enters keys via UI → stored encrypted in browser → NEVER sen
|
|
|
467
585
|
|
|
468
586
|
### NEVER
|
|
469
587
|
- Don't duplicate explore/research phases — sparc-prd-mini does this internally
|
|
470
|
-
- Never skip validation — toolkit is built on validated docs
|
|
588
|
+
- Never skip validation — toolkit is built on validated docs. The enforcing check is Phase 3's precondition, which reads the first `**Verdict:**` line of `docs/validation-report.md`
|
|
471
589
|
- Never use base cc-toolkit-generator — only enhanced version
|
|
472
590
|
- Don't overwrite template files (generic commands, rules, settings.json)
|
|
473
591
|
|
|
@@ -33,7 +33,25 @@ via `Task` tool, commits per logical change for safe error recovery.
|
|
|
33
33
|
2. **Generate root configs:** `package.json` (monorepo workspaces),
|
|
34
34
|
`docker-compose.yml`, `.env.example`, `.gitignore`, `tsconfig.base.json`.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
**Два из них `/replicate` уже мог записать.** Штатная последовательность — `/replicate`, затем
|
|
37
|
+
`/start`, а Phase 4 команды `/replicate` создаёт `docker-compose.yml` и `.gitignore` из того же
|
|
38
|
+
`Architecture.md`. Значит это не два разных файла, а один, выведенный дважды, и слепая
|
|
39
|
+
перегенерация молча выбросит всё, что между двумя запусками правили руками.
|
|
40
|
+
|
|
41
|
+
Для `docker-compose.yml`, `.gitignore` и `README.md` действует правило `if not exists`:
|
|
42
|
+
- файла НЕТ → создать, как обычно;
|
|
43
|
+
- файл ЕСТЬ → **не перегенерировать**. Прочитать его и оставить; внести только МИНИМАЛЬНУЮ
|
|
44
|
+
точечную правку, которой требует названное требование из документов (появился пакет, которого
|
|
45
|
+
в файле нет; порт из `Architecture.md` не проброшен). Всё, что к этому требованию не относится,
|
|
46
|
+
сохраняется без изменений.
|
|
47
|
+
- Перед коммитом ПОСМОТРЕТЬ ДИФ и НАЗВАТЬ каждый изменённый фрагмент и его причину. «Добавил
|
|
48
|
+
недостающий порт», под которым лежит переписанный целиком файл, — это и есть та потеря,
|
|
49
|
+
которую правило предотвращает; диф отличает одно от другого, а обещание не отличает.
|
|
50
|
+
|
|
51
|
+
3. **Git commit:** `chore: project root configuration`. Если на шаге 2 менялся уже существовавший
|
|
52
|
+
`docker-compose.yml`, `.gitignore` или `README.md` — тело коммита перечисляет каждый изменённый
|
|
53
|
+
фрагмент и причину. Фиксированный заголовок не отменяет этого перечня: заголовок говорит, ЧТО за
|
|
54
|
+
шаг, тело — что именно он тронул в чужом файле.
|
|
37
55
|
|
|
38
56
|
### Phase 2: Packages (PARALLEL via Task tool ⚡)
|
|
39
57
|
|
|
@@ -10,30 +10,115 @@ const fs = require('node:fs');
|
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// The project root, never the process cwd: a `cd` inside any tool call moves cwd for the rest of
|
|
14
|
+
// the session, and these hooks are non-blocking, so a wrong anchor fails SILENTLY. CLAUDE_PROJECT_DIR
|
|
15
|
+
// first — the host is authoritative about what the project is. `__dirname` second: a hook always
|
|
16
|
+
// lives at <project>/.claude/hooks/<x>.cjs, so its own location settles the root with no cooperation
|
|
17
|
+
// from anyone, which is what keeps this working when the variable is absent (hand-run, older host).
|
|
18
|
+
const ENV_ROOT = process.env.CLAUDE_PROJECT_DIR;
|
|
19
|
+
// isAbsolute, not just truthy: a RELATIVE value would still be resolved against the drifting
|
|
20
|
+
// cwd, which is the very bug this anchor exists to remove.
|
|
21
|
+
const ROOT = (ENV_ROOT && path.isAbsolute(ENV_ROOT))
|
|
22
|
+
? ENV_ROOT
|
|
23
|
+
: path.resolve(__dirname, '..', '..');
|
|
24
|
+
|
|
25
|
+
const TARGET_DIR = path.resolve(ROOT, '.claude', 'insights');
|
|
26
|
+
const RELATIVE = path.relative(ROOT, TARGET_DIR);
|
|
27
|
+
// cwd: ROOT — the paths below are relative to ROOT, so git must run there too.
|
|
28
|
+
// stderr is PIPED, not ignored: on success nothing is printed anyway, and on failure git's
|
|
29
|
+
// own words are the only thing that tells a reader WHY. Discarding them leaves a report that
|
|
30
|
+
// names what failed and not why, which is half a report.
|
|
31
|
+
const SILENT = { stdio: ['ignore', 'pipe', 'pipe'], cwd: ROOT };
|
|
32
|
+
|
|
33
|
+
// What the hook was DOING when it failed. The outer catch also sees staging failures and a
|
|
34
|
+
// missing git binary, and reporting either of those as "could not commit" would send the reader
|
|
35
|
+
// looking in the wrong place.
|
|
36
|
+
let stage = 'start';
|
|
16
37
|
|
|
17
38
|
function git(args) {
|
|
18
39
|
return execFileSync('git', args, SILENT);
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
// The repository probe comes FIRST now, because the absence check below needs to ask git a
|
|
44
|
+
// question and there is no point asking outside a repository.
|
|
45
|
+
stage = 'inspect the repository';
|
|
46
|
+
try {
|
|
47
|
+
git(['rev-parse', '--git-dir']);
|
|
48
|
+
} catch (probeErr) {
|
|
49
|
+
// "Not a git repository" is the ordinary case and stays silent. Everything else — no git on
|
|
50
|
+
// PATH, dubious ownership, a permission error — is a real failure that used to look exactly
|
|
51
|
+
// like it, which is how a broken machine and an ordinary directory became indistinguishable.
|
|
52
|
+
const why = String((probeErr && probeErr.stderr) || '').trim();
|
|
53
|
+
if (!why || /not a git repository/i.test(why)) process.exit(0);
|
|
54
|
+
throw probeErr;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Absent — but absent HOW? A path that never existed is nothing to do, as before. A path git
|
|
58
|
+
// still tracks is a DELETION, and a deletion is exactly the change this hook exists to record.
|
|
59
|
+
//
|
|
60
|
+
// `git ls-files` is the discriminator, and it must run BEFORE staging: staging a deletion
|
|
61
|
+
// removes the entry from the index, after which the same question answers "not tracked".
|
|
62
|
+
// CORRECTION, measured: `--error-unmatch` also discriminates correctly here, for files AND for
|
|
63
|
+
// directories — 0 for a deleted-but-tracked path, 1 for one that never existed. An earlier note
|
|
64
|
+
// here claimed otherwise; that claim came from measuring it AFTER staging, which is what made it
|
|
65
|
+
// fail, not the directory-ness. `ls-files --` is preferred only because it answers with DATA
|
|
66
|
+
// (empty or not) instead of by throwing, so the ordinary case needs no exception handling.
|
|
67
|
+
//
|
|
68
|
+
// Simply DELETING the existence guard would have been the naive fix and would have broken the
|
|
69
|
+
// reporting that shipped yesterday: `git add` on a path that never existed exits 128, so every
|
|
70
|
+
// session stop in every project without this artifact would print a failure line. MEASURED.
|
|
71
|
+
if (!fs.existsSync(TARGET_DIR)) {
|
|
72
|
+
// A bare `catch { tracked = '' }` here would turn a REAL git failure into "never existed"
|
|
73
|
+
// and skip a deletion that should have been recorded — a silent bypass of the very thing
|
|
74
|
+
// this feature adds. A failure is rethrown into the reporting path; only a genuinely
|
|
75
|
+
// empty answer means the artifact never lived here.
|
|
76
|
+
const tracked = String(git(['ls-files', '--', RELATIVE]) || '').trim();
|
|
77
|
+
if (!tracked) process.exit(0); // never existed here — silent, exactly as before
|
|
78
|
+
}
|
|
24
79
|
|
|
80
|
+
stage = 'stage the change';
|
|
25
81
|
git(['add', '--', RELATIVE]);
|
|
26
82
|
|
|
27
|
-
|
|
83
|
+
// Ask git WHAT it staged, not merely WHETHER something changed — same subprocess, strictly
|
|
84
|
+
// more information. Deriving "this is a removal" from whether the path still exists was
|
|
85
|
+
// wrong for a directory: deleting ONE file inside it, or deleting every tracked file while
|
|
86
|
+
// an ignored one keeps the directory present, would have been committed as an update and
|
|
87
|
+
// defeated the very search this feature promises.
|
|
88
|
+
let staged = '';
|
|
28
89
|
try {
|
|
29
|
-
git(['diff', '--cached', '--
|
|
90
|
+
staged = String(git(['diff', '--cached', '--name-status', '--', RELATIVE]) || '').trim();
|
|
30
91
|
} catch {
|
|
31
|
-
|
|
92
|
+
// No HEAD yet (an unborn repository): there is no history to record a removal against.
|
|
93
|
+
staged = '';
|
|
32
94
|
}
|
|
95
|
+
const deleted = staged.split('\n').some((l) => /^D/.test(l.trim()));
|
|
96
|
+
const hasDiff = staged.length > 0;
|
|
33
97
|
|
|
34
98
|
if (hasDiff) {
|
|
35
|
-
|
|
99
|
+
// -m BEFORE the `--`: everything after `--` is a PATHSPEC, so the old order made git
|
|
100
|
+
// look for files literally named '-m' and 'docs(insights): auto-capture' — it failed every
|
|
101
|
+
// time, from every directory, and this hook exits 0 on failure, so nobody saw it.
|
|
102
|
+
stage = 'commit';
|
|
103
|
+
// A deletion gets its own subject, so the event is findable in `git log` without
|
|
104
|
+
// reading diffs — which is the whole point of recording it.
|
|
105
|
+
git(['commit', '-m', deleted ? 'docs(insights): auto-remove' : 'docs(insights): auto-capture', '--only', '--', RELATIVE]);
|
|
36
106
|
}
|
|
37
|
-
} catch (
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Best-effort — never break the Claude session on commit failures. But "not breaking the session"
|
|
109
|
+
// and "saying nothing" are different things, and only the first one is the contract: it is about
|
|
110
|
+
// the EXIT CODE. Silence is what let a permanent defect live here undetected — `-m` after `--`
|
|
111
|
+
// made every commit fail, from every directory, forever, and nothing said so.
|
|
112
|
+
//
|
|
113
|
+
// A held index.lock, a missing user.email, a repository pre-commit hook that rejected the commit,
|
|
114
|
+
// an ignored target: each now costs one line and still exits 0. The ordinary "nothing to commit"
|
|
115
|
+
// path never reaches here, deliberately — a notice that fires when nothing is wrong trains people
|
|
116
|
+
// to ignore notices, and the next real failure scrolls past with them.
|
|
117
|
+
const artifactName = '.claude/insights/';
|
|
118
|
+
const gitSaid = err && err.stderr ? String(err.stderr).trim() : '';
|
|
119
|
+
const detail = (gitSaid || String((err && err.message) || err)).split('\n')
|
|
120
|
+
.map((l) => l.trim()).filter(Boolean)[0] || 'unknown error';
|
|
121
|
+
process.stdout.write('[autocommit-insights] could not ' + stage + ' — ' + artifactName + ': '
|
|
122
|
+
+ detail.slice(0, 200) + '\n');
|
|
38
123
|
process.exit(0);
|
|
39
124
|
}
|
|
@@ -10,30 +10,115 @@ const fs = require('node:fs');
|
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// The project root, never the process cwd: a `cd` inside any tool call moves cwd for the rest of
|
|
14
|
+
// the session, and these hooks are non-blocking, so a wrong anchor fails SILENTLY. CLAUDE_PROJECT_DIR
|
|
15
|
+
// first — the host is authoritative about what the project is. `__dirname` second: a hook always
|
|
16
|
+
// lives at <project>/.claude/hooks/<x>.cjs, so its own location settles the root with no cooperation
|
|
17
|
+
// from anyone, which is what keeps this working when the variable is absent (hand-run, older host).
|
|
18
|
+
const ENV_ROOT = process.env.CLAUDE_PROJECT_DIR;
|
|
19
|
+
// isAbsolute, not just truthy: a RELATIVE value would still be resolved against the drifting
|
|
20
|
+
// cwd, which is the very bug this anchor exists to remove.
|
|
21
|
+
const ROOT = (ENV_ROOT && path.isAbsolute(ENV_ROOT))
|
|
22
|
+
? ENV_ROOT
|
|
23
|
+
: path.resolve(__dirname, '..', '..');
|
|
24
|
+
|
|
25
|
+
const TARGET_DIR = path.resolve(ROOT, 'docs', 'plans');
|
|
26
|
+
const RELATIVE = path.relative(ROOT, TARGET_DIR);
|
|
27
|
+
// cwd: ROOT — the paths below are relative to ROOT, so git must run there too.
|
|
28
|
+
// stderr is PIPED, not ignored: on success nothing is printed anyway, and on failure git's
|
|
29
|
+
// own words are the only thing that tells a reader WHY. Discarding them leaves a report that
|
|
30
|
+
// names what failed and not why, which is half a report.
|
|
31
|
+
const SILENT = { stdio: ['ignore', 'pipe', 'pipe'], cwd: ROOT };
|
|
32
|
+
|
|
33
|
+
// What the hook was DOING when it failed. The outer catch also sees staging failures and a
|
|
34
|
+
// missing git binary, and reporting either of those as "could not commit" would send the reader
|
|
35
|
+
// looking in the wrong place.
|
|
36
|
+
let stage = 'start';
|
|
16
37
|
|
|
17
38
|
function git(args) {
|
|
18
39
|
return execFileSync('git', args, SILENT);
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
// The repository probe comes FIRST now, because the absence check below needs to ask git a
|
|
44
|
+
// question and there is no point asking outside a repository.
|
|
45
|
+
stage = 'inspect the repository';
|
|
46
|
+
try {
|
|
47
|
+
git(['rev-parse', '--git-dir']);
|
|
48
|
+
} catch (probeErr) {
|
|
49
|
+
// "Not a git repository" is the ordinary case and stays silent. Everything else — no git on
|
|
50
|
+
// PATH, dubious ownership, a permission error — is a real failure that used to look exactly
|
|
51
|
+
// like it, which is how a broken machine and an ordinary directory became indistinguishable.
|
|
52
|
+
const why = String((probeErr && probeErr.stderr) || '').trim();
|
|
53
|
+
if (!why || /not a git repository/i.test(why)) process.exit(0);
|
|
54
|
+
throw probeErr;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Absent — but absent HOW? A path that never existed is nothing to do, as before. A path git
|
|
58
|
+
// still tracks is a DELETION, and a deletion is exactly the change this hook exists to record.
|
|
59
|
+
//
|
|
60
|
+
// `git ls-files` is the discriminator, and it must run BEFORE staging: staging a deletion
|
|
61
|
+
// removes the entry from the index, after which the same question answers "not tracked".
|
|
62
|
+
// CORRECTION, measured: `--error-unmatch` also discriminates correctly here, for files AND for
|
|
63
|
+
// directories — 0 for a deleted-but-tracked path, 1 for one that never existed. An earlier note
|
|
64
|
+
// here claimed otherwise; that claim came from measuring it AFTER staging, which is what made it
|
|
65
|
+
// fail, not the directory-ness. `ls-files --` is preferred only because it answers with DATA
|
|
66
|
+
// (empty or not) instead of by throwing, so the ordinary case needs no exception handling.
|
|
67
|
+
//
|
|
68
|
+
// Simply DELETING the existence guard would have been the naive fix and would have broken the
|
|
69
|
+
// reporting that shipped yesterday: `git add` on a path that never existed exits 128, so every
|
|
70
|
+
// session stop in every project without this artifact would print a failure line. MEASURED.
|
|
71
|
+
if (!fs.existsSync(TARGET_DIR)) {
|
|
72
|
+
// A bare `catch { tracked = '' }` here would turn a REAL git failure into "never existed"
|
|
73
|
+
// and skip a deletion that should have been recorded — a silent bypass of the very thing
|
|
74
|
+
// this feature adds. A failure is rethrown into the reporting path; only a genuinely
|
|
75
|
+
// empty answer means the artifact never lived here.
|
|
76
|
+
const tracked = String(git(['ls-files', '--', RELATIVE]) || '').trim();
|
|
77
|
+
if (!tracked) process.exit(0); // never existed here — silent, exactly as before
|
|
78
|
+
}
|
|
24
79
|
|
|
80
|
+
stage = 'stage the change';
|
|
25
81
|
git(['add', '--', RELATIVE]);
|
|
26
82
|
|
|
27
|
-
|
|
83
|
+
// Ask git WHAT it staged, not merely WHETHER something changed — same subprocess, strictly
|
|
84
|
+
// more information. Deriving "this is a removal" from whether the path still exists was
|
|
85
|
+
// wrong for a directory: deleting ONE file inside it, or deleting every tracked file while
|
|
86
|
+
// an ignored one keeps the directory present, would have been committed as an update and
|
|
87
|
+
// defeated the very search this feature promises.
|
|
88
|
+
let staged = '';
|
|
28
89
|
try {
|
|
29
|
-
git(['diff', '--cached', '--
|
|
90
|
+
staged = String(git(['diff', '--cached', '--name-status', '--', RELATIVE]) || '').trim();
|
|
30
91
|
} catch {
|
|
31
|
-
|
|
92
|
+
// No HEAD yet (an unborn repository): there is no history to record a removal against.
|
|
93
|
+
staged = '';
|
|
32
94
|
}
|
|
95
|
+
const deleted = staged.split('\n').some((l) => /^D/.test(l.trim()));
|
|
96
|
+
const hasDiff = staged.length > 0;
|
|
33
97
|
|
|
34
98
|
if (hasDiff) {
|
|
35
|
-
|
|
99
|
+
// -m BEFORE the `--`: everything after `--` is a PATHSPEC, so the old order made git
|
|
100
|
+
// look for files literally named '-m' and 'docs(plan): auto-save' — it failed every
|
|
101
|
+
// time, from every directory, and this hook exits 0 on failure, so nobody saw it.
|
|
102
|
+
stage = 'commit';
|
|
103
|
+
// A deletion gets its own subject, so the event is findable in `git log` without
|
|
104
|
+
// reading diffs — which is the whole point of recording it.
|
|
105
|
+
git(['commit', '-m', deleted ? 'docs(plan): auto-remove' : 'docs(plan): auto-save', '--only', '--', RELATIVE]);
|
|
36
106
|
}
|
|
37
|
-
} catch (
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Best-effort — never break the Claude session on commit failures. But "not breaking the session"
|
|
109
|
+
// and "saying nothing" are different things, and only the first one is the contract: it is about
|
|
110
|
+
// the EXIT CODE. Silence is what let a permanent defect live here undetected — `-m` after `--`
|
|
111
|
+
// made every commit fail, from every directory, forever, and nothing said so.
|
|
112
|
+
//
|
|
113
|
+
// A held index.lock, a missing user.email, a repository pre-commit hook that rejected the commit,
|
|
114
|
+
// an ignored target: each now costs one line and still exits 0. The ordinary "nothing to commit"
|
|
115
|
+
// path never reaches here, deliberately — a notice that fires when nothing is wrong trains people
|
|
116
|
+
// to ignore notices, and the next real failure scrolls past with them.
|
|
117
|
+
const artifactName = 'docs/plans/';
|
|
118
|
+
const gitSaid = err && err.stderr ? String(err.stderr).trim() : '';
|
|
119
|
+
const detail = (gitSaid || String((err && err.message) || err)).split('\n')
|
|
120
|
+
.map((l) => l.trim()).filter(Boolean)[0] || 'unknown error';
|
|
121
|
+
process.stdout.write('[autocommit-plans] could not ' + stage + ' — ' + artifactName + ': '
|
|
122
|
+
+ detail.slice(0, 200) + '\n');
|
|
38
123
|
process.exit(0);
|
|
39
124
|
}
|
|
@@ -10,35 +10,118 @@ const fs = require('node:fs');
|
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// The project root, never the process cwd: a `cd` inside any tool call moves cwd for the rest of
|
|
14
|
+
// the session, and these hooks are non-blocking, so a wrong anchor fails SILENTLY. CLAUDE_PROJECT_DIR
|
|
15
|
+
// first — the host is authoritative about what the project is. `__dirname` second: a hook always
|
|
16
|
+
// lives at <project>/.claude/hooks/<x>.cjs, so its own location settles the root with no cooperation
|
|
17
|
+
// from anyone, which is what keeps this working when the variable is absent (hand-run, older host).
|
|
18
|
+
const ENV_ROOT = process.env.CLAUDE_PROJECT_DIR;
|
|
19
|
+
// isAbsolute, not just truthy: a RELATIVE value would still be resolved against the drifting
|
|
20
|
+
// cwd, which is the very bug this anchor exists to remove.
|
|
21
|
+
const ROOT = (ENV_ROOT && path.isAbsolute(ENV_ROOT))
|
|
22
|
+
? ENV_ROOT
|
|
23
|
+
: path.resolve(__dirname, '..', '..');
|
|
24
|
+
|
|
25
|
+
const TARGET = path.resolve(ROOT, '.claude', 'feature-roadmap.json');
|
|
26
|
+
const RELATIVE = path.relative(ROOT, TARGET);
|
|
27
|
+
// cwd: ROOT — the paths below are relative to ROOT, so git must run there too.
|
|
28
|
+
// stderr is PIPED, not ignored: on success nothing is printed anyway, and on failure git's
|
|
29
|
+
// own words are the only thing that tells a reader WHY. Discarding them leaves a report that
|
|
30
|
+
// names what failed and not why, which is half a report.
|
|
31
|
+
const SILENT = { stdio: ['ignore', 'pipe', 'pipe'], cwd: ROOT };
|
|
32
|
+
|
|
33
|
+
// What the hook was DOING when it failed. The outer catch also sees staging failures and a
|
|
34
|
+
// missing git binary, and reporting either of those as "could not commit" would send the reader
|
|
35
|
+
// looking in the wrong place.
|
|
36
|
+
let stage = 'start';
|
|
16
37
|
|
|
17
38
|
function git(args) {
|
|
18
39
|
return execFileSync('git', args, SILENT);
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
try {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
// The repository probe comes FIRST now, because the absence check below needs to ask git a
|
|
44
|
+
// question and there is no point asking outside a repository.
|
|
45
|
+
stage = 'inspect the repository';
|
|
46
|
+
try {
|
|
47
|
+
git(['rev-parse', '--git-dir']);
|
|
48
|
+
} catch (probeErr) {
|
|
49
|
+
// "Not a git repository" is the ordinary case and stays silent. Everything else — no git on
|
|
50
|
+
// PATH, dubious ownership, a permission error — is a real failure that used to look exactly
|
|
51
|
+
// like it, which is how a broken machine and an ordinary directory became indistinguishable.
|
|
52
|
+
const why = String((probeErr && probeErr.stderr) || '').trim();
|
|
53
|
+
if (!why || /not a git repository/i.test(why)) process.exit(0);
|
|
54
|
+
throw probeErr;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Absent — but absent HOW? A path that never existed is nothing to do, as before. A path git
|
|
58
|
+
// still tracks is a DELETION, and a deletion is exactly the change this hook exists to record.
|
|
59
|
+
//
|
|
60
|
+
// `git ls-files` is the discriminator, and it must run BEFORE staging: staging a deletion
|
|
61
|
+
// removes the entry from the index, after which the same question answers "not tracked".
|
|
62
|
+
// CORRECTION, measured: `--error-unmatch` also discriminates correctly here, for files AND for
|
|
63
|
+
// directories — 0 for a deleted-but-tracked path, 1 for one that never existed. An earlier note
|
|
64
|
+
// here claimed otherwise; that claim came from measuring it AFTER staging, which is what made it
|
|
65
|
+
// fail, not the directory-ness. `ls-files --` is preferred only because it answers with DATA
|
|
66
|
+
// (empty or not) instead of by throwing, so the ordinary case needs no exception handling.
|
|
67
|
+
//
|
|
68
|
+
// Simply DELETING the existence guard would have been the naive fix and would have broken the
|
|
69
|
+
// reporting that shipped yesterday: `git add` on a path that never existed exits 128, so every
|
|
70
|
+
// session stop in every project without this artifact would print a failure line. MEASURED.
|
|
71
|
+
if (!fs.existsSync(TARGET)) {
|
|
72
|
+
// A bare `catch { tracked = '' }` here would turn a REAL git failure into "never existed"
|
|
73
|
+
// and skip a deletion that should have been recorded — a silent bypass of the very thing
|
|
74
|
+
// this feature adds. A failure is rethrown into the reporting path; only a genuinely
|
|
75
|
+
// empty answer means the artifact never lived here.
|
|
76
|
+
const tracked = String(git(['ls-files', '--', RELATIVE]) || '').trim();
|
|
77
|
+
if (!tracked) process.exit(0); // never existed here — silent, exactly as before
|
|
78
|
+
}
|
|
25
79
|
|
|
26
|
-
// Stage
|
|
80
|
+
// Stage the target (or its removal).
|
|
81
|
+
stage = 'stage the change';
|
|
27
82
|
git(['add', '--', RELATIVE]);
|
|
28
83
|
|
|
29
84
|
// Check whether anything is staged for THIS path.
|
|
30
85
|
// `git diff --cached --quiet -- <path>` exits 0 = no diff, 1 = diff exists.
|
|
31
|
-
|
|
86
|
+
// Ask git WHAT it staged, not merely WHETHER something changed — same subprocess, strictly
|
|
87
|
+
// more information. Deriving "this is a removal" from whether the path still exists was
|
|
88
|
+
// wrong for a directory: deleting ONE file inside it, or deleting every tracked file while
|
|
89
|
+
// an ignored one keeps the directory present, would have been committed as an update and
|
|
90
|
+
// defeated the very search this feature promises.
|
|
91
|
+
let staged = '';
|
|
32
92
|
try {
|
|
33
|
-
git(['diff', '--cached', '--
|
|
93
|
+
staged = String(git(['diff', '--cached', '--name-status', '--', RELATIVE]) || '').trim();
|
|
34
94
|
} catch {
|
|
35
|
-
|
|
95
|
+
// No HEAD yet (an unborn repository): there is no history to record a removal against.
|
|
96
|
+
staged = '';
|
|
36
97
|
}
|
|
98
|
+
const deleted = staged.split('\n').some((l) => /^D/.test(l.trim()));
|
|
99
|
+
const hasDiff = staged.length > 0;
|
|
37
100
|
|
|
38
101
|
if (hasDiff) {
|
|
39
|
-
|
|
102
|
+
// -m BEFORE the `--`: everything after `--` is a PATHSPEC, so the old order made git
|
|
103
|
+
// look for files literally named '-m' and 'docs(roadmap): auto-update' — it failed every
|
|
104
|
+
// time, from every directory, and this hook exits 0 on failure, so nobody saw it.
|
|
105
|
+
stage = 'commit';
|
|
106
|
+
// A deletion gets its own subject, so the event is findable in `git log` without
|
|
107
|
+
// reading diffs — which is the whole point of recording it.
|
|
108
|
+
git(['commit', '-m', deleted ? 'docs(roadmap): auto-remove' : 'docs(roadmap): auto-update', '--only', '--', RELATIVE]);
|
|
40
109
|
}
|
|
41
|
-
} catch (
|
|
42
|
-
// Best-effort — never break Claude session on commit failures.
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// Best-effort — never break the Claude session on commit failures. But "not breaking the session"
|
|
112
|
+
// and "saying nothing" are different things, and only the first one is the contract: it is about
|
|
113
|
+
// the EXIT CODE. Silence is what let a permanent defect live here undetected — `-m` after `--`
|
|
114
|
+
// made every commit fail, from every directory, forever, and nothing said so.
|
|
115
|
+
//
|
|
116
|
+
// A held index.lock, a missing user.email, a repository pre-commit hook that rejected the commit,
|
|
117
|
+
// an ignored target: each now costs one line and still exits 0. The ordinary "nothing to commit"
|
|
118
|
+
// path never reaches here, deliberately — a notice that fires when nothing is wrong trains people
|
|
119
|
+
// to ignore notices, and the next real failure scrolls past with them.
|
|
120
|
+
const artifactName = '.claude/feature-roadmap.json';
|
|
121
|
+
const gitSaid = err && err.stderr ? String(err.stderr).trim() : '';
|
|
122
|
+
const detail = (gitSaid || String((err && err.message) || err)).split('\n')
|
|
123
|
+
.map((l) => l.trim()).filter(Boolean)[0] || 'unknown error';
|
|
124
|
+
process.stdout.write('[autocommit-roadmap] could not ' + stage + ' — ' + artifactName + ': '
|
|
125
|
+
+ detail.slice(0, 200) + '\n');
|
|
43
126
|
process.exit(0);
|
|
44
127
|
}
|