@grainulation/harvest 1.0.0 → 1.0.2

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.
@@ -0,0 +1,25 @@
1
+ # Code of Conduct
2
+
3
+ ## Our standards
4
+
5
+ We are committed to providing a welcoming and productive environment for everyone. We expect participants to:
6
+
7
+ - Use welcoming and inclusive language
8
+ - Respect differing viewpoints and experiences
9
+ - Accept constructive criticism gracefully
10
+ - Focus on what is best for the community and the project
11
+ - Show empathy toward other participants
12
+
13
+ Unacceptable behavior includes harassment, trolling, personal attacks, and publishing others' private information without permission.
14
+
15
+ ## Scope
16
+
17
+ This code of conduct applies to all project spaces -- issues, pull requests, discussions, and any public channel where someone represents the project.
18
+
19
+ ## Enforcement
20
+
21
+ Instances of unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated, and will result in a response deemed necessary and appropriate.
22
+
23
+ ## Attribution
24
+
25
+ Adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
@@ -0,0 +1,93 @@
1
+ # Contributing to Harvest
2
+
3
+ Thanks for considering contributing. Harvest is the retrospective and analytics engine for the grainulation ecosystem -- it turns sprint history into insights and calibration data.
4
+
5
+ ## Quick setup
6
+
7
+ ```bash
8
+ git clone https://github.com/grainulation/harvest.git
9
+ cd harvest
10
+ node bin/harvest.js --help
11
+ ```
12
+
13
+ No `npm install` needed -- harvest has zero dependencies.
14
+
15
+ ## How to contribute
16
+
17
+ ### Report a bug
18
+
19
+ Open an issue with:
20
+
21
+ - What you expected
22
+ - What happened instead
23
+ - Your Node version (`node --version`)
24
+ - Steps to reproduce
25
+
26
+ ### Suggest a feature
27
+
28
+ Open an issue describing the use case, not just the solution. "I need X because Y" is more useful than "add X."
29
+
30
+ ### Submit a PR
31
+
32
+ 1. Fork the repo
33
+ 2. Create a branch (`git checkout -b fix/description`)
34
+ 3. Make your changes
35
+ 4. Run the tests: `node --test test/basic.test.js`
36
+ 5. Commit with a clear message
37
+ 6. Open a PR
38
+
39
+ ## Architecture
40
+
41
+ ```
42
+ bin/harvest.js CLI entrypoint -- dispatches subcommands
43
+ lib/analyzer.js Sprint data analysis and pattern detection
44
+ lib/calibration.js Prediction vs outcome scoring
45
+ lib/decay.js Claim staleness and evidence decay
46
+ lib/patterns.js Recurring pattern extraction
47
+ lib/report.js Report generation from analyzed data
48
+ lib/server.js Local preview server (SSE, zero deps)
49
+ lib/templates.js Template rendering for HTML output
50
+ lib/velocity.js Sprint velocity tracking
51
+ templates/ HTML templates (retrospective, etc.)
52
+ public/ Web UI -- retrospective dashboard
53
+ site/ Public website (harvest.grainulation.com)
54
+ test/ Node built-in test runner tests
55
+ ```
56
+
57
+ The key architectural principle: **harvest reads sprint artifacts (claims, compilations, git history) and produces calibrated insights.** It never modifies source data -- read-only analysis, write-only reports.
58
+
59
+ ## Code style
60
+
61
+ - Zero dependencies. If you need something, write it or use Node built-ins.
62
+ - No transpilation. Ship what you write.
63
+ - ESM imports (`import`/`export`). Node 18+ required.
64
+ - Keep functions small. If a function needs a scroll, split it.
65
+ - No emojis in code, CLI output, or reports.
66
+
67
+ ## Testing
68
+
69
+ ```bash
70
+ node --test test/basic.test.js
71
+ ```
72
+
73
+ Tests use Node's built-in test runner. No test framework dependencies.
74
+
75
+ ## Commit messages
76
+
77
+ Follow the existing pattern:
78
+
79
+ ```
80
+ harvest: <what changed>
81
+ ```
82
+
83
+ Examples:
84
+
85
+ ```
86
+ harvest: add velocity trend chart
87
+ harvest: fix decay calculation for stale claims
88
+ harvest: update calibration scoring algorithm
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT. See LICENSE for details.
package/README.md CHANGED
@@ -1,10 +1,27 @@
1
- # @grainulation/harvest
1
+ <p align="center">
2
+ <img src="site/wordmark.svg" alt="Harvest" width="400">
3
+ </p>
2
4
 
3
- **Are your decisions getting better?**
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/v/@grainulation/harvest" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/dm/@grainulation/harvest" alt="npm downloads"></a> <a href="https://github.com/grainulation/harvest/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/harvest" alt="node"></a> <a href="https://github.com/grainulation/harvest/actions"><img src="https://github.com/grainulation/harvest/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://deepwiki.com/grainulation/harvest"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
8
+ </p>
4
9
 
5
- Harvest is the analytics and retrospective layer for research sprints. It looks across sprints to find patterns, score predictions, and surface knowledge that's gone stale.
10
+ <p align="center"><strong>Are your decisions getting better?</strong></p>
6
11
 
7
- Learn from every decision you've made.
12
+ Harvest is the analytics layer for research sprints. It looks across sprints to find patterns, score predictions, and surface knowledge that's gone stale.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -g @grainulation/harvest
18
+ ```
19
+
20
+ Or use directly:
21
+
22
+ ```bash
23
+ npx @grainulation/harvest analyze ./sprints/
24
+ ```
8
25
 
9
26
  ## What it does
10
27
 
@@ -15,21 +32,9 @@ Learn from every decision you've made.
15
32
  - **Sprint velocity** -- how long do sprints take, where do they stall?
16
33
  - **Retrospective reports** -- dark-themed HTML reports for the team
17
34
 
18
- ## Install
19
-
20
- ```sh
21
- npm install @grainulation/harvest
22
- ```
35
+ ## Quick start
23
36
 
24
- Or run directly:
25
-
26
- ```sh
27
- npx @grainulation/harvest analyze ./sprints/
28
- ```
29
-
30
- ## Usage
31
-
32
- ```sh
37
+ ```bash
33
38
  # Cross-sprint claim analysis
34
39
  harvest analyze ./sprints/
35
40
 
@@ -50,52 +55,46 @@ harvest report ./sprints/ -o retrospective.html
50
55
 
51
56
  # All analyses in one pass
52
57
  harvest trends ./sprints/ --json
58
+
59
+ # Start the live dashboard (SSE updates, dark theme)
60
+ harvest serve --root ./sprints/ --port 9096
61
+
62
+ # Connect to farmer for mobile monitoring
63
+ harvest connect farmer --url http://localhost:9094
53
64
  ```
54
65
 
55
66
  ## Data format
56
67
 
57
68
  Harvest reads standard wheat sprint data:
58
69
 
59
- - `claims.json` -- array of typed claims with `id`, `type`, `evidence`, `status`, `text`, `created`, etc.
70
+ - `claims.json` -- array of typed claims with `id`, `type`, `evidence`, `status`, `text`, `created`
60
71
  - `compilation.json` -- compiled sprint state (optional, enriches analysis)
61
72
  - Git history on `claims.json` -- used for velocity and timing analysis
62
73
 
63
- Point harvest at a directory containing sprint subdirectories, or at a single sprint directory:
64
-
65
- ```
66
- sprints/
67
- sprint-alpha/
68
- claims.json
69
- compilation.json
70
- sprint-beta/
71
- claims.json
72
- ```
74
+ Point harvest at a directory containing sprint subdirectories, or at a single sprint directory.
73
75
 
74
76
  ## Design
75
77
 
76
- - **Zero dependencies** -- Node built-in modules only (fs, path, child_process)
77
78
  - **Reads, never writes** -- harvest is a pure analysis tool; it won't modify your sprint data
78
79
  - **Git-aware** -- uses git log timestamps for velocity analysis when available
79
80
  - **Composable** -- each module (analyzer, calibration, patterns, decay, velocity) works independently
80
81
 
81
- ## Claim types it understands
82
+ ## Zero dependencies
82
83
 
83
- | Type | What it means |
84
- |---|---|
85
- | `constraint` | Hard requirements, non-negotiable |
86
- | `factual` | Verifiable statements |
87
- | `estimate` | Predictions, projections, ranges |
88
- | `risk` | Potential failure modes |
89
- | `recommendation` | Proposed courses of action |
90
- | `feedback` | Stakeholder input |
84
+ Node built-in modules only.
91
85
 
92
- ## Evidence tiers (lowest to highest)
86
+ ## Part of the grainulation ecosystem
93
87
 
94
- 1. `stated` -- someone said it
95
- 2. `web` -- found online
96
- 3. `documented` -- in source code or official docs
97
- 4. `tested` -- verified via prototype or benchmark
98
- 5. `production` -- measured from live systems
88
+ | Tool | Role |
89
+ | ------------------------------------------------------------ | ----------------------------------------------------------- |
90
+ | [wheat](https://github.com/grainulation/wheat) | Research engine -- grow structured evidence |
91
+ | [farmer](https://github.com/grainulation/farmer) | Permission dashboard -- approve AI actions in real time |
92
+ | [barn](https://github.com/grainulation/barn) | Shared tools -- templates, validators, sprint detection |
93
+ | [mill](https://github.com/grainulation/mill) | Format conversion -- export to PDF, CSV, slides, 24 formats |
94
+ | [silo](https://github.com/grainulation/silo) | Knowledge storage -- reusable claim libraries and packs |
95
+ | **harvest** | Analytics -- cross-sprint patterns and prediction scoring |
96
+ | [orchard](https://github.com/grainulation/orchard) | Orchestration -- multi-sprint coordination and dependencies |
97
+ | [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem |
99
98
 
100
99
  ## License
101
100
 
package/bin/harvest.js CHANGED
@@ -1,23 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- 'use strict';
4
-
5
- const path = require('node:path');
6
- const fs = require('node:fs');
7
-
8
- const { analyze } = require('../lib/analyzer.js');
9
- const { calibrate } = require('../lib/calibration.js');
10
- const { detectPatterns } = require('../lib/patterns.js');
11
- const { checkDecay } = require('../lib/decay.js');
12
- const { measureVelocity } = require('../lib/velocity.js');
13
- const { generateReport } = require('../lib/report.js');
14
- const { connect: farmerConnect } = require('../lib/farmer.js');
15
-
16
- const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
3
+ "use strict";
4
+
5
+ const path = require("node:path");
6
+ const fs = require("node:fs");
7
+
8
+ const { analyze } = require("../lib/analyzer.js");
9
+ const { calibrate } = require("../lib/calibration.js");
10
+ const { detectPatterns } = require("../lib/patterns.js");
11
+ const { checkDecay, decayAlerts } = require("../lib/decay.js");
12
+ const { measureVelocity } = require("../lib/velocity.js");
13
+ const { generateReport } = require("../lib/report.js");
14
+ const { connect: farmerConnect } = require("../lib/farmer.js");
15
+ const { analyzeTokens } = require("../lib/tokens.js");
16
+ const { trackCosts } = require("../lib/token-tracker.js");
17
+ const { generateWrapped } = require("../lib/wrapped.js");
18
+ const {
19
+ generateCard,
20
+ generateEmbedSnippet,
21
+ } = require("../lib/harvest-card.js");
22
+
23
+ const verbose =
24
+ process.argv.includes("--verbose") || process.argv.includes("-v");
17
25
  function vlog(...a) {
18
26
  if (!verbose) return;
19
27
  const ts = new Date().toISOString();
20
- process.stderr.write(`[${ts}] harvest: ${a.join(' ')}\n`);
28
+ process.stderr.write(`[${ts}] harvest: ${a.join(" ")}\n`);
21
29
  }
22
30
 
23
31
  const USAGE = `
@@ -29,8 +37,11 @@ Usage:
29
37
  harvest patterns <sprints-dir> Detect decision patterns
30
38
  harvest decay <sprints-dir> Find claims that need refreshing
31
39
  harvest velocity <sprints-dir> Sprint timing and phase analysis
40
+ harvest tokens <sprints-dir> Token cost tracking and efficiency
41
+ harvest card <sprints-dir> [-o <output>] Generate Harvest Report SVG card
32
42
  harvest report <sprints-dir> [-o <output>] Generate retrospective HTML
33
43
  harvest trends <sprints-dir> All analyses in one pass
44
+ harvest intelligence <sprints-dir> Full intelligence report (all features)
34
45
  harvest serve [--port 9096] [--root <sprints-dir>] Start the dashboard UI
35
46
  harvest connect farmer [--url <url>] Configure farmer integration
36
47
 
@@ -43,22 +54,29 @@ Options:
43
54
 
44
55
  function parseArgs(argv) {
45
56
  const args = argv.slice(2);
46
- const parsed = { command: null, dir: null, output: null, json: false, days: 90 };
57
+ const parsed = {
58
+ command: null,
59
+ dir: null,
60
+ output: null,
61
+ json: false,
62
+ days: 90,
63
+ };
47
64
 
48
- if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
65
+ if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
49
66
  console.log(USAGE);
50
67
  process.exit(0);
51
68
  }
52
69
 
53
70
  parsed.command = args[0];
54
- parsed.dir = (args[1] && !args[1].startsWith('-')) ? path.resolve(args[1]) : null;
71
+ parsed.dir =
72
+ args[1] && !args[1].startsWith("-") ? path.resolve(args[1]) : null;
55
73
 
56
74
  for (let i = 2; i < args.length; i++) {
57
- if ((args[i] === '-o' || args[i] === '--output') && args[i + 1]) {
75
+ if ((args[i] === "-o" || args[i] === "--output") && args[i + 1]) {
58
76
  parsed.output = path.resolve(args[++i]);
59
- } else if (args[i] === '--json') {
77
+ } else if (args[i] === "--json") {
60
78
  parsed.json = true;
61
- } else if (args[i] === '--days' && args[i + 1]) {
79
+ } else if (args[i] === "--days" && args[i + 1]) {
62
80
  parsed.days = parseInt(args[++i], 10);
63
81
  }
64
82
  }
@@ -75,7 +93,7 @@ function loadSprintData(dir) {
75
93
  const sprints = [];
76
94
 
77
95
  // Include root if it has claims.json
78
- const directClaims = path.join(dir, 'claims.json');
96
+ const directClaims = path.join(dir, "claims.json");
79
97
  if (fs.existsSync(directClaims)) {
80
98
  sprints.push(loadSingleSprint(dir));
81
99
  }
@@ -85,9 +103,9 @@ function loadSprintData(dir) {
85
103
  const entries = fs.readdirSync(dir, { withFileTypes: true });
86
104
  for (const entry of entries) {
87
105
  if (!entry.isDirectory()) continue;
88
- if (entry.name.startsWith('.')) continue;
106
+ if (entry.name.startsWith(".")) continue;
89
107
  const childDir = path.join(dir, entry.name);
90
- const childClaims = path.join(childDir, 'claims.json');
108
+ const childClaims = path.join(childDir, "claims.json");
91
109
  if (fs.existsSync(childClaims)) {
92
110
  sprints.push(loadSingleSprint(childDir));
93
111
  }
@@ -96,20 +114,26 @@ function loadSprintData(dir) {
96
114
  const subEntries = fs.readdirSync(childDir, { withFileTypes: true });
97
115
  for (const sub of subEntries) {
98
116
  if (!sub.isDirectory()) continue;
99
- if (sub.name.startsWith('.')) continue;
117
+ if (sub.name.startsWith(".")) continue;
100
118
  const subDir = path.join(childDir, sub.name);
101
- const subClaims = path.join(subDir, 'claims.json');
119
+ const subClaims = path.join(subDir, "claims.json");
102
120
  if (fs.existsSync(subClaims)) {
103
121
  sprints.push(loadSingleSprint(subDir));
104
122
  }
105
123
  }
106
- } catch { /* skip */ }
124
+ } catch {
125
+ /* skip */
126
+ }
107
127
  }
108
- } catch { /* skip */ }
128
+ } catch {
129
+ /* skip */
130
+ }
109
131
 
110
132
  if (sprints.length === 0) {
111
133
  console.error(`harvest: no sprint data found in ${dir}`);
112
- console.error('Expected claims.json in the directory or its subdirectories.');
134
+ console.error(
135
+ "Expected claims.json in the directory or its subdirectories.",
136
+ );
113
137
  process.exit(1);
114
138
  }
115
139
 
@@ -125,9 +149,9 @@ function loadSingleSprint(dir) {
125
149
  gitLog: null,
126
150
  };
127
151
 
128
- const claimsPath = path.join(dir, 'claims.json');
152
+ const claimsPath = path.join(dir, "claims.json");
129
153
  try {
130
- sprint.claims = JSON.parse(fs.readFileSync(claimsPath, 'utf8'));
154
+ sprint.claims = JSON.parse(fs.readFileSync(claimsPath, "utf8"));
131
155
  if (!Array.isArray(sprint.claims)) {
132
156
  // Handle { claims: [...] } wrapper
133
157
  sprint.claims = sprint.claims.claims || [];
@@ -136,10 +160,10 @@ function loadSingleSprint(dir) {
136
160
  console.error(`harvest: could not parse ${claimsPath}: ${e.message}`);
137
161
  }
138
162
 
139
- const compilationPath = path.join(dir, 'compilation.json');
163
+ const compilationPath = path.join(dir, "compilation.json");
140
164
  if (fs.existsSync(compilationPath)) {
141
165
  try {
142
- sprint.compilation = JSON.parse(fs.readFileSync(compilationPath, 'utf8'));
166
+ sprint.compilation = JSON.parse(fs.readFileSync(compilationPath, "utf8"));
143
167
  } catch (e) {
144
168
  // skip
145
169
  }
@@ -147,14 +171,23 @@ function loadSingleSprint(dir) {
147
171
 
148
172
  // Try to read git log for the sprint directory
149
173
  try {
150
- const { execSync } = require('node:child_process');
174
+ const { execSync } = require("node:child_process");
151
175
  sprint.gitLog = execSync(
152
176
  `git log --oneline --format="%H|%ai|%s" -- claims.json`,
153
- { cwd: dir, encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
154
- ).trim().split('\n').filter(Boolean).map(line => {
155
- const [hash, date, ...msg] = line.split('|');
156
- return { hash, date, message: msg.join('|') };
157
- });
177
+ {
178
+ cwd: dir,
179
+ encoding: "utf8",
180
+ timeout: 5000,
181
+ stdio: ["pipe", "pipe", "pipe"],
182
+ },
183
+ )
184
+ .trim()
185
+ .split("\n")
186
+ .filter(Boolean)
187
+ .map((line) => {
188
+ const [hash, date, ...msg] = line.split("|");
189
+ return { hash, date, message: msg.join("|") };
190
+ });
158
191
  } catch (e) {
159
192
  sprint.gitLog = [];
160
193
  }
@@ -165,7 +198,7 @@ function loadSingleSprint(dir) {
165
198
  function output(result, opts) {
166
199
  if (opts.json) {
167
200
  console.log(JSON.stringify(result, null, 2));
168
- } else if (typeof result === 'string') {
201
+ } else if (typeof result === "string") {
169
202
  console.log(result);
170
203
  } else {
171
204
  console.log(JSON.stringify(result, null, 2));
@@ -174,7 +207,11 @@ function output(result, opts) {
174
207
 
175
208
  async function main() {
176
209
  const opts = parseArgs(process.argv);
177
- vlog('startup', `command=${opts.command || '(none)'}`, `dir=${opts.dir || 'none'}`);
210
+ vlog(
211
+ "startup",
212
+ `command=${opts.command || "(none)"}`,
213
+ `dir=${opts.dir || "none"}`,
214
+ );
178
215
 
179
216
  const commands = {
180
217
  analyze() {
@@ -210,11 +247,49 @@ async function main() {
210
247
  patternsFn: detectPatterns,
211
248
  decayFn: checkDecay,
212
249
  velocityFn: measureVelocity,
250
+ tokensFn: analyzeTokens,
251
+ wrappedFn: generateWrapped,
213
252
  });
214
- const outPath = opts.output || path.join(process.cwd(), 'retrospective.html');
215
- fs.writeFileSync(outPath, html, 'utf8');
253
+ const outPath =
254
+ opts.output || path.join(process.cwd(), "retrospective.html");
255
+ fs.writeFileSync(outPath, html, "utf8");
216
256
  console.log(`Retrospective written to ${outPath}`);
217
257
  },
258
+ tokens() {
259
+ const sprints = loadSprintData(opts.dir);
260
+ const result = analyzeTokens(sprints);
261
+ output(result, opts);
262
+ },
263
+ card() {
264
+ const sprints = loadSprintData(opts.dir);
265
+ const { svg, stats } = generateCard(sprints);
266
+
267
+ if (opts.json) {
268
+ output(stats, opts);
269
+ return;
270
+ }
271
+
272
+ const outPath =
273
+ opts.output || path.join(process.cwd(), "harvest-card.svg");
274
+ fs.writeFileSync(outPath, svg, "utf8");
275
+ const embed = generateEmbedSnippet(path.basename(outPath));
276
+ console.log(`Harvest card written to ${outPath}`);
277
+ console.log(`\nEmbed in README:\n ${embed.markdown}`);
278
+ console.log(`\nHTML:\n ${embed.html}`);
279
+ },
280
+ intelligence() {
281
+ const sprints = loadSprintData(opts.dir);
282
+ const result = {
283
+ analysis: analyze(sprints),
284
+ calibration: calibrate(sprints),
285
+ patterns: detectPatterns(sprints),
286
+ decay: checkDecay(sprints, { thresholdDays: opts.days }),
287
+ decayAlerts: decayAlerts(sprints),
288
+ velocity: measureVelocity(sprints),
289
+ tokens: analyzeTokens(sprints),
290
+ };
291
+ output(result, opts);
292
+ },
218
293
  trends() {
219
294
  const sprints = loadSprintData(opts.dir);
220
295
  const result = {
@@ -228,47 +303,47 @@ async function main() {
228
303
  },
229
304
  };
230
305
 
231
- if (opts.command === 'help') {
306
+ if (opts.command === "help") {
232
307
  console.log(USAGE);
233
308
  process.exit(0);
234
309
  }
235
310
 
236
- if (opts.command === 'connect') {
311
+ if (opts.command === "connect") {
237
312
  // Forward remaining args to farmer connect handler
238
- const connectArgs = process.argv.slice(process.argv.indexOf('connect') + 1);
313
+ const connectArgs = process.argv.slice(process.argv.indexOf("connect") + 1);
239
314
  await farmerConnect(opts.dir || process.cwd(), connectArgs);
240
315
  return;
241
316
  }
242
317
 
243
- if (opts.command === 'serve') {
318
+ if (opts.command === "serve") {
244
319
  // Launch the ESM server module
245
- const { execFile } = require('node:child_process');
246
- const serverPath = path.join(__dirname, '..', 'lib', 'server.js');
320
+ const { execFile } = require("node:child_process");
321
+ const serverPath = path.join(__dirname, "..", "lib", "server.js");
247
322
  const serverArgs = [];
248
323
  // Forward --port and --root
249
- const portIdx = process.argv.indexOf('--port');
324
+ const portIdx = process.argv.indexOf("--port");
250
325
  if (portIdx !== -1 && process.argv[portIdx + 1]) {
251
- serverArgs.push('--port', process.argv[portIdx + 1]);
326
+ serverArgs.push("--port", process.argv[portIdx + 1]);
252
327
  }
253
- const rootIdx = process.argv.indexOf('--root');
328
+ const rootIdx = process.argv.indexOf("--root");
254
329
  if (rootIdx !== -1 && process.argv[rootIdx + 1]) {
255
- serverArgs.push('--root', process.argv[rootIdx + 1]);
330
+ serverArgs.push("--root", process.argv[rootIdx + 1]);
256
331
  } else if (opts.dir) {
257
- serverArgs.push('--root', opts.dir);
332
+ serverArgs.push("--root", opts.dir);
258
333
  }
259
- const child = execFile('node', [serverPath, ...serverArgs], {
260
- stdio: 'inherit',
334
+ const child = execFile("node", [serverPath, ...serverArgs], {
335
+ stdio: "inherit",
261
336
  env: process.env,
262
337
  });
263
338
  child.stdout && child.stdout.pipe(process.stdout);
264
339
  child.stderr && child.stderr.pipe(process.stderr);
265
- child.on('error', (err) => {
340
+ child.on("error", (err) => {
266
341
  console.error(`harvest: error starting server: ${err.message}`);
267
342
  process.exit(1);
268
343
  });
269
- child.on('exit', (code) => process.exit(code || 0));
270
- process.on('SIGTERM', () => child.kill('SIGTERM'));
271
- process.on('SIGINT', () => child.kill('SIGINT'));
344
+ child.on("exit", (code) => process.exit(code || 0));
345
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
346
+ process.on("SIGINT", () => child.kill("SIGINT"));
272
347
  return;
273
348
  }
274
349