@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.
Files changed (3) hide show
  1. package/README.md +15 -2
  2. package/lib/index.mjs +89 -39
  3. 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, flood/DESIGN.md and .agents/skills/flood-ui/SKILL.md. Refuses existing
8
- owned files or linked destinations. Does not rewrite source code or AGENTS.md.
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, access } from "node:fs/promises";
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.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
- // Refuse links and existing instruction files before changing any project data.
21
- export async function checkTarget(root, relative) {
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
- if ((await lstat(current)).isSymbolicLink())
27
- throw new Error(`Refusing linked path: ${relative}`);
61
+ info = await lstat(current);
62
+ if (info.isSymbolicLink()) throw new Error(`Refusing linked path: ${relative}`);
28
63
  } catch (error) {
29
- if (error.code !== "ENOENT") throw error;
64
+ if (error.code === "ENOENT") return undefined;
65
+ throw error;
30
66
  }
31
67
  }
32
- if (await exists(join(root, relative)))
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
- const manifest = await json(join(root, "package.json")).catch(() => {
60
- throw new Error("Run init inside a React project with a package.json.");
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(join(root, "flood"), { recursive: true });
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
- await writeFile(
100
- join(root, files[1]),
101
- await readFile(agentFile("DESIGN.md")),
102
- { flag: "wx" },
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 name of ["flood/DESIGN.md", ".agents/skills/flood-ui/SKILL.md"])
135
- if (!(await exists(join(root, name)))) failures.push(`${name}: missing`);
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.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.2"
24
+ "@flood-md/agent": "0.2.4"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"