@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.
- package/LICENSE +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +47 -36
- package/compiler/detect-sprints.js +126 -92
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +789 -468
- package/lib/compiler.js +11 -6
- package/lib/connect.js +273 -134
- package/lib/disconnect.js +61 -40
- package/lib/guard.js +20 -17
- package/lib/index.js +8 -8
- package/lib/init.js +217 -142
- package/lib/install-prompt.js +26 -26
- package/lib/load-claims.js +88 -0
- package/lib/quickstart.js +225 -111
- package/lib/serve-mcp.js +495 -180
- package/lib/server.js +198 -111
- package/lib/stats.js +65 -39
- package/lib/status.js +65 -34
- package/lib/update.js +13 -11
- package/package.json +8 -4
- package/templates/claude.md +31 -17
- package/templates/commands/blind-spot.md +9 -2
- package/templates/commands/brief.md +11 -1
- package/templates/commands/calibrate.md +3 -1
- package/templates/commands/challenge.md +4 -1
- package/templates/commands/connect.md +12 -1
- package/templates/commands/evaluate.md +4 -0
- package/templates/commands/feedback.md +3 -1
- package/templates/commands/handoff.md +11 -7
- package/templates/commands/init.md +4 -1
- package/templates/commands/merge.md +4 -1
- package/templates/commands/next.md +1 -0
- package/templates/commands/present.md +3 -0
- package/templates/commands/prototype.md +2 -0
- package/templates/commands/pull.md +103 -0
- package/templates/commands/replay.md +8 -0
- package/templates/commands/research.md +1 -0
- package/templates/commands/resolve.md +4 -1
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +94 -0
- 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
|
|
10
|
-
import https from
|
|
11
|
-
import fs from
|
|
12
|
-
import path from
|
|
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 =
|
|
21
|
+
const SETTINGS_FILENAME = ".claude/settings.local.json";
|
|
22
22
|
|
|
23
23
|
const HOOK_ENDPOINTS = {
|
|
24
|
-
permission:
|
|
25
|
-
activity:
|
|
26
|
-
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] ===
|
|
35
|
-
flags.url = args[i + 1];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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] ===
|
|
42
|
+
} else if (args[i] === "--force") {
|
|
41
43
|
flags.force = true;
|
|
42
|
-
} else if (args[i] ===
|
|
44
|
+
} else if (args[i] === "--json") {
|
|
43
45
|
flags.json = true;
|
|
44
|
-
} else if (args[i] ===
|
|
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 ===
|
|
58
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
57
59
|
const timeout = options.timeout || DETECT_TIMEOUT_MS;
|
|
58
60
|
|
|
59
|
-
const req = client.request(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
res
|
|
67
|
-
|
|
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(
|
|
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 +
|
|
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 +
|
|
109
|
+
const rootResp = await httpRequest(baseUrl + "/", {
|
|
110
|
+
timeout: DETECT_TIMEOUT_MS,
|
|
111
|
+
});
|
|
91
112
|
if (rootResp.error || rootResp.status !== 200) {
|
|
92
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
found: false,
|
|
115
|
+
error: `Port responds (HTTP ${stateResp.status}) but /api/state not available`,
|
|
116
|
+
};
|
|
93
117
|
}
|
|
94
|
-
const looksLikeFarmer =
|
|
118
|
+
const looksLikeFarmer =
|
|
119
|
+
rootResp.body.includes("farmer") || rootResp.body.includes("Farmer");
|
|
95
120
|
if (!looksLikeFarmer) {
|
|
96
|
-
return {
|
|
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:
|
|
103
|
-
tool_name:
|
|
104
|
-
tool_input:
|
|
105
|
-
session_id:
|
|
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:
|
|
110
|
-
headers: {
|
|
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 {
|
|
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
|
|
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 +
|
|
135
|
-
const req = http.request(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 {
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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,
|
|
254
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
182
255
|
} catch (err) {
|
|
183
|
-
if (err.code ===
|
|
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(
|
|
191
|
-
(h
|
|
192
|
-
|
|
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(
|
|
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 +
|
|
210
|
-
const backupPath = settingsPath +
|
|
211
|
-
const tmpPath = settingsPath +
|
|
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,
|
|
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 ===
|
|
298
|
+
if (err.code === "EEXIST") {
|
|
223
299
|
try {
|
|
224
|
-
const holderPid = parseInt(
|
|
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 {
|
|
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(
|
|
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) +
|
|
327
|
+
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n");
|
|
245
328
|
fs.renameSync(tmpPath, settingsPath);
|
|
246
329
|
} finally {
|
|
247
|
-
try {
|
|
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(
|
|
256
|
-
console.log(
|
|
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(
|
|
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(
|
|
351
|
+
console.log(" \x1b[33m(dry run \u2014 no files were modified)\x1b[0m");
|
|
267
352
|
console.log();
|
|
268
353
|
}
|
|
269
|
-
console.log(
|
|
270
|
-
console.log(
|
|
271
|
-
console.log(
|
|
272
|
-
console.log(
|
|
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(
|
|
275
|
-
console.log(
|
|
276
|
-
console.log(
|
|
277
|
-
console.log(
|
|
278
|
-
console.log(
|
|
279
|
-
console.log(
|
|
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(
|
|
282
|
-
console.log(
|
|
283
|
-
console.log(
|
|
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(
|
|
290
|
-
console.log(
|
|
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(
|
|
294
|
-
console.log(
|
|
380
|
+
console.log(" To start Farmer:");
|
|
381
|
+
console.log(" npx @grainulation/farmer start");
|
|
295
382
|
console.log();
|
|
296
|
-
console.log(
|
|
297
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
365
|
-
|
|
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(
|
|
469
|
+
console.log(
|
|
470
|
+
` \x1b[32m\u2713\x1b[0m SSE event stream verified at ${farmerUrl}/events`
|
|
471
|
+
);
|
|
375
472
|
} else {
|
|
376
|
-
console.log(
|
|
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 =
|
|
384
|
-
|
|
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(
|
|
494
|
+
console.log(
|
|
495
|
+
JSON.stringify({
|
|
496
|
+
success: true,
|
|
497
|
+
alreadyConfigured: true,
|
|
498
|
+
url: farmerUrl,
|
|
499
|
+
})
|
|
500
|
+
);
|
|
390
501
|
} else {
|
|
391
|
-
console.log(
|
|
392
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
418
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
552
|
+
farmerConfig = JSON.parse(fs.readFileSync(farmerConfigPath, "utf8"));
|
|
429
553
|
}
|
|
430
|
-
const claimsPath = path.join(targetDir,
|
|
431
|
-
const compilationPath = path.join(targetDir,
|
|
432
|
-
if (fs.existsSync(claimsPath) || true) {
|
|
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(
|
|
443
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|