@flood-md/cli 0.2.8 → 0.2.10
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 +29 -0
- package/bin/flood.mjs +2 -1
- package/lib/index.mjs +108 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
`npx @flood-md/cli init [--dry-run]` connects an existing React/Next.js npm project.
|
|
4
4
|
`npx @flood-md/cli doctor` checks package versions and agent file presence.
|
|
5
5
|
|
|
6
|
+
Version 0.2.10 adds read-only JSON discovery. Use the CLI version matching
|
|
7
|
+
flood.json; source availability does not mean the candidate is published.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx @flood-md/cli@0.2.10 info
|
|
11
|
+
npx @flood-md/cli@0.2.10 catalog --kind workflow --group composition
|
|
12
|
+
npx @flood-md/cli@0.2.10 read component:data-table
|
|
13
|
+
npx @flood-md/cli@0.2.10 read recipe:table-bulk-actions
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
info distinguishes connected, bundle-only, version-mismatch and missing-packages
|
|
17
|
+
states. catalog/read refuse mismatched or incomplete connected installations.
|
|
18
|
+
All three commands return JSON; failures return structured JSON on stderr and a
|
|
19
|
+
nonzero exit code. They do not install, modify host files or fetch remote guides.
|
|
20
|
+
The npx runner itself may fetch the explicitly selected package.
|
|
21
|
+
|
|
22
|
+
catalog filters by kind, group and literal metadata search. Default pages contain
|
|
23
|
+
at most 12 entries; --limit accepts 1–50, within a 16 KiB output budget. Follow
|
|
24
|
+
nextCursor; a changed query or catalog invalidates an old cursor. read returns
|
|
25
|
+
one known ID, at most 10,000 Unicode code points by default (maximum 40,000).
|
|
26
|
+
Follow nextOffset using --offset to finish instructions; --links-offset pages
|
|
27
|
+
optional links separately. Required prerequisites are explicit in requires.
|
|
28
|
+
|
|
29
|
+
Copied guides default to project-owned content, identified by source, customized
|
|
30
|
+
and hashes. --source package compares canonical content without overwriting.
|
|
31
|
+
Missing/linked project copies fail explicitly. A modified flood/catalog.json
|
|
32
|
+
cannot redirect reads. See [Agent knowledge](../../docs/agent-knowledge.md)
|
|
33
|
+
for the complete protocol and limits.
|
|
34
|
+
|
|
6
35
|
Requires Node 22.12+. Installs matching tokens, React and agent packages; creates
|
|
7
36
|
flood.json and every owned guide listed in the matching agent manifest: DESIGN.md,
|
|
8
37
|
rules.json and manifest.json under flood/, plus SKILL.md and focused references
|
package/bin/flood.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { run } from "../lib/index.mjs";
|
|
|
3
3
|
try {
|
|
4
4
|
await run(process.argv.slice(2));
|
|
5
5
|
} catch (error) {
|
|
6
|
-
console.error(
|
|
6
|
+
if (['info','catalog','read'].includes(process.argv[2])) console.error(JSON.stringify({ok:false,error:{code:error.code ?? 'KNOWLEDGE_ERROR',message:error.message}}));
|
|
7
|
+
else console.error(`Flood: ${error.message}`);
|
|
7
8
|
process.exitCode = 1;
|
|
8
9
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -2,8 +2,10 @@ import { readFile, writeFile, mkdir, lstat } from "node:fs/promises";
|
|
|
2
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
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { createKnowledgeReader } from '@flood-md/agent';
|
|
5
7
|
|
|
6
|
-
export const VERSION = "0.2.
|
|
8
|
+
export const VERSION = "0.2.10";
|
|
7
9
|
const packages = ["@flood-md/tokens", "@flood-md/react", "@flood-md/agent"];
|
|
8
10
|
const json = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
9
11
|
const agentFile = (name) =>
|
|
@@ -189,8 +191,111 @@ export async function doctor(root, log = console.log) {
|
|
|
189
191
|
);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
|
|
194
|
+
async function installedVersion(root, name) {
|
|
195
|
+
let current = root;
|
|
196
|
+
while (true) {
|
|
197
|
+
try {
|
|
198
|
+
const path = join(current, 'node_modules', name, 'package.json');
|
|
199
|
+
const info = await lstat(path);
|
|
200
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 64 * 1024) throw Error('Invalid package manifest.');
|
|
201
|
+
const pkg = await json(path);
|
|
202
|
+
if (pkg.name !== name || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(pkg.version ?? '')) throw Error('Invalid package identity.');
|
|
203
|
+
return pkg.version;
|
|
204
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
205
|
+
const parent = resolve(current, '..'); if (parent === current) return null; current = parent;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function knowledgeInfo(root) {
|
|
210
|
+
root = await projectDirectory(root);
|
|
211
|
+
const app = await projectManifest(root);
|
|
212
|
+
const configInfo = await inspectTarget(root, 'flood.json');
|
|
213
|
+
let config;
|
|
214
|
+
if (configInfo) {
|
|
215
|
+
if (!configInfo.isFile() || configInfo.nlink !== 1 || configInfo.size > 64 * 1024) throw Error('Invalid flood.json.');
|
|
216
|
+
config = await json(join(root, 'flood.json')).catch(() => { throw Error('Malformed flood.json.'); });
|
|
217
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) || !/^\d+\.\d+\.\d+$/.test(config.version ?? '')) throw Error('Invalid flood.json version.');
|
|
218
|
+
}
|
|
219
|
+
const versions = Object.fromEntries(await Promise.all([...packages, '@flood-md/cli'].map(async name => [name, await installedVersion(root, name)])));
|
|
220
|
+
const knowledge = await createKnowledgeReader().info();
|
|
221
|
+
const present = Object.values(versions).filter(Boolean);
|
|
222
|
+
let guideVersion = null;
|
|
223
|
+
if (config) {
|
|
224
|
+
const stat = await inspectTarget(root, 'flood/manifest.json');
|
|
225
|
+
if (!stat?.isFile() || stat.nlink !== 1 || stat.size > 64 * 1024) throw Error('Missing or invalid project guide manifest.');
|
|
226
|
+
const installed = await json(join(root, 'flood/manifest.json')).catch(() => { throw Error('Malformed project guide manifest.'); });
|
|
227
|
+
if (installed?.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(installed.version ?? '')) throw Error('Invalid project guide manifest version.');
|
|
228
|
+
guideVersion = installed.version;
|
|
229
|
+
}
|
|
230
|
+
const mismatch = knowledge.version !== VERSION || (config && (config.version !== VERSION || guideVersion !== VERSION)) || present.some(version => version !== VERSION);
|
|
231
|
+
const missing = config && packages.some(name => !versions[name]);
|
|
232
|
+
const state = mismatch ? 'version-mismatch' : missing ? 'missing-packages' : config ? 'connected' : 'bundle-only';
|
|
233
|
+
const dependencies = {...app.devDependencies, ...app.dependencies};
|
|
234
|
+
return {schemaVersion:1, version:VERSION, project:{state, framework:dependencies.next ? 'next' : dependencies.react ? 'react' : 'unknown', configuredVersion:config?.version ?? null, guideVersion, installedVersions:versions}, knowledge};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function knowledgeContext(root) {
|
|
238
|
+
const info = await knowledgeInfo(root);
|
|
239
|
+
if (['version-mismatch','missing-packages'].includes(info.project.state)) {
|
|
240
|
+
const error = new Error('Project packages, Flood configuration and this CLI must match before reading knowledge. Run info or the matching installed CLI.');
|
|
241
|
+
error.code = 'VERSION_MISMATCH'; throw error;
|
|
242
|
+
}
|
|
243
|
+
return info;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function knowledgeCatalog(root, query = {}) {
|
|
247
|
+
const info = await knowledgeContext(root);
|
|
248
|
+
return {...await createKnowledgeReader().list(query), scope:info.project.state};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function knowledgeRead(root, id, {source = 'auto', offset = 0, length = 10000, linksOffset = 0} = {}) {
|
|
252
|
+
if (!['auto','package'].includes(source)) throw Error('Read source must be auto or package.');
|
|
253
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 1 || length > 40000) throw Error('Invalid read range.');
|
|
254
|
+
const info = await knowledgeContext(root);
|
|
255
|
+
const reader = createKnowledgeReader();
|
|
256
|
+
const upstream = await reader.read(id, {length:1, linksOffset});
|
|
257
|
+
if (source === 'package' || info.project.state !== 'connected' || !upstream.installedTarget) return {...await reader.read(id, {offset,length,linksOffset}), customized:false, scope:info.project.state};
|
|
258
|
+
const stat = await inspectTarget(root, upstream.installedTarget);
|
|
259
|
+
if (!stat?.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024) throw Error('Selected project guide is missing, linked or exceeds the read budget.');
|
|
260
|
+
const bytes = await readFile(join(root, upstream.installedTarget));
|
|
261
|
+
if (bytes.length > 1024 * 1024) throw Error('Project guide exceeds the read budget.');
|
|
262
|
+
const body = new TextDecoder('utf-8', {fatal:true}).decode(bytes);
|
|
263
|
+
if (id === 'guide:design' && !body.split(/\r?\n/,1)[0].endsWith('· ' + VERSION)) throw Error('Project design guide version differs. Compare and merge the matching guide.');
|
|
264
|
+
const chars = Array.from(body);
|
|
265
|
+
if (offset > chars.length) throw Error('Read offset is outside the project material.');
|
|
266
|
+
const end = Math.min(chars.length, offset + length), sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
267
|
+
return {...upstream, source:'project', scope:info.project.state, customized:sha256 !== upstream.sha256,
|
|
268
|
+
packageSha256:upstream.sha256, sha256, text:chars.slice(offset,end).join(''), offset,
|
|
269
|
+
totalCharacters:chars.length, truncated:end < chars.length, nextOffset:end < chars.length ? end : null};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function knowledgeArguments(command, args) {
|
|
273
|
+
const options = {}, positional = [];
|
|
274
|
+
const flags = command === 'catalog' ? new Set(['kind','group','search','limit','cursor']) : command === 'read' ? new Set(['offset','length','source','links-offset']) : new Set();
|
|
275
|
+
for (let i=0; i<args.length; i++) {
|
|
276
|
+
const value = args[i];
|
|
277
|
+
if (value === '--json') { if (options.json) throw Error('Duplicate --json.'); options.json = true; continue; }
|
|
278
|
+
if (!value.startsWith('--')) { positional.push(value); continue; }
|
|
279
|
+
const key = value.slice(2);
|
|
280
|
+
const optionKey = key === 'links-offset' ? 'linksOffset' : key;
|
|
281
|
+
if (!flags.has(key) || Object.hasOwn(options,optionKey) || args[i+1] === undefined || args[i+1].startsWith('--')) throw Error('Unknown, repeated or incomplete knowledge option.');
|
|
282
|
+
const raw = args[++i];
|
|
283
|
+
if (['offset','length','limit','links-offset'].includes(key)) {
|
|
284
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) throw Error('Knowledge ranges must be integers.');
|
|
285
|
+
options[optionKey] = Number(raw);
|
|
286
|
+
} else options[key] = raw;
|
|
287
|
+
}
|
|
288
|
+
if (positional.length !== (command === 'read' ? 1 : 0)) throw Error('Read requires one material ID; info and catalog accept only options.');
|
|
289
|
+
delete options.json; return {id:positional[0], options};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function run(args, {root = process.cwd(), log = console.log} = {}) {
|
|
193
293
|
const [command, ...flags] = args;
|
|
294
|
+
if (['info','catalog','read'].includes(command)) {
|
|
295
|
+
const {id, options} = knowledgeArguments(command, flags);
|
|
296
|
+
const result = command === 'info' ? await knowledgeInfo(root) : command === 'catalog' ? await knowledgeCatalog(root, options) : await knowledgeRead(root,id,options);
|
|
297
|
+
log(JSON.stringify(result)); return result;
|
|
298
|
+
}
|
|
194
299
|
if (
|
|
195
300
|
flags.some((flag) => flag !== "--dry-run") ||
|
|
196
301
|
(flags.length && command !== "init")
|
|
@@ -203,6 +308,6 @@ export async function run(args) {
|
|
|
203
308
|
if (command && command !== "--help")
|
|
204
309
|
throw new Error(`Unknown command: ${command}. Use --help.`);
|
|
205
310
|
console.log(
|
|
206
|
-
`Flood ${VERSION}\n\nnpx @flood-md/cli init [--dry-run] Connect a React or Next.js project\nnpx @flood-md/cli doctor Check packages and agent files\n\nComponents ship together in @flood-md/react; import the ones you need.\nUpdates: review the changelog and upgrade Flood packages together.\nhttps://flood.md`,
|
|
311
|
+
`Flood ${VERSION}\n\nnpx @flood-md/cli init [--dry-run] Connect a React or Next.js project\nnpx @flood-md/cli doctor Check packages and agent files\nnpx @flood-md/cli info Read project and knowledge versions as JSON\nnpx @flood-md/cli catalog Discover bounded knowledge metadata as JSON\nnpx @flood-md/cli read <id> Read one installed-version material as JSON\n\nComponents ship together in @flood-md/react; import the ones you need.\nUpdates: review the changelog and upgrade Flood packages together.\nhttps://flood.md`,
|
|
207
312
|
);
|
|
208
313
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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.10"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|