@fui-org/fui-cli 2.1.1 → 2.1.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.
package/dist/fui.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-kchph3mb.js");await e();
2
+ import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-x86j3m2z.js");await e();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fui-org/fui-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Token-lean CLI for the FUI low-code platform (Node.js)",
5
5
  "keywords": [
6
6
  "ai",
@@ -67,7 +67,7 @@ description: Use the `fui` CLI for all FUI (low-code platform) work — projects
67
67
  - Unchanged files are skipped via the publish ledger.
68
68
  5. `fui file status` lists what is still unpublished.
69
69
 
70
- `fui remote module --part ui|html|script|css|load|components|imports` and `fui remote project --part config|modules|…` show the server copy without touching the workspace. `fui remote diff <path...>` compares local files with the server.
70
+ `fui remote module --part ui|html|script|css|load|components|imports` and `fui remote project --part config|modules|…` show the server copy without touching the workspace. `fui remote diff [path...]` compares local files with the server (no path: the whole target module); `--stat` adds the +/- line counts.
71
71
 
72
72
  Ask the user before `module delete`, `component delete`, `import delete` and `user remove`.
73
73
 
@@ -1,7 +1,7 @@
1
1
  import { atom, read, update } from "claude-code";
2
2
  import type { EngineInterface as Engine, Register } from "claude-code";
3
3
 
4
- import type { FuiTarget } from "../types";
4
+ import type { FuiDiff, FuiTarget } from "../types";
5
5
 
6
6
  type SessionTarget = { projectId?: string; moduleId?: string; moduleDir?: string; workspaceRoot?: string };
7
7
 
@@ -12,11 +12,47 @@ const FUI_CALL = /(?:^|[;&|(]|\bthen|\bdo)\s*(?:[A-Za-z_]\w*=\S*\s+)*(?:bunx\s+)
12
12
 
13
13
  // Mirrors sessionFile() in src/core/session/state.ts.
14
14
  async function sessionFile($: Engine, command: string) {
15
- const key = command.match(/FUI_SESSION=([A-Za-z0-9_-]+)/)?.[1] ?? (await $.env.get("FUI_SESSION")) ?? "default";
15
+ const key = sessionKey(command) ?? (await $.env.get("FUI_SESSION")) ?? "default";
16
16
  const dir = (await $.env.get("FUI_SESSION_DIR")) ?? `${await $.env.get("HOME")}/.fui/session`;
17
17
  return `${dir}/cli-${key}.json`;
18
18
  }
19
19
 
20
+ const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
21
+
22
+ function sessionKey(command: string) {
23
+ return command.match(/FUI_SESSION=([A-Za-z0-9_-]+)/)?.[1];
24
+ }
25
+
26
+ async function runFui($: Engine, args: string[], session: string | undefined) {
27
+ const env = session ? { FUI_SESSION: session } : undefined;
28
+ try {
29
+ return await $.process.run(["fui", ...args], { env, timeoutMs: 30_000 });
30
+ } catch {
31
+ // fui is often on a PATH only the login shell sets up (bun, nvm).
32
+ const shell = (await $.env.get("SHELL")) ?? "/bin/zsh";
33
+ return await $.process.run([shell, "-lc", `fui ${args.join(" ")}`], { env, timeoutMs: 30_000 });
34
+ }
35
+ }
36
+
37
+ // Runs in the background: the numbers land on the line, never in the model's context.
38
+ async function remoteDiff($: Engine, session: string | undefined): Promise<FuiDiff | undefined> {
39
+ const { exitCode, stdout } = await runFui($, ["remote", "diff", "--stat", "--json"], session).catch(() => ({
40
+ exitCode: 1,
41
+ stdout: "",
42
+ }));
43
+ if (exitCode !== 0) return undefined;
44
+ try {
45
+ const rows = JSON.parse(stdout.slice(stdout.indexOf("["))) as { added?: number; removed?: number }[];
46
+ return {
47
+ files: rows.length,
48
+ added: rows.reduce((sum, row) => sum + (row.added ?? 0), 0),
49
+ removed: rows.reduce((sum, row) => sum + (row.removed ?? 0), 0),
50
+ };
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
20
56
  async function readJson($: Engine, path: string): Promise<unknown> {
21
57
  try {
22
58
  return JSON.parse(await $.fs.read(path));
@@ -71,24 +107,65 @@ async function sync($: Engine, command: string, lastCommand: string) {
71
107
  if (session?.projectId) {
72
108
  found.project = session.projectId;
73
109
  found.module = session.moduleId;
110
+ found.moduleDir = session.moduleDir;
74
111
  found.projectUrl = await projectUrl($, session);
75
112
  found.moduleUrl = session.moduleId ? await moduleUrl($, session) : undefined;
76
113
  }
77
114
  await update($, target, () => found);
78
115
  }
79
116
 
117
+ let session: string | undefined;
118
+ let isStale = false;
119
+ let running: Promise<void> | undefined;
120
+
121
+ async function showDiff($: Engine, diff: FuiTarget["diff"]) {
122
+ const current: FuiTarget | null = await read($, target);
123
+ if (current?.moduleDir) await update($, target, () => ({ ...current, diff }));
124
+ }
125
+
126
+ function refreshDiff($: Engine) {
127
+ isStale = false;
128
+ running ??= (async () => {
129
+ do {
130
+ isStale = false;
131
+ await showDiff($, "pending");
132
+ await showDiff($, await remoteDiff($, session));
133
+ } while (isStale);
134
+ })().finally(() => {
135
+ running = undefined;
136
+ });
137
+ }
138
+
80
139
  export const register: Register = (on) => {
81
140
  on("prompt.submit", async ($, e, next) => {
82
141
  await update($, target, () => null);
83
142
  return next(e);
84
143
  });
85
144
 
145
+ on("tool.call", async ($, e, next) => {
146
+ const result = await next(e);
147
+ if (!EDIT_TOOLS.has(e.tool)) return result;
148
+ const current: FuiTarget | null = await read($, target);
149
+ const path =
150
+ (e as { file_path?: string; notebook_path?: string }).file_path ??
151
+ (e as { notebook_path?: string }).notebook_path;
152
+ if (current?.moduleDir && path?.startsWith(`${current.moduleDir}/`)) isStale = true;
153
+ return result;
154
+ });
155
+
156
+ on("turn.complete", async ($, e, next) => {
157
+ if (isStale) refreshDiff($);
158
+ return next(e);
159
+ });
160
+
86
161
  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
87
162
  const result = await next(e);
88
163
  const call = e.command.match(FUI_CALL);
89
164
  if (!call) return result;
90
165
 
166
+ session = sessionKey(e.command);
91
167
  await sync($, e.command, [call[1], call[2]].filter(Boolean).join(" "));
168
+ if (!(call[1] === "remote" && call[2] === "diff")) refreshDiff($);
92
169
  return result;
93
170
  });
94
171
 
@@ -120,7 +197,23 @@ export const register: Register = (on) => {
120
197
  ) : (
121
198
  <Text dimColor>no active module</Text>
122
199
  )}
123
- {current.lastCommand && <Text dimColor> · fui {current.lastCommand}</Text>}
200
+ {current.diff === "pending" && <Text dimColor> · diff…</Text>}
201
+ {current.diff && current.diff !== "pending" && (
202
+ <Text>
203
+ <Text dimColor> · </Text>
204
+ {current.diff.files === 0 ? (
205
+ <Text dimColor>in sync</Text>
206
+ ) : (
207
+ <Text>
208
+ <Text>{`${current.diff.files} file${current.diff.files === 1 ? "" : "s"} `}</Text>
209
+ <Text color="green">{`+${current.diff.added}`}</Text>
210
+ <Text> </Text>
211
+ <Text color="red">{`−${current.diff.removed}`}</Text>
212
+ </Text>
213
+ )}
214
+ </Text>
215
+ )}
216
+ {current.lastCommand && <Text dimColor>{` · fui ${current.lastCommand}`}</Text>}
124
217
  </Box>
125
218
  );
126
219
  });
@@ -1,9 +1,13 @@
1
+ export type FuiDiff = { files: number; added: number; removed: number };
2
+
1
3
  export type FuiTarget = {
2
4
  project?: string;
3
5
  module?: string;
6
+ moduleDir?: string;
4
7
  projectUrl?: string;
5
8
  moduleUrl?: string;
6
9
  lastCommand?: string;
10
+ diff?: FuiDiff | "pending";
7
11
  };
8
12
 
9
13
  declare module "claude-code" {