@fui-org/fui-cli 2.1.1 → 2.1.3
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-
|
|
2
|
+
import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-hk5frcn2.js");await e();
|
package/package.json
CHANGED
package/skills/fui/SKILL.md
CHANGED
|
@@ -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
|
|
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,49 @@ 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
|
|
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, current: FuiTarget): Promise<FuiDiff | undefined> {
|
|
39
|
+
const args = ["remote", "diff", "--stat", "--json"];
|
|
40
|
+
if (current.project && current.module) args.push("-p", current.project, "-m", current.module);
|
|
41
|
+
const { exitCode, stdout } = await runFui($, args, session).catch(() => ({
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
stdout: "",
|
|
44
|
+
}));
|
|
45
|
+
if (exitCode !== 0) return undefined;
|
|
46
|
+
try {
|
|
47
|
+
const rows = JSON.parse(stdout.slice(stdout.indexOf("["))) as { added?: number; removed?: number }[];
|
|
48
|
+
return {
|
|
49
|
+
files: rows.length,
|
|
50
|
+
added: rows.reduce((sum, row) => sum + (row.added ?? 0), 0),
|
|
51
|
+
removed: rows.reduce((sum, row) => sum + (row.removed ?? 0), 0),
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
20
58
|
async function readJson($: Engine, path: string): Promise<unknown> {
|
|
21
59
|
try {
|
|
22
60
|
return JSON.parse(await $.fs.read(path));
|
|
@@ -64,22 +102,96 @@ async function projectUrl($: Engine, session: SessionTarget) {
|
|
|
64
102
|
return httpsUrl(firstDomain(info?.pDomain));
|
|
65
103
|
}
|
|
66
104
|
|
|
67
|
-
async function
|
|
68
|
-
const state = (await readJson($, await sessionFile($, command))) as { target?: SessionTarget } | undefined;
|
|
69
|
-
const session = state?.target;
|
|
105
|
+
async function describe($: Engine, session: SessionTarget | undefined, lastCommand?: string): Promise<FuiTarget> {
|
|
70
106
|
const found: FuiTarget = { lastCommand };
|
|
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
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readSessionTarget($: Engine, command: string) {
|
|
118
|
+
const state = (await readJson($, await sessionFile($, command))) as { target?: SessionTarget } | undefined;
|
|
119
|
+
return state?.target;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A folder holding _db or _projectInfo.json is a fui project; <project>/modules/<module> inside it is a module.
|
|
123
|
+
async function folderTarget($: Engine, cwd: string): Promise<SessionTarget | undefined> {
|
|
124
|
+
const parts = cwd.replace(/\/+$/, "").split("/");
|
|
125
|
+
for (let end = parts.length; end > 1; end--) {
|
|
126
|
+
const projectDir = parts.slice(0, end).join("/");
|
|
127
|
+
if (!(await $.fs.exists(`${projectDir}/_db`)) && !(await $.fs.exists(`${projectDir}/_projectInfo.json`))) continue;
|
|
128
|
+
const info = (await readJson($, `${projectDir}/_projectInfo.json`)) as { ProjectID?: unknown } | undefined;
|
|
129
|
+
const projectId = typeof info?.ProjectID === "string" ? info.ProjectID : parts[end - 1];
|
|
130
|
+
const workspaceRoot = parts.slice(0, end - 1).join("/");
|
|
131
|
+
const moduleId = parts[end] === "modules" ? parts[end + 1] : undefined;
|
|
132
|
+
if (moduleId) return { projectId, moduleId, moduleDir: `${projectDir}/modules/${moduleId}`, workspaceRoot };
|
|
133
|
+
const active = await readSessionTarget($, "");
|
|
134
|
+
return active?.projectId === projectId ? active : { projectId, workspaceRoot };
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let session: string | undefined;
|
|
140
|
+
let isPinned = false;
|
|
141
|
+
let isStale = false;
|
|
142
|
+
let running: Promise<void> | undefined;
|
|
143
|
+
|
|
144
|
+
async function showDiff($: Engine, diff: FuiTarget["diff"]) {
|
|
145
|
+
const current: FuiTarget | null = await read($, target);
|
|
146
|
+
if (current?.moduleDir) await update($, target, () => ({ ...current, diff }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function refreshDiff($: Engine) {
|
|
150
|
+
isStale = false;
|
|
151
|
+
running ??= (async () => {
|
|
152
|
+
do {
|
|
153
|
+
isStale = false;
|
|
154
|
+
await showDiff($, "pending");
|
|
155
|
+
const current: FuiTarget | null = await read($, target);
|
|
156
|
+
if (current?.moduleDir) await showDiff($, await remoteDiff($, current));
|
|
157
|
+
} while (isStale);
|
|
158
|
+
})().finally(() => {
|
|
159
|
+
running = undefined;
|
|
160
|
+
});
|
|
78
161
|
}
|
|
79
162
|
|
|
80
163
|
export const register: Register = (on) => {
|
|
164
|
+
on("session.start", async ($, e, next) => {
|
|
165
|
+
const started = await next(e);
|
|
166
|
+
const folder = await folderTarget($, e.cwd);
|
|
167
|
+
isPinned = folder !== undefined;
|
|
168
|
+
if (folder) {
|
|
169
|
+
await update($, target, () => null);
|
|
170
|
+
const found = await describe($, folder);
|
|
171
|
+
await update($, target, () => found);
|
|
172
|
+
refreshDiff($);
|
|
173
|
+
}
|
|
174
|
+
return started;
|
|
175
|
+
});
|
|
176
|
+
|
|
81
177
|
on("prompt.submit", async ($, e, next) => {
|
|
82
|
-
await update($, target, () => null);
|
|
178
|
+
if (!isPinned) await update($, target, () => null);
|
|
179
|
+
return next(e);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
on("tool.call", async ($, e, next) => {
|
|
183
|
+
const result = await next(e);
|
|
184
|
+
if (!EDIT_TOOLS.has(e.tool)) return result;
|
|
185
|
+
const current: FuiTarget | null = await read($, target);
|
|
186
|
+
const path =
|
|
187
|
+
(e as { file_path?: string; notebook_path?: string }).file_path ??
|
|
188
|
+
(e as { notebook_path?: string }).notebook_path;
|
|
189
|
+
if (current?.moduleDir && path?.startsWith(`${current.moduleDir}/`)) isStale = true;
|
|
190
|
+
return result;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
on("turn.complete", async ($, e, next) => {
|
|
194
|
+
if (isStale) refreshDiff($);
|
|
83
195
|
return next(e);
|
|
84
196
|
});
|
|
85
197
|
|
|
@@ -88,7 +200,14 @@ export const register: Register = (on) => {
|
|
|
88
200
|
const call = e.command.match(FUI_CALL);
|
|
89
201
|
if (!call) return result;
|
|
90
202
|
|
|
91
|
-
|
|
203
|
+
session = sessionKey(e.command);
|
|
204
|
+
const found = await describe(
|
|
205
|
+
$,
|
|
206
|
+
await readSessionTarget($, e.command),
|
|
207
|
+
[call[1], call[2]].filter(Boolean).join(" "),
|
|
208
|
+
);
|
|
209
|
+
await update($, target, () => found);
|
|
210
|
+
if (!(call[1] === "remote" && call[2] === "diff")) refreshDiff($);
|
|
92
211
|
return result;
|
|
93
212
|
});
|
|
94
213
|
|
|
@@ -120,7 +239,23 @@ export const register: Register = (on) => {
|
|
|
120
239
|
) : (
|
|
121
240
|
<Text dimColor>no active module</Text>
|
|
122
241
|
)}
|
|
123
|
-
{current.
|
|
242
|
+
{current.diff === "pending" && <Text dimColor> · diff…</Text>}
|
|
243
|
+
{current.diff && current.diff !== "pending" && (
|
|
244
|
+
<Text>
|
|
245
|
+
<Text dimColor> · </Text>
|
|
246
|
+
{current.diff.files === 0 ? (
|
|
247
|
+
<Text dimColor>in sync</Text>
|
|
248
|
+
) : (
|
|
249
|
+
<Text>
|
|
250
|
+
<Text>{`${current.diff.files} file${current.diff.files === 1 ? "" : "s"} `}</Text>
|
|
251
|
+
<Text color="green">{`+${current.diff.added}`}</Text>
|
|
252
|
+
<Text> </Text>
|
|
253
|
+
<Text color="red">{`−${current.diff.removed}`}</Text>
|
|
254
|
+
</Text>
|
|
255
|
+
)}
|
|
256
|
+
</Text>
|
|
257
|
+
)}
|
|
258
|
+
{current.lastCommand && <Text dimColor>{` · fui ${current.lastCommand}`}</Text>}
|
|
124
259
|
</Box>
|
|
125
260
|
);
|
|
126
261
|
});
|
|
@@ -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" {
|