@grainulation/wheat 1.0.2 → 1.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.
Files changed (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -31
  3. package/bin/wheat.js +47 -36
  4. package/compiler/detect-sprints.js +126 -92
  5. package/compiler/generate-manifest.js +116 -69
  6. package/compiler/wheat-compiler.js +789 -468
  7. package/lib/compiler.js +11 -6
  8. package/lib/connect.js +273 -134
  9. package/lib/disconnect.js +61 -40
  10. package/lib/guard.js +20 -17
  11. package/lib/index.js +8 -8
  12. package/lib/init.js +217 -142
  13. package/lib/install-prompt.js +26 -26
  14. package/lib/load-claims.js +88 -0
  15. package/lib/quickstart.js +225 -111
  16. package/lib/serve-mcp.js +495 -180
  17. package/lib/server.js +198 -111
  18. package/lib/stats.js +65 -39
  19. package/lib/status.js +65 -34
  20. package/lib/update.js +13 -11
  21. package/package.json +8 -4
  22. package/templates/claude.md +31 -17
  23. package/templates/commands/blind-spot.md +9 -2
  24. package/templates/commands/brief.md +11 -1
  25. package/templates/commands/calibrate.md +3 -1
  26. package/templates/commands/challenge.md +4 -1
  27. package/templates/commands/connect.md +12 -1
  28. package/templates/commands/evaluate.md +4 -0
  29. package/templates/commands/feedback.md +3 -1
  30. package/templates/commands/handoff.md +11 -7
  31. package/templates/commands/init.md +4 -1
  32. package/templates/commands/merge.md +4 -1
  33. package/templates/commands/next.md +1 -0
  34. package/templates/commands/present.md +3 -0
  35. package/templates/commands/prototype.md +2 -0
  36. package/templates/commands/pull.md +103 -0
  37. package/templates/commands/replay.md +8 -0
  38. package/templates/commands/research.md +1 -0
  39. package/templates/commands/resolve.md +4 -1
  40. package/templates/commands/status.md +4 -0
  41. package/templates/commands/sync.md +94 -0
  42. package/templates/commands/witness.md +6 -2
package/lib/connect.js CHANGED
@@ -6,10 +6,10 @@
6
6
  * Zero npm dependencies.
7
7
  */
8
8
 
9
- import http from 'node:http';
10
- import https from 'node:https';
11
- import fs from 'node:fs';
12
- import path from 'node:path';
9
+ import http from "node:http";
10
+ import https from "node:https";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
13
 
14
14
  // ─── Constants ─────────────────────────────────────────────────────────────
15
15
 
@@ -18,12 +18,12 @@ const DETECT_TIMEOUT_MS = 2000;
18
18
  const VERIFY_TIMEOUT_MS = 5000;
19
19
  const LOCK_RETRY_MS = 200;
20
20
  const LOCK_MAX_RETRIES = 10;
21
- const SETTINGS_FILENAME = '.claude/settings.local.json';
21
+ const SETTINGS_FILENAME = ".claude/settings.local.json";
22
22
 
23
23
  const HOOK_ENDPOINTS = {
24
- permission: '/hooks/permission',
25
- activity: '/hooks/activity',
26
- notification: '/hooks/notification',
24
+ permission: "/hooks/permission",
25
+ activity: "/hooks/activity",
26
+ notification: "/hooks/notification",
27
27
  };
28
28
 
29
29
  // ─── Argument parsing ──────────────────────────────────────────────────────
@@ -31,17 +31,19 @@ const HOOK_ENDPOINTS = {
31
31
  function parseArgs(args) {
32
32
  const flags = {};
33
33
  for (let i = 0; i < args.length; i++) {
34
- if (args[i] === '--url' && args[i + 1]) {
35
- flags.url = args[i + 1]; i++;
36
- } else if (args[i] === '--port' && args[i + 1]) {
37
- flags.port = parseInt(args[i + 1], 10); i++;
38
- } else if (args[i] === '--dry-run') {
34
+ if (args[i] === "--url" && args[i + 1]) {
35
+ flags.url = args[i + 1];
36
+ i++;
37
+ } else if (args[i] === "--port" && args[i + 1]) {
38
+ flags.port = parseInt(args[i + 1], 10);
39
+ i++;
40
+ } else if (args[i] === "--dry-run") {
39
41
  flags.dryRun = true;
40
- } else if (args[i] === '--force') {
42
+ } else if (args[i] === "--force") {
41
43
  flags.force = true;
42
- } else if (args[i] === '--json') {
44
+ } else if (args[i] === "--json") {
43
45
  flags.json = true;
44
- } else if (args[i] === '--help' || args[i] === '-h') {
46
+ } else if (args[i] === "--help" || args[i] === "-h") {
45
47
  flags.help = true;
46
48
  }
47
49
  }
@@ -51,28 +53,43 @@ function parseArgs(args) {
51
53
  // ─── HTTP helpers (zero-dep) ───────────────────────────────────────────────
52
54
 
53
55
  function httpRequest(url, options = {}) {
54
- return new Promise(resolve => {
56
+ return new Promise((resolve) => {
55
57
  const parsed = new URL(url);
56
- const client = parsed.protocol === 'https:' ? https : http;
58
+ const client = parsed.protocol === "https:" ? https : http;
57
59
  const timeout = options.timeout || DETECT_TIMEOUT_MS;
58
60
 
59
- const req = client.request(parsed, {
60
- method: options.method || 'GET',
61
- headers: options.headers || {},
62
- timeout,
63
- }, res => {
64
- let body = '';
65
- res.on('data', chunk => { body += chunk; });
66
- res.on('end', () => {
67
- resolve({ status: res.statusCode, body, error: null });
68
- });
61
+ const req = client.request(
62
+ parsed,
63
+ {
64
+ method: options.method || "GET",
65
+ headers: options.headers || {},
66
+ timeout,
67
+ },
68
+ (res) => {
69
+ let body = "";
70
+ res.on("data", (chunk) => {
71
+ body += chunk;
72
+ });
73
+ res.on("end", () => {
74
+ resolve({ status: res.statusCode, body, error: null });
75
+ });
76
+ }
77
+ );
78
+
79
+ req.on("error", (err) =>
80
+ resolve({ status: 0, body: "", error: err.message })
81
+ );
82
+ req.on("timeout", () => {
83
+ req.destroy();
84
+ resolve({ status: 0, body: "", error: "timeout" });
69
85
  });
70
86
 
71
- req.on('error', err => resolve({ status: 0, body: '', error: err.message }));
72
- req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: '', error: 'timeout' }); });
73
-
74
87
  if (options.body) {
75
- req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
88
+ req.write(
89
+ typeof options.body === "string"
90
+ ? options.body
91
+ : JSON.stringify(options.body)
92
+ );
76
93
  }
77
94
  req.end();
78
95
  });
@@ -82,38 +99,53 @@ function httpRequest(url, options = {}) {
82
99
 
83
100
  async function probeFarmer(baseUrl) {
84
101
  // Primary detection: hit /api/state which is the canonical farmer endpoint
85
- const stateResp = await httpRequest(baseUrl + '/api/state', { timeout: DETECT_TIMEOUT_MS });
102
+ const stateResp = await httpRequest(baseUrl + "/api/state", {
103
+ timeout: DETECT_TIMEOUT_MS,
104
+ });
86
105
  if (stateResp.error) return { found: false, error: stateResp.error };
87
106
 
88
107
  if (stateResp.status !== 200) {
89
108
  // Fallback: try root to see if it's farmer at all
90
- const rootResp = await httpRequest(baseUrl + '/', { timeout: DETECT_TIMEOUT_MS });
109
+ const rootResp = await httpRequest(baseUrl + "/", {
110
+ timeout: DETECT_TIMEOUT_MS,
111
+ });
91
112
  if (rootResp.error || rootResp.status !== 200) {
92
- return { found: false, error: `Port responds (HTTP ${stateResp.status}) but /api/state not available` };
113
+ return {
114
+ found: false,
115
+ error: `Port responds (HTTP ${stateResp.status}) but /api/state not available`,
116
+ };
93
117
  }
94
- const looksLikeFarmer = rootResp.body.includes('farmer') || rootResp.body.includes('Farmer');
118
+ const looksLikeFarmer =
119
+ rootResp.body.includes("farmer") || rootResp.body.includes("Farmer");
95
120
  if (!looksLikeFarmer) {
96
- return { found: false, error: `Port responds but does not look like Farmer` };
121
+ return {
122
+ found: false,
123
+ error: `Port responds but does not look like Farmer`,
124
+ };
97
125
  }
98
126
  }
99
127
 
100
128
  // Hook probe: verify the permission endpoint accepts POSTs
101
129
  const probePayload = {
102
- hook_event_name: 'PreToolUse',
103
- tool_name: '__wheat_connect_probe__',
104
- tool_input: '{}',
105
- session_id: 'wheat-connect-probe',
130
+ hook_event_name: "PreToolUse",
131
+ tool_name: "__wheat_connect_probe__",
132
+ tool_input: "{}",
133
+ session_id: "wheat-connect-probe",
106
134
  };
107
135
 
108
136
  const hookResp = await httpRequest(baseUrl + HOOK_ENDPOINTS.permission, {
109
- method: 'POST',
110
- headers: { 'Content-Type': 'application/json' },
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
111
139
  body: probePayload,
112
140
  timeout: VERIFY_TIMEOUT_MS,
113
141
  });
114
142
 
115
143
  if (hookResp.error) {
116
- return { found: true, verified: false, error: `Farmer found but hook probe failed: ${hookResp.error}` };
144
+ return {
145
+ found: true,
146
+ verified: false,
147
+ error: `Farmer found but hook probe failed: ${hookResp.error}`,
148
+ };
117
149
  }
118
150
 
119
151
  let isVerified = false;
@@ -125,20 +157,30 @@ async function probeFarmer(baseUrl) {
125
157
  found: true,
126
158
  verified: isVerified,
127
159
  status: hookResp.status,
128
- error: isVerified ? null : `Hook endpoint returned unexpected response (HTTP ${hookResp.status})`,
160
+ error: isVerified
161
+ ? null
162
+ : `Hook endpoint returned unexpected response (HTTP ${hookResp.status})`,
129
163
  };
130
164
  }
131
165
 
132
166
  async function verifySse(baseUrl) {
133
- return new Promise(resolve => {
134
- const parsed = new URL(baseUrl + '/events');
135
- const req = http.request(parsed, { method: 'GET', timeout: VERIFY_TIMEOUT_MS }, res => {
136
- const isSSE = res.headers['content-type']?.includes('text/event-stream');
137
- res.destroy(); // We only need to confirm it opens
138
- resolve({ ok: isSSE, status: res.statusCode });
167
+ return new Promise((resolve) => {
168
+ const parsed = new URL(baseUrl + "/events");
169
+ const req = http.request(
170
+ parsed,
171
+ { method: "GET", timeout: VERIFY_TIMEOUT_MS },
172
+ (res) => {
173
+ const isSSE =
174
+ res.headers["content-type"]?.includes("text/event-stream");
175
+ res.destroy(); // We only need to confirm it opens
176
+ resolve({ ok: isSSE, status: res.statusCode });
177
+ }
178
+ );
179
+ req.on("error", (err) => resolve({ ok: false, error: err.message }));
180
+ req.on("timeout", () => {
181
+ req.destroy();
182
+ resolve({ ok: false, error: "timeout" });
139
183
  });
140
- req.on('error', err => resolve({ ok: false, error: err.message }));
141
- req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
142
184
  req.end();
143
185
  });
144
186
  }
@@ -150,46 +192,78 @@ async function detectFarmer(preferredPort) {
150
192
  const result = await probeFarmer(baseUrl);
151
193
  if (result.found) return { ...result, url: baseUrl, port };
152
194
  }
153
- return { found: false, url: null, port: null, error: 'No farmer server found on default ports' };
195
+ return {
196
+ found: false,
197
+ url: null,
198
+ port: null,
199
+ error: "No farmer server found on default ports",
200
+ };
154
201
  }
155
202
 
156
203
  // ─── Settings file management ──────────────────────────────────────────────
157
204
 
158
205
  function hookCommand(farmerUrl, endpoint) {
206
+ if (process.platform === "win32") {
207
+ // PowerShell: read stdin via [Console]::In, POST via Invoke-RestMethod
208
+ const url = `${farmerUrl}${endpoint}`;
209
+ return `powershell -NoProfile -Command "$b=[Console]::In.ReadToEnd(); try{Invoke-RestMethod -Uri '${url}' -Method Post -ContentType 'application/json' -Body $b}catch{}"`;
210
+ }
159
211
  return `cat | curl -s -X POST ${farmerUrl}${endpoint} -H 'Content-Type: application/json' --data-binary @- 2>/dev/null || true`;
160
212
  }
161
213
 
162
214
  function buildHooksConfig(farmerUrl) {
163
215
  return {
164
- PreToolUse: [{
165
- matcher: '',
166
- hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.permission) }],
167
- }],
168
- PostToolUse: [{
169
- matcher: '',
170
- hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.activity) }],
171
- }],
172
- Notification: [{
173
- matcher: '',
174
- hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.notification) }],
175
- }],
216
+ PreToolUse: [
217
+ {
218
+ matcher: "",
219
+ hooks: [
220
+ {
221
+ type: "command",
222
+ command: hookCommand(farmerUrl, HOOK_ENDPOINTS.permission),
223
+ },
224
+ ],
225
+ },
226
+ ],
227
+ PostToolUse: [
228
+ {
229
+ matcher: "",
230
+ hooks: [
231
+ {
232
+ type: "command",
233
+ command: hookCommand(farmerUrl, HOOK_ENDPOINTS.activity),
234
+ },
235
+ ],
236
+ },
237
+ ],
238
+ Notification: [
239
+ {
240
+ matcher: "",
241
+ hooks: [
242
+ {
243
+ type: "command",
244
+ command: hookCommand(farmerUrl, HOOK_ENDPOINTS.notification),
245
+ },
246
+ ],
247
+ },
248
+ ],
176
249
  };
177
250
  }
178
251
 
179
252
  function readSettings(settingsPath) {
180
253
  try {
181
- return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
254
+ return JSON.parse(fs.readFileSync(settingsPath, "utf8"));
182
255
  } catch (err) {
183
- if (err.code === 'ENOENT') return {};
256
+ if (err.code === "ENOENT") return {};
184
257
  throw new Error(`Cannot parse ${settingsPath}: ${err.message}`);
185
258
  }
186
259
  }
187
260
 
188
261
  function isFarmerHookEntry(entry) {
189
262
  if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
190
- return entry.hooks.some(h =>
191
- (h.type === 'command' && h.command && h.command.includes('/hooks/'))
192
- || (h.type === 'url' && h.url && h.url.includes('/hooks/'))
263
+ return entry.hooks.some(
264
+ (h) =>
265
+ (h.type === "command" && h.command && h.command.includes("/hooks/")) ||
266
+ (h.type === "url" && h.url && h.url.includes("/hooks/"))
193
267
  );
194
268
  }
195
269
 
@@ -199,33 +273,40 @@ function mergeHooks(existing, farmerHooks) {
199
273
 
200
274
  for (const hookType of Object.keys(farmerHooks)) {
201
275
  const existingHooks = merged.hooks[hookType] || [];
202
- const nonFarmerHooks = existingHooks.filter(entry => !isFarmerHookEntry(entry));
276
+ const nonFarmerHooks = existingHooks.filter(
277
+ (entry) => !isFarmerHookEntry(entry)
278
+ );
203
279
  merged.hooks[hookType] = [...nonFarmerHooks, ...farmerHooks[hookType]];
204
280
  }
205
281
  return merged;
206
282
  }
207
283
 
208
284
  async function writeSettingsAtomic(settingsPath, settings) {
209
- const lockPath = settingsPath + '.lock';
210
- const backupPath = settingsPath + '.backup';
211
- const tmpPath = settingsPath + '.tmp';
285
+ const lockPath = settingsPath + ".lock";
286
+ const backupPath = settingsPath + ".backup";
287
+ const tmpPath = settingsPath + ".tmp";
212
288
 
213
289
  let lockAcquired = false;
214
290
  for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
215
291
  try {
216
- const fd = fs.openSync(lockPath, 'wx');
292
+ const fd = fs.openSync(lockPath, "wx");
217
293
  fs.writeSync(fd, String(process.pid));
218
294
  fs.closeSync(fd);
219
295
  lockAcquired = true;
220
296
  break;
221
297
  } catch (err) {
222
- if (err.code === 'EEXIST') {
298
+ if (err.code === "EEXIST") {
223
299
  try {
224
- const holderPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
300
+ const holderPid = parseInt(
301
+ fs.readFileSync(lockPath, "utf8").trim(),
302
+ 10
303
+ );
225
304
  process.kill(holderPid, 0);
226
- await new Promise(r => setTimeout(r, LOCK_RETRY_MS));
305
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
227
306
  } catch {
228
- try { fs.unlinkSync(lockPath); } catch {}
307
+ try {
308
+ fs.unlinkSync(lockPath);
309
+ } catch {}
229
310
  }
230
311
  } else {
231
312
  throw err;
@@ -234,17 +315,21 @@ async function writeSettingsAtomic(settingsPath, settings) {
234
315
  }
235
316
 
236
317
  if (!lockAcquired) {
237
- throw new Error('Cannot acquire file lock — another process is writing to settings');
318
+ throw new Error(
319
+ "Cannot acquire file lock — another process is writing to settings"
320
+ );
238
321
  }
239
322
 
240
323
  try {
241
324
  if (fs.existsSync(settingsPath)) {
242
325
  fs.copyFileSync(settingsPath, backupPath);
243
326
  }
244
- fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
327
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n");
245
328
  fs.renameSync(tmpPath, settingsPath);
246
329
  } finally {
247
- try { fs.unlinkSync(lockPath); } catch {}
330
+ try {
331
+ fs.unlinkSync(lockPath);
332
+ } catch {}
248
333
  }
249
334
  }
250
335
 
@@ -252,49 +337,53 @@ async function writeSettingsAtomic(settingsPath, settings) {
252
337
 
253
338
  function printSuccess(farmerUrl, settingsPath, dryRun) {
254
339
  console.log();
255
- console.log(' \x1b[32m\u2713\x1b[0m \x1b[1mFarmer connected\x1b[0m');
256
- console.log(' \u2500'.repeat(40));
340
+ console.log(" \x1b[32m\u2713\x1b[0m \x1b[1mFarmer connected\x1b[0m");
341
+ console.log(" \u2500".repeat(40));
257
342
  console.log(` Server: ${farmerUrl}`);
258
343
  console.log(` Settings: ${settingsPath}`);
259
344
  console.log();
260
- console.log(' Hooks configured:');
345
+ console.log(" Hooks configured:");
261
346
  console.log(` PreToolUse \u2192 ${farmerUrl}/hooks/permission`);
262
347
  console.log(` PostToolUse \u2192 ${farmerUrl}/hooks/activity`);
263
348
  console.log(` Notification \u2192 ${farmerUrl}/hooks/notification`);
264
349
  console.log();
265
350
  if (dryRun) {
266
- console.log(' \x1b[33m(dry run \u2014 no files were modified)\x1b[0m');
351
+ console.log(" \x1b[33m(dry run \u2014 no files were modified)\x1b[0m");
267
352
  console.log();
268
353
  }
269
- console.log(' What this means:');
270
- console.log(' Every Claude Code tool call in this project now routes');
271
- console.log(' through Farmer. You can approve, deny, or monitor');
272
- console.log(' from your phone or desktop.');
354
+ console.log(" What this means:");
355
+ console.log(" Every Claude Code tool call in this project now routes");
356
+ console.log(" through Farmer. You can approve, deny, or monitor");
357
+ console.log(" from your phone or desktop.");
273
358
  console.log();
274
- console.log(' What was verified:');
275
- console.log(' - Farmer is running and responding to hook probes');
276
- console.log(' - SSE event stream is available for live monitoring');
277
- console.log(' - Hooks were merged without overwriting existing settings');
278
- console.log(' - Slash commands installed/updated');
279
- console.log(' - .farmer-config.json written with sprint paths');
359
+ console.log(" What was verified:");
360
+ console.log(" - Farmer is running and responding to hook probes");
361
+ console.log(" - SSE event stream is available for live monitoring");
362
+ console.log(" - Hooks were merged without overwriting existing settings");
363
+ console.log(" - Slash commands installed/updated");
364
+ console.log(" - .farmer-config.json written with sprint paths");
280
365
  console.log();
281
- console.log(' What to do next:');
282
- console.log(' Open Claude Code in this directory. If Farmer goes down,');
283
- console.log(' hooks fail silently (|| true) so your workflow is never blocked.');
366
+ console.log(" What to do next:");
367
+ console.log(" Open Claude Code in this directory. If Farmer goes down,");
368
+ console.log(
369
+ " hooks fail silently (|| true) so your workflow is never blocked."
370
+ );
284
371
  console.log();
285
372
  }
286
373
 
287
374
  function printNotFound(triedPorts) {
288
375
  console.log();
289
- console.log(' \x1b[31m\u2717\x1b[0m \x1b[1mFarmer not detected\x1b[0m');
290
- console.log(' \u2500'.repeat(40));
291
- console.log(` Tried ports: ${triedPorts.join(', ')}`);
376
+ console.log(" \x1b[31m\u2717\x1b[0m \x1b[1mFarmer not detected\x1b[0m");
377
+ console.log(" \u2500".repeat(40));
378
+ console.log(` Tried ports: ${triedPorts.join(", ")}`);
292
379
  console.log();
293
- console.log(' To start Farmer:');
294
- console.log(' npx @grainulation/farmer start');
380
+ console.log(" To start Farmer:");
381
+ console.log(" npx @grainulation/farmer start");
295
382
  console.log();
296
- console.log(' Or connect to a remote Farmer:');
297
- console.log(' wheat connect farmer --url https://your-tunnel.trycloudflare.com');
383
+ console.log(" Or connect to a remote Farmer:");
384
+ console.log(
385
+ " wheat connect farmer --url https://your-tunnel.trycloudflare.com"
386
+ );
298
387
  console.log();
299
388
  }
300
389
 
@@ -334,20 +423,24 @@ export async function run(dir, args) {
334
423
  let detection;
335
424
 
336
425
  if (flags.url) {
337
- farmerUrl = flags.url.replace(/\/+$/, '');
426
+ farmerUrl = flags.url.replace(/\/+$/, "");
338
427
  console.log(`\n Connecting to ${farmerUrl}...`);
339
428
  detection = await probeFarmer(farmerUrl);
340
429
  if (!detection.found) {
341
430
  if (flags.json) {
342
431
  console.log(JSON.stringify({ success: false, error: detection.error }));
343
432
  } else {
344
- console.log(`\n \x1b[31m\u2717\x1b[0m Cannot reach Farmer at ${farmerUrl}: ${detection.error}\n`);
433
+ console.log(
434
+ `\n \x1b[31m\u2717\x1b[0m Cannot reach Farmer at ${farmerUrl}: ${detection.error}\n`
435
+ );
345
436
  }
346
437
  process.exit(1);
347
438
  }
348
439
  } else {
349
440
  const ports = flags.port ? [flags.port] : DEFAULT_PORTS;
350
- console.log(`\n Detecting Farmer on localhost (ports: ${ports.join(', ')})...`);
441
+ console.log(
442
+ `\n Detecting Farmer on localhost (ports: ${ports.join(", ")})...`
443
+ );
351
444
  detection = await detectFarmer(flags.port);
352
445
  if (!detection.found) {
353
446
  if (flags.json) {
@@ -361,8 +454,10 @@ export async function run(dir, args) {
361
454
  }
362
455
 
363
456
  if (!detection.verified) {
364
- console.log(` \x1b[33m!\x1b[0m Farmer found but hook verification failed.`);
365
- console.log(` ${detection.error || 'Unknown verification error'}`);
457
+ console.log(
458
+ ` \x1b[33m!\x1b[0m Farmer found but hook verification failed.`
459
+ );
460
+ console.log(` ${detection.error || "Unknown verification error"}`);
366
461
  console.log(` Proceeding with configuration anyway...`);
367
462
  } else {
368
463
  console.log(` \x1b[32m\u2713\x1b[0m Farmer detected at ${farmerUrl}`);
@@ -371,25 +466,43 @@ export async function run(dir, args) {
371
466
  // Verify SSE endpoint is available
372
467
  const sseResult = await verifySse(farmerUrl);
373
468
  if (sseResult.ok) {
374
- console.log(` \x1b[32m\u2713\x1b[0m SSE event stream verified at ${farmerUrl}/events`);
469
+ console.log(
470
+ ` \x1b[32m\u2713\x1b[0m SSE event stream verified at ${farmerUrl}/events`
471
+ );
375
472
  } else {
376
- console.log(` \x1b[33m!\x1b[0m SSE endpoint not confirmed (${sseResult.error || 'unexpected response'})`);
473
+ console.log(
474
+ ` \x1b[33m!\x1b[0m SSE endpoint not confirmed (${
475
+ sseResult.error || "unexpected response"
476
+ })`
477
+ );
377
478
  console.log(` Live monitoring may not work until Farmer restarts.`);
378
479
  }
379
480
 
380
481
  // Step 2: Read existing settings, merge, write
381
482
  const existing = readSettings(settingsPath);
382
483
 
383
- const hasExistingFarmerHooks = existing.hooks && Object.values(existing.hooks).some(
384
- entries => Array.isArray(entries) && entries.some(entry => isFarmerHookEntry(entry))
385
- );
484
+ const hasExistingFarmerHooks =
485
+ existing.hooks &&
486
+ Object.values(existing.hooks).some(
487
+ (entries) =>
488
+ Array.isArray(entries) &&
489
+ entries.some((entry) => isFarmerHookEntry(entry))
490
+ );
386
491
 
387
492
  if (hasExistingFarmerHooks && !flags.force) {
388
493
  if (flags.json) {
389
- console.log(JSON.stringify({ success: true, alreadyConfigured: true, url: farmerUrl }));
494
+ console.log(
495
+ JSON.stringify({
496
+ success: true,
497
+ alreadyConfigured: true,
498
+ url: farmerUrl,
499
+ })
500
+ );
390
501
  } else {
391
- console.log(` \x1b[33m!\x1b[0m Farmer hooks already configured in ${SETTINGS_FILENAME}`);
392
- console.log(' Use --force to overwrite.');
502
+ console.log(
503
+ ` \x1b[33m!\x1b[0m Farmer hooks already configured in ${SETTINGS_FILENAME}`
504
+ );
505
+ console.log(" Use --force to overwrite.");
393
506
  }
394
507
  return;
395
508
  }
@@ -399,9 +512,16 @@ export async function run(dir, args) {
399
512
 
400
513
  if (flags.dryRun) {
401
514
  if (flags.json) {
402
- console.log(JSON.stringify({ success: true, dryRun: true, url: farmerUrl, settings: merged }));
515
+ console.log(
516
+ JSON.stringify({
517
+ success: true,
518
+ dryRun: true,
519
+ url: farmerUrl,
520
+ settings: merged,
521
+ })
522
+ );
403
523
  } else {
404
- console.log('\n Would write to: ' + settingsPath);
524
+ console.log("\n Would write to: " + settingsPath);
405
525
  console.log();
406
526
  console.log(JSON.stringify(merged, null, 2));
407
527
  printSuccess(farmerUrl, settingsPath, true);
@@ -414,22 +534,27 @@ export async function run(dir, args) {
414
534
 
415
535
  // Install/update slash commands
416
536
  try {
417
- const updateModule = await import(new URL('./update.js', import.meta.url).href);
418
- await updateModule.run(targetDir, ['--force']);
537
+ const updateModule = await import(
538
+ new URL("./update.js", import.meta.url).href
539
+ );
540
+ await updateModule.run(targetDir, ["--force"]);
419
541
  } catch (err) {
420
- console.log(` \x1b[33m!\x1b[0m Could not install slash commands: ${err.message}`);
542
+ console.log(
543
+ ` \x1b[33m!\x1b[0m Could not install slash commands: ${err.message}`
544
+ );
421
545
  }
422
546
 
423
547
  // Write sprint paths to .farmer-config.json so farmer auto-discovers them
424
- const farmerConfigPath = path.join(targetDir, '.farmer-config.json');
548
+ const farmerConfigPath = path.join(targetDir, ".farmer-config.json");
425
549
  try {
426
550
  let farmerConfig = {};
427
551
  if (fs.existsSync(farmerConfigPath)) {
428
- farmerConfig = JSON.parse(fs.readFileSync(farmerConfigPath, 'utf8'));
552
+ farmerConfig = JSON.parse(fs.readFileSync(farmerConfigPath, "utf8"));
429
553
  }
430
- const claimsPath = path.join(targetDir, 'claims.json');
431
- const compilationPath = path.join(targetDir, 'compilation.json');
432
- if (fs.existsSync(claimsPath) || true) { // Always write paths, claims may come later
554
+ const claimsPath = path.join(targetDir, "claims.json");
555
+ const compilationPath = path.join(targetDir, "compilation.json");
556
+ if (fs.existsSync(claimsPath) || true) {
557
+ // Always write paths, claims may come later
433
558
  farmerConfig.claimsPath = claimsPath;
434
559
  farmerConfig.compilationPath = compilationPath;
435
560
  }
@@ -439,14 +564,28 @@ export async function run(dir, args) {
439
564
  if (!farmerConfig.registeredProjects.includes(targetDir)) {
440
565
  farmerConfig.registeredProjects.push(targetDir);
441
566
  }
442
- fs.writeFileSync(farmerConfigPath, JSON.stringify(farmerConfig, null, 2) + '\n');
443
- console.log(` \x1b[32m+\x1b[0m .farmer-config.json (sprint paths registered)`);
567
+ fs.writeFileSync(
568
+ farmerConfigPath,
569
+ JSON.stringify(farmerConfig, null, 2) + "\n"
570
+ );
571
+ console.log(
572
+ ` \x1b[32m+\x1b[0m .farmer-config.json (sprint paths registered)`
573
+ );
444
574
  } catch (err) {
445
- console.log(` \x1b[33m!\x1b[0m Could not write .farmer-config.json: ${err.message}`);
575
+ console.log(
576
+ ` \x1b[33m!\x1b[0m Could not write .farmer-config.json: ${err.message}`
577
+ );
446
578
  }
447
579
 
448
580
  if (flags.json) {
449
- console.log(JSON.stringify({ success: true, url: farmerUrl, settingsPath, verified: detection.verified }));
581
+ console.log(
582
+ JSON.stringify({
583
+ success: true,
584
+ url: farmerUrl,
585
+ settingsPath,
586
+ verified: detection.verified,
587
+ })
588
+ );
450
589
  } else {
451
590
  printSuccess(farmerUrl, settingsPath, false);
452
591
  }