@flood-md/cli 0.2.2 → 0.2.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/README.md +15 -2
- package/lib/index.mjs +89 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,21 @@
|
|
|
4
4
|
`npx @flood-md/cli doctor` checks package versions and agent file presence.
|
|
5
5
|
|
|
6
6
|
Requires Node 22.12+. Installs matching tokens, React and agent packages; creates
|
|
7
|
-
flood.json
|
|
8
|
-
|
|
7
|
+
flood.json and every owned guide listed in the matching agent manifest: DESIGN.md,
|
|
8
|
+
rules.json and manifest.json under flood/, plus SKILL.md and focused references
|
|
9
|
+
under .agents/skills/flood-ui/. Version 0.2.3 includes the expanded bundle and
|
|
10
|
+
spacing reference. Refuses existing owned files or linked destinations.
|
|
11
|
+
Does not rewrite source code, client settings or AGENTS.md.
|
|
9
12
|
After init, import @flood-md/react/styles.css once at the application root.
|
|
10
13
|
Wrap new interfaces in FloodRoot from @flood-md/react to select the dark
|
|
11
14
|
foundations profile. Read the installed flood/DESIGN.md for the matching API.
|
|
15
|
+
Read the skill's routing reference for the task/source/check matrix. Doctor checks
|
|
16
|
+
the complete guide set and package alignment; it does not validate application
|
|
17
|
+
behavior, CSS imports or agent compliance. Merge customized guide upgrades
|
|
18
|
+
deliberately; init has no overwrite mode.
|
|
19
|
+
|
|
20
|
+
Init and doctor reject linked project-directory ancestry. Init also requires a
|
|
21
|
+
regular nonlinked package.json (including no hardlinks), validates its project
|
|
22
|
+
object and React dependency, and rechecks paths after npm before owned writes.
|
|
23
|
+
These checks do not sandbox npm or stop an external process from racing filesystem
|
|
24
|
+
changes; run installs only in a trusted project and inspect npm failures.
|
package/lib/index.mjs
CHANGED
|
@@ -1,35 +1,76 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, lstat
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
1
|
+
import { readFile, writeFile, mkdir, lstat } from "node:fs/promises";
|
|
2
|
+
import { resolve, join, parse, relative, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
export const VERSION = "0.2.
|
|
6
|
+
export const VERSION = "0.2.4";
|
|
7
7
|
const packages = ["@flood-md/tokens", "@flood-md/react", "@flood-md/agent"];
|
|
8
8
|
const json = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
9
|
-
const exists = async (path) => {
|
|
10
|
-
try {
|
|
11
|
-
await access(path);
|
|
12
|
-
return true;
|
|
13
|
-
} catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
9
|
const agentFile = (name) =>
|
|
18
10
|
fileURLToPath(import.meta.resolve(`@flood-md/agent/${name}`));
|
|
11
|
+
const agentManifest = await json(agentFile("manifest.json"));
|
|
12
|
+
const safeRelative = (path) => typeof path === "string" && /^[a-zA-Z0-9._/-]+$/.test(path) &&
|
|
13
|
+
!path.startsWith("/") && !path.split("/").some(part => !part || part === "." || part === "..");
|
|
14
|
+
if (agentManifest.schemaVersion !== 1 || agentManifest.version !== VERSION || !Array.isArray(agentManifest.files) ||
|
|
15
|
+
!agentManifest.files.length || agentManifest.files.some(file =>
|
|
16
|
+
!safeRelative(file.source) || !safeRelative(file.target) ||
|
|
17
|
+
!(file.target.startsWith("flood/") || file.target.startsWith(".agents/skills/flood-ui/"))) ||
|
|
18
|
+
new Set(agentManifest.files.map(file => file.target)).size !== agentManifest.files.length)
|
|
19
|
+
throw new Error("Invalid Flood agent file manifest.");
|
|
20
|
+
export const AGENT_FILES = Object.freeze(agentManifest.files.map(file => Object.freeze({ ...file })));
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
async function projectDirectory(root) {
|
|
23
|
+
const absolute = resolve(root);
|
|
24
|
+
const anchor = parse(absolute).root;
|
|
25
|
+
let current = anchor;
|
|
26
|
+
// Inspect prefixes before descendants so a linked ancestor cannot redirect
|
|
27
|
+
// either package.json reads or subsequent npm/configuration writes.
|
|
28
|
+
for (const part of ["", ...relative(anchor, absolute).split(sep).filter(Boolean)]) {
|
|
29
|
+
if (part) current = join(current, part);
|
|
30
|
+
const info = await lstat(current).catch(() => {
|
|
31
|
+
throw new Error("Run Flood inside an existing project directory.");
|
|
32
|
+
});
|
|
33
|
+
if (info.isSymbolicLink()) throw new Error(`Refusing linked project directory: ${current}`);
|
|
34
|
+
if (!info.isDirectory()) throw new Error("Run Flood inside a regular project directory.");
|
|
35
|
+
}
|
|
36
|
+
return absolute;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function projectManifest(root) {
|
|
40
|
+
const path = join(root, "package.json");
|
|
41
|
+
const info = await lstat(path).catch(() => {
|
|
42
|
+
throw new Error("Run init inside a React project with a package.json.");
|
|
43
|
+
});
|
|
44
|
+
if (info.isSymbolicLink()) throw new Error("Refusing linked path: package.json");
|
|
45
|
+
if (!info.isFile()) throw new Error("package.json must be a regular file.");
|
|
46
|
+
if (info.nlink > 1) throw new Error("Refusing linked path: package.json");
|
|
47
|
+
const manifest = await json(path).catch(() => {
|
|
48
|
+
throw new Error("package.json must contain a valid project object.");
|
|
49
|
+
});
|
|
50
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
51
|
+
throw new Error("package.json must contain a valid project object.");
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function inspectTarget(root, relative) {
|
|
22
56
|
let current = root;
|
|
57
|
+
let info;
|
|
23
58
|
for (const part of relative.split("/")) {
|
|
24
59
|
current = join(current, part);
|
|
25
60
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
info = await lstat(current);
|
|
62
|
+
if (info.isSymbolicLink()) throw new Error(`Refusing linked path: ${relative}`);
|
|
28
63
|
} catch (error) {
|
|
29
|
-
if (error.code
|
|
64
|
+
if (error.code === "ENOENT") return undefined;
|
|
65
|
+
throw error;
|
|
30
66
|
}
|
|
31
67
|
}
|
|
32
|
-
|
|
68
|
+
return info;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Refuse links and existing instruction files before changing any project data.
|
|
72
|
+
export async function checkTarget(root, relative) {
|
|
73
|
+
if (await inspectTarget(root, relative))
|
|
33
74
|
throw new Error(`${relative} already exists. No files were overwritten.`);
|
|
34
75
|
}
|
|
35
76
|
|
|
@@ -56,10 +97,9 @@ export async function initProject(
|
|
|
56
97
|
root,
|
|
57
98
|
{ dryRun = false, installer = install, log = console.log } = {},
|
|
58
99
|
) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!(manifest.dependencies?.react || manifest.devDependencies?.react))
|
|
100
|
+
root = await projectDirectory(root);
|
|
101
|
+
const manifest = await projectManifest(root);
|
|
102
|
+
if (![manifest.dependencies?.react, manifest.devDependencies?.react].some(value => typeof value === "string" && value.trim()))
|
|
63
103
|
throw new Error(
|
|
64
104
|
"React was not found. Create a React or Next.js project first.",
|
|
65
105
|
);
|
|
@@ -67,17 +107,19 @@ export async function initProject(
|
|
|
67
107
|
manifest.dependencies?.next || manifest.devDependencies?.next
|
|
68
108
|
? "next"
|
|
69
109
|
: "react";
|
|
70
|
-
const files = [
|
|
71
|
-
"flood.json",
|
|
72
|
-
"flood/DESIGN.md",
|
|
73
|
-
".agents/skills/flood-ui/SKILL.md",
|
|
74
|
-
];
|
|
110
|
+
const files = ["flood.json", ...AGENT_FILES.map(file => file.target)];
|
|
75
111
|
for (const file of files) await checkTarget(root, file);
|
|
76
112
|
log(
|
|
77
113
|
`Flood ${VERSION} · ${framework} · npm\nInstall: ${packages.map((p) => `${p}@${VERSION}`).join(", ")}\nCreate: ${files.join(", ")}`,
|
|
78
114
|
);
|
|
79
115
|
if (dryRun) return;
|
|
116
|
+
// Read the complete bundled guide set before npm or owned writes can change anything.
|
|
117
|
+
const guides = await Promise.all(AGENT_FILES.map(async file => ({
|
|
118
|
+
...file, content: await readFile(agentFile(file.source)),
|
|
119
|
+
})));
|
|
80
120
|
await installer(root);
|
|
121
|
+
await projectDirectory(root);
|
|
122
|
+
await projectManifest(root);
|
|
81
123
|
const config = {
|
|
82
124
|
$schema: "https://flood.md/schema/config.json",
|
|
83
125
|
version: VERSION,
|
|
@@ -89,27 +131,25 @@ export async function initProject(
|
|
|
89
131
|
};
|
|
90
132
|
// Recheck after npm has run, before creating owned files.
|
|
91
133
|
for (const file of files) await checkTarget(root, file);
|
|
92
|
-
await mkdir(
|
|
93
|
-
await mkdir(join(root, ".agents/skills/flood-ui"), { recursive: true });
|
|
134
|
+
for (const file of files) await mkdir(resolve(root, file, ".."), { recursive: true });
|
|
94
135
|
await writeFile(
|
|
95
136
|
join(root, files[0]),
|
|
96
137
|
JSON.stringify(config, null, 2) + "\n",
|
|
97
138
|
{ flag: "wx" },
|
|
98
139
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
await writeFile(join(root, files[2]), await readFile(agentFile("SKILL.md")), {
|
|
105
|
-
flag: "wx",
|
|
106
|
-
});
|
|
140
|
+
for (const file of guides) {
|
|
141
|
+
await checkTarget(root, file.target);
|
|
142
|
+
await writeFile(join(root, file.target), file.content, { flag: "wx" });
|
|
143
|
+
}
|
|
107
144
|
log(
|
|
108
145
|
`\nConnected. Import '@flood-md/react/styles.css' in ${framework === "next" ? "app/layout.tsx" : "your app entry"}.\nWrap your interface in <FloodRoot> from '@flood-md/react' to use the dark foundations.\nRead flood/DESIGN.md for component APIs. Run npx @flood-md/cli doctor to verify.`,
|
|
109
146
|
);
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
export async function doctor(root, log = console.log) {
|
|
150
|
+
root = await projectDirectory(root);
|
|
151
|
+
const configInfo = await inspectTarget(root, "flood.json");
|
|
152
|
+
if (!configInfo?.isFile()) throw new Error("No regular flood.json. Run init first.");
|
|
113
153
|
const config = await json(join(root, "flood.json")).catch(() => {
|
|
114
154
|
throw new Error("No flood.json. Run init first.");
|
|
115
155
|
});
|
|
@@ -131,11 +171,21 @@ export async function doctor(root, log = console.log) {
|
|
|
131
171
|
`${name}: expected ${config.version}, found ${found?.version ?? "missing"}`,
|
|
132
172
|
);
|
|
133
173
|
}
|
|
134
|
-
for (const
|
|
135
|
-
|
|
174
|
+
for (const { target } of AGENT_FILES) {
|
|
175
|
+
try {
|
|
176
|
+
const info = await inspectTarget(root, target);
|
|
177
|
+
if (!info?.isFile()) failures.push(`${target}: missing or not a regular file`);
|
|
178
|
+
} catch (error) { failures.push(error.message); }
|
|
179
|
+
}
|
|
180
|
+
if (!failures.length) {
|
|
181
|
+
const installed = await json(join(root, "flood/manifest.json")).catch(() => undefined);
|
|
182
|
+
if (installed?.version !== config.version) failures.push("Agent manifest version differs; compare and merge the matching guide bundle.");
|
|
183
|
+
const header = (await readFile(join(root, "flood/DESIGN.md"), "utf8")).split(/\r?\n/, 1)[0];
|
|
184
|
+
if (!header.endsWith("\u00b7 " + config.version)) failures.push("DESIGN.md version differs; preserve custom edits while updating the contract.");
|
|
185
|
+
}
|
|
136
186
|
if (failures.length) throw new Error(failures.join("\n"));
|
|
137
187
|
log(
|
|
138
|
-
`Flood ${config.version}: package versions and agent files are aligned.\nCheck the stylesheet import in your application entry; doctor does not parse application code.`,
|
|
188
|
+
`Flood ${config.version}: package versions and ${AGENT_FILES.length} agent files are aligned.\nCheck the stylesheet import in your application entry; doctor does not parse application code or validate UI.`,
|
|
139
189
|
);
|
|
140
190
|
}
|
|
141
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Connect Flood to React and Next.js projects.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node": ">=22.12.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@flood-md/agent": "0.2.
|
|
24
|
+
"@flood-md/agent": "0.2.4"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|