u-foo 2.4.5 → 2.4.7

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 (64) hide show
  1. package/README.md +6 -8
  2. package/README.zh-CN.md +5 -5
  3. package/SKILLS/ufoo/SKILL.md +2 -2
  4. package/SKILLS/uinit/SKILL.md +8 -11
  5. package/package.json +3 -8
  6. package/scripts/postinstall.js +1 -21
  7. package/src/agents/launch/launcher.js +1 -13
  8. package/src/app/chat/commandExecutor.js +14 -9
  9. package/src/app/chat/commands.js +2 -2
  10. package/src/app/chat/daemonCoordinator.js +4 -0
  11. package/src/app/chat/daemonReconnect.js +17 -7
  12. package/src/app/cli/features/doctor.js +11 -18
  13. package/src/app/cli/features/init.js +12 -191
  14. package/src/app/cli/features/skills.js +0 -15
  15. package/src/app/cli/run.js +12 -7
  16. package/src/code/README.md +4 -4
  17. package/src/code/cli.js +1 -1
  18. package/src/code/launcher/ucode.js +18 -45
  19. package/src/code/skills/loader.js +0 -13
  20. package/src/coordination/context/doctor.js +6 -26
  21. package/src/online/server.js +1 -1
  22. package/src/runtime/daemon/index.js +1 -1
  23. package/src/runtime/daemon/mcpServer.js +1 -1
  24. package/src/runtime/daemon/restart.js +293 -0
  25. package/src/runtime/daemon/run.js +31 -37
  26. package/src/runtime/terminal/index.js +1 -1
  27. package/src/ui/MIGRATION.md +8 -10
  28. package/src/ui/ink/ChatApp.js +12 -6
  29. package/bin/ucode-core.js +0 -15
  30. package/bin/ufoo +0 -71
  31. package/modules/AGENTS.template.md +0 -8
  32. package/modules/bus/README.md +0 -140
  33. package/modules/context/README.md +0 -60
  34. package/modules/online/README.md +0 -92
  35. package/modules/resources/ICONS/README.md +0 -12
  36. package/modules/resources/ICONS/libraries/README.md +0 -17
  37. package/modules/resources/ICONS/libraries/heroicons/LICENSE +0 -22
  38. package/modules/resources/ICONS/libraries/heroicons/README.md +0 -15
  39. package/modules/resources/ICONS/libraries/heroicons/arrow-right.svg +0 -4
  40. package/modules/resources/ICONS/libraries/heroicons/check.svg +0 -4
  41. package/modules/resources/ICONS/libraries/heroicons/chevron-down.svg +0 -4
  42. package/modules/resources/ICONS/libraries/heroicons/cog-6-tooth.svg +0 -5
  43. package/modules/resources/ICONS/libraries/heroicons/magnifying-glass.svg +0 -4
  44. package/modules/resources/ICONS/libraries/heroicons/x-mark.svg +0 -4
  45. package/modules/resources/ICONS/libraries/lucide/LICENSE +0 -40
  46. package/modules/resources/ICONS/libraries/lucide/README.md +0 -15
  47. package/modules/resources/ICONS/libraries/lucide/arrow-right.svg +0 -15
  48. package/modules/resources/ICONS/libraries/lucide/check.svg +0 -14
  49. package/modules/resources/ICONS/libraries/lucide/chevron-down.svg +0 -14
  50. package/modules/resources/ICONS/libraries/lucide/search.svg +0 -15
  51. package/modules/resources/ICONS/libraries/lucide/settings.svg +0 -15
  52. package/modules/resources/ICONS/libraries/lucide/x.svg +0 -15
  53. package/modules/resources/ICONS/rules.md +0 -7
  54. package/modules/resources/README.md +0 -9
  55. package/modules/resources/UI/ANTI-PATTERNS.md +0 -6
  56. package/modules/resources/UI/TONE.md +0 -6
  57. package/scripts/chat-app-smoke.js +0 -30
  58. package/scripts/global-chat-switch-benchmark.js +0 -406
  59. package/scripts/ink-demo.js +0 -23
  60. package/scripts/ink-smoke.js +0 -30
  61. package/scripts/ucode-app-smoke.js +0 -36
  62. /package/{modules/bus/SKILLS → SKILLS}/ubus/SKILL.md +0 -0
  63. /package/{modules/context/SKILLS → SKILLS}/uctx/SKILL.md +0 -0
  64. /package/{modules/online/SKILLS → SKILLS}/ufoo-online/SKILL.md +0 -0
@@ -1,406 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { spawn, spawnSync } = require("child_process");
6
- const UfooInit = require("../src/app/cli/features/init");
7
- const { socketPath, isRunning } = require("../src/runtime/daemon");
8
- const { connectWithRetry } = require("../src/app/chat/transport");
9
- const { createDaemonTransport } = require("../src/app/chat/daemonTransport");
10
- const { createDaemonCoordinator } = require("../src/app/chat/daemonCoordinator");
11
-
12
- function sleep(ms) {
13
- return new Promise((resolve) => setTimeout(resolve, ms));
14
- }
15
-
16
- function parseIntArg(argv, flag, fallback) {
17
- const idx = argv.indexOf(flag);
18
- if (idx < 0 || idx + 1 >= argv.length) return fallback;
19
- const parsed = Number.parseInt(String(argv[idx + 1] || ""), 10);
20
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
21
- }
22
-
23
- function parseStringArg(argv, flag, fallback) {
24
- const idx = argv.indexOf(flag);
25
- if (idx < 0 || idx + 1 >= argv.length) return fallback;
26
- const value = String(argv[idx + 1] || "").trim();
27
- return value || fallback;
28
- }
29
-
30
- function hasFlag(argv, flag) {
31
- return argv.includes(flag);
32
- }
33
-
34
- function percentile(sortedValues, p) {
35
- if (!Array.isArray(sortedValues) || sortedValues.length === 0) return 0;
36
- const clamped = Math.max(0, Math.min(1, p));
37
- const idx = Math.ceil(clamped * sortedValues.length) - 1;
38
- const safeIdx = Math.max(0, Math.min(sortedValues.length - 1, idx));
39
- return sortedValues[safeIdx];
40
- }
41
-
42
- function summarizeDurations(values) {
43
- if (!Array.isArray(values) || values.length === 0) {
44
- return {
45
- count: 0,
46
- minMs: 0,
47
- maxMs: 0,
48
- avgMs: 0,
49
- p50Ms: 0,
50
- p95Ms: 0,
51
- };
52
- }
53
- const sorted = [...values].sort((a, b) => a - b);
54
- const total = values.reduce((sum, n) => sum + n, 0);
55
- return {
56
- count: values.length,
57
- minMs: sorted[0],
58
- maxMs: sorted[sorted.length - 1],
59
- avgMs: total / values.length,
60
- p50Ms: percentile(sorted, 0.5),
61
- p95Ms: percentile(sorted, 0.95),
62
- };
63
- }
64
-
65
- function normalizeProjectRootForCompare(projectRoot) {
66
- const raw = String(projectRoot || "").trim();
67
- if (!raw) return "";
68
- try {
69
- return fs.realpathSync.native(raw);
70
- } catch {
71
- return path.resolve(raw);
72
- }
73
- }
74
-
75
- async function waitForDaemonReady(projectRoot, timeoutMs = 20000) {
76
- const startedAt = Date.now();
77
- while (Date.now() - startedAt < timeoutMs) {
78
- if (isRunning(projectRoot)) {
79
- const client = await connectWithRetry(socketPath(projectRoot), 1, 0);
80
- if (client) {
81
- try {
82
- client.end();
83
- client.destroy();
84
- } catch {
85
- // ignore
86
- }
87
- return true;
88
- }
89
- }
90
- // eslint-disable-next-line no-await-in-loop
91
- await sleep(150);
92
- }
93
- return false;
94
- }
95
-
96
- async function waitForDaemonStopped(projectRoot, timeoutMs = 10000) {
97
- const startedAt = Date.now();
98
- while (Date.now() - startedAt < timeoutMs) {
99
- if (!isRunning(projectRoot)) return true;
100
- // eslint-disable-next-line no-await-in-loop
101
- await sleep(120);
102
- }
103
- return !isRunning(projectRoot);
104
- }
105
-
106
- function createStatusWaiter() {
107
- let latestProjectRoot = "";
108
- const waiting = new Set();
109
- const seen = [];
110
-
111
- function settle(targetRoot, ok, error) {
112
- for (const waiter of Array.from(waiting)) {
113
- if (waiter.targetRoot !== targetRoot) continue;
114
- waiting.delete(waiter);
115
- if (ok) waiter.resolve(targetRoot);
116
- else waiter.reject(error || new Error(`status wait failed: ${targetRoot}`));
117
- }
118
- }
119
-
120
- function handleMessage(msg) {
121
- const type = msg && msg.type ? String(msg.type) : "";
122
- seen.push({
123
- ts: Date.now(),
124
- type: type || "unknown",
125
- projectRoot: msg && msg.data && msg.data.projectRoot ? String(msg.data.projectRoot) : "",
126
- });
127
- if (seen.length > 50) {
128
- seen.shift();
129
- }
130
- if (!msg || msg.type !== "status") return false;
131
- const rootRaw = msg.data && msg.data.projectRoot ? String(msg.data.projectRoot) : "";
132
- const root = normalizeProjectRootForCompare(rootRaw);
133
- if (!root) return false;
134
- latestProjectRoot = root;
135
- settle(root, true);
136
- return false;
137
- }
138
-
139
- function waitForProject(targetRoot, timeoutMs = 5000) {
140
- const normalizedTarget = normalizeProjectRootForCompare(targetRoot);
141
- if (!normalizedTarget) {
142
- return Promise.reject(new Error("invalid target root for status wait"));
143
- }
144
- if (latestProjectRoot === normalizedTarget) return Promise.resolve(normalizedTarget);
145
- return new Promise((resolve, reject) => {
146
- const waiter = { targetRoot: normalizedTarget, resolve, reject, timer: null };
147
- waiter.timer = setTimeout(() => {
148
- waiting.delete(waiter);
149
- const seenTail = seen.slice(-5)
150
- .map((entry) => `${entry.type}:${entry.projectRoot || "-"}`)
151
- .join(", ");
152
- reject(new Error(`timeout waiting status for ${normalizedTarget}; seen=[${seenTail}]`));
153
- }, timeoutMs);
154
- const wrappedResolve = (value) => {
155
- clearTimeout(waiter.timer);
156
- resolve(value);
157
- };
158
- const wrappedReject = (err) => {
159
- clearTimeout(waiter.timer);
160
- reject(err);
161
- };
162
- waiting.add({
163
- targetRoot: normalizedTarget,
164
- resolve: wrappedResolve,
165
- reject: wrappedReject,
166
- });
167
- });
168
- }
169
-
170
- function clearAll(err) {
171
- for (const waiter of Array.from(waiting)) {
172
- waiting.delete(waiter);
173
- waiter.reject(err || new Error("status waiter cleared"));
174
- }
175
- }
176
-
177
- return {
178
- handleMessage,
179
- waitForProject,
180
- clearAll,
181
- getSeen: () => seen.slice(),
182
- };
183
- }
184
-
185
- async function main() {
186
- const argv = process.argv.slice(2);
187
- const switches = parseIntArg(argv, "--switches", 50);
188
- const keepTmp = hasFlag(argv, "--keep-tmp");
189
- const jsonOnly = hasFlag(argv, "--json");
190
- const tempParent = parseStringArg(argv, "--tmp-root", "/tmp");
191
-
192
- const tempRoot = fs.mkdtempSync(path.join(tempParent, "ufoo-global-switch-bench-"));
193
- const projectA = path.join(tempRoot, "project-a");
194
- const projectB = path.join(tempRoot, "project-b");
195
- fs.mkdirSync(projectA, { recursive: true });
196
- fs.mkdirSync(projectB, { recursive: true });
197
-
198
- let coordinator = null;
199
- const statusWaiter = createStatusWaiter();
200
- const daemonProcesses = new Map();
201
- let exitCode = 0;
202
- const errors = [];
203
- const daemonBin = path.resolve(__dirname, "..", "bin", "ufoo.js");
204
-
205
- function startManagedDaemon(projectRoot) {
206
- const existing = daemonProcesses.get(projectRoot);
207
- if (existing && !existing.child.killed && existing.child.exitCode === null) {
208
- return existing.child;
209
- }
210
- const child = spawn(process.execPath, [daemonBin, "daemon", "start"], {
211
- cwd: projectRoot,
212
- env: { ...process.env, UFOO_DAEMON_CHILD: "1" },
213
- stdio: ["ignore", "pipe", "pipe"],
214
- });
215
- const logs = { stdout: "", stderr: "" };
216
- child.stdout.on("data", (chunk) => {
217
- logs.stdout += String(chunk || "");
218
- if (logs.stdout.length > 8000) logs.stdout = logs.stdout.slice(-8000);
219
- });
220
- child.stderr.on("data", (chunk) => {
221
- logs.stderr += String(chunk || "");
222
- if (logs.stderr.length > 8000) logs.stderr = logs.stderr.slice(-8000);
223
- });
224
- daemonProcesses.set(projectRoot, { child, logs });
225
- return child;
226
- }
227
-
228
- function stopManagedDaemon(projectRoot) {
229
- try {
230
- spawnSync(process.execPath, [daemonBin, "daemon", "stop"], {
231
- cwd: projectRoot,
232
- stdio: "ignore",
233
- });
234
- } catch {
235
- // ignore
236
- }
237
- }
238
-
239
- try {
240
- const init = new UfooInit(path.resolve(__dirname, ".."));
241
- await init.init({ modules: "context,bus", project: projectA });
242
- await init.init({ modules: "context,bus", project: projectB });
243
-
244
- startManagedDaemon(projectA);
245
- startManagedDaemon(projectB);
246
- const readyA = await waitForDaemonReady(projectA);
247
- const readyB = await waitForDaemonReady(projectB);
248
- if (!readyA || !readyB) {
249
- const aMeta = daemonProcesses.get(projectA);
250
- const bMeta = daemonProcesses.get(projectB);
251
- const aErr = aMeta ? aMeta.logs.stderr || aMeta.logs.stdout : "";
252
- const bErr = bMeta ? bMeta.logs.stderr || bMeta.logs.stdout : "";
253
- if (aErr) errors.push(`daemon A log: ${aErr.trim().slice(-400)}`);
254
- if (bErr) errors.push(`daemon B log: ${bErr.trim().slice(-400)}`);
255
- throw new Error(`daemon readiness failed: A=${readyA} B=${readyB}`);
256
- }
257
-
258
- const transport = createDaemonTransport({
259
- projectRoot: projectA,
260
- sockPath: socketPath(projectA),
261
- isRunning,
262
- startDaemon: startManagedDaemon,
263
- connectWithRetry,
264
- primaryRetries: 12,
265
- secondaryRetries: 20,
266
- retryDelayMs: 80,
267
- restartDelayMs: 600,
268
- });
269
-
270
- coordinator = createDaemonCoordinator({
271
- projectRoot: projectA,
272
- daemonTransport: transport,
273
- handleMessage: statusWaiter.handleMessage,
274
- queueStatusLine: () => {},
275
- resolveStatusLine: () => {},
276
- logMessage: () => {},
277
- stopDaemon: stopManagedDaemon,
278
- startDaemon: startManagedDaemon,
279
- });
280
-
281
- const connected = await coordinator.connect();
282
- if (!connected) {
283
- throw new Error("initial coordinator.connect() failed");
284
- }
285
- coordinator.requestStatus();
286
- await statusWaiter.waitForProject(projectA, 5000);
287
-
288
- const durations = [];
289
- let routingChecksPassed = 0;
290
-
291
- for (let i = 0; i < switches; i += 1) {
292
- const targetRoot = i % 2 === 0 ? projectB : projectA;
293
- const startedNs = process.hrtime.bigint();
294
- // eslint-disable-next-line no-await-in-loop
295
- const result = await coordinator.switchProject({
296
- projectRoot: targetRoot,
297
- sockPath: socketPath(targetRoot),
298
- });
299
- const durationMs = Number(process.hrtime.bigint() - startedNs) / 1e6;
300
- durations.push(durationMs);
301
- if (!result || result.ok !== true) {
302
- errors.push(`switch ${i + 1} failed: ${(result && result.error) || "unknown"}`);
303
- continue;
304
- }
305
- try {
306
- // eslint-disable-next-line no-await-in-loop
307
- await statusWaiter.waitForProject(targetRoot, 5000);
308
- routingChecksPassed += 1;
309
- } catch (err) {
310
- errors.push(`switch ${i + 1} status mismatch: ${err.message || err}`);
311
- }
312
- }
313
-
314
- const summary = summarizeDurations(durations);
315
- const thresholds = {
316
- p50MsLt500: summary.p50Ms < 500,
317
- p95MsLt1200: summary.p95Ms < 1200,
318
- };
319
- const routeOk = routingChecksPassed === switches;
320
- const pass = routeOk && thresholds.p50MsLt500 && thresholds.p95MsLt1200 && errors.length === 0;
321
- exitCode = pass ? 0 : 2;
322
-
323
- const report = {
324
- switches,
325
- routingChecksPassed,
326
- routeOk,
327
- summary,
328
- thresholds,
329
- pass,
330
- tempRoot,
331
- errors,
332
- };
333
-
334
- if (jsonOnly) {
335
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
336
- } else {
337
- process.stdout.write("=== Global Chat Switch Benchmark ===\n");
338
- process.stdout.write(`tempRoot: ${tempRoot}\n`);
339
- process.stdout.write(`switches: ${switches}\n`);
340
- process.stdout.write(`routing checks: ${routingChecksPassed}/${switches} (${routeOk ? "PASS" : "FAIL"})\n`);
341
- process.stdout.write(
342
- `latency ms: min=${summary.minMs.toFixed(2)} avg=${summary.avgMs.toFixed(2)} ` +
343
- `p50=${summary.p50Ms.toFixed(2)} p95=${summary.p95Ms.toFixed(2)} max=${summary.maxMs.toFixed(2)}\n`
344
- );
345
- process.stdout.write(
346
- `thresholds: p50<500=${thresholds.p50MsLt500 ? "PASS" : "FAIL"} ` +
347
- `p95<1200=${thresholds.p95MsLt1200 ? "PASS" : "FAIL"}\n`
348
- );
349
- if (errors.length > 0) {
350
- process.stdout.write("errors:\n");
351
- errors.forEach((line) => process.stdout.write(`- ${line}\n`));
352
- }
353
- process.stdout.write(`overall: ${pass ? "PASS" : "FAIL"}\n`);
354
- }
355
- } finally {
356
- statusWaiter.clearAll(new Error("benchmark teardown"));
357
- if (coordinator) {
358
- try {
359
- coordinator.close();
360
- } catch {
361
- // ignore
362
- }
363
- }
364
- try {
365
- stopManagedDaemon(projectA);
366
- } catch {
367
- // ignore
368
- }
369
- try {
370
- stopManagedDaemon(projectB);
371
- } catch {
372
- // ignore
373
- }
374
- for (const [projectRoot, meta] of daemonProcesses.entries()) {
375
- const child = meta && meta.child;
376
- if (!child || child.exitCode !== null) continue;
377
- try {
378
- child.kill("SIGTERM");
379
- } catch {
380
- // ignore
381
- }
382
- // Ensure child cannot leak if SIGTERM is ignored.
383
- await sleep(80);
384
- if (child.exitCode === null) {
385
- try {
386
- child.kill("SIGKILL");
387
- } catch {
388
- // ignore
389
- }
390
- }
391
- daemonProcesses.delete(projectRoot);
392
- }
393
- await waitForDaemonStopped(projectA, 8000);
394
- await waitForDaemonStopped(projectB, 8000);
395
- if (!keepTmp) {
396
- fs.rmSync(tempRoot, { recursive: true, force: true });
397
- }
398
- }
399
-
400
- process.exit(exitCode);
401
- }
402
-
403
- main().catch((err) => {
404
- process.stderr.write(`${err && err.stack ? err.stack : err}\n`);
405
- process.exit(1);
406
- });
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- /**
5
- * Interactive ink demo. Run from a real TTY:
6
- * npm run ink:demo
7
- * or
8
- * node scripts/ink-demo.js
9
- */
10
-
11
- const { runInk } = require("../src/ui/runInk");
12
- const { createInkDemo } = require("../src/ui/ink/InkDemo");
13
-
14
- (async () => {
15
- const handle = await runInk((React, ink) => {
16
- const InkDemo = createInkDemo({ React, ink, interactive: true });
17
- return React.createElement(InkDemo);
18
- });
19
- await handle.waitUntilExit();
20
- })().catch((err) => {
21
- process.stderr.write(`ink-demo: ${err && err.stack || err}\n`);
22
- process.exit(1);
23
- });
@@ -1,30 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Headless smoke test for the ink demo: mounts the component, lets it tick
5
- * once, then unmounts. Exits with non-zero on any error. Used to confirm
6
- * the CJS->ESM bridge and component tree work without occupying the TTY.
7
- */
8
-
9
- const { runInk } = require("../src/ui/runInk");
10
- const { createInkDemo } = require("../src/ui/ink/InkDemo");
11
-
12
- (async () => {
13
- const handle = await runInk((React, ink) => {
14
- const InkDemo = createInkDemo({ React, ink, interactive: false });
15
- return React.createElement(InkDemo);
16
- }, {
17
- stdout: process.stdout,
18
- stderr: process.stderr,
19
- stdin: process.stdin,
20
- exitOnCtrlC: false,
21
- });
22
- await new Promise((r) => setTimeout(r, 100));
23
- handle.unmount();
24
- await handle.waitUntilExit().catch(() => undefined);
25
- process.stdout.write("\nink-smoke: ok\n");
26
- process.exit(0);
27
- })().catch((err) => {
28
- process.stderr.write(`ink-smoke: failed: ${err && err.stack || err}\n`);
29
- process.exit(1);
30
- });
@@ -1,36 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Headless mount test for the UcodeApp shell. Boots the ink TUI with stub
5
- * runner props, lets it render once, then unmounts. Used to confirm the ink
6
- * code path stays compilable as P1 evolves.
7
- */
8
-
9
- const { runInk } = require("../src/ui/runInk");
10
- const { createUcodeApp } = require("../src/ui/ink/UcodeApp");
11
-
12
- (async () => {
13
- const props = {
14
- stdin: process.stdin,
15
- stdout: process.stdout,
16
- runSingleCommand: () => ({ kind: "empty" }),
17
- runNaturalLanguageTask: async () => ({ ok: true, summary: "ok" }),
18
- runUbusCommand: async () => ({ ok: false, error: "ubus unsupported", summary: "" }),
19
- formatNlResult: () => "ok",
20
- workspaceRoot: process.cwd(),
21
- state: { model: "test-model", sessionId: "smoke", engine: "ufoo-core" },
22
- autoBus: { enabled: false, getPendingCount: () => 0, subscriberId: "" },
23
- };
24
- const handle = await runInk((React, ink) => {
25
- const UcodeApp = createUcodeApp({ React, ink, props, interactive: false });
26
- return React.createElement(UcodeApp);
27
- }, { stdout: process.stdout, stderr: process.stderr });
28
- await new Promise((r) => setTimeout(r, 80));
29
- handle.unmount();
30
- await handle.waitUntilExit().catch(() => undefined);
31
- process.stdout.write("\nucode-app-smoke: ok\n");
32
- process.exit(0);
33
- })().catch((err) => {
34
- process.stderr.write(`ucode-app-smoke: failed: ${err && err.stack || err}\n`);
35
- process.exit(1);
36
- });
File without changes