@usegraft/registry 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.js +365 -0
- package/package.json +41 -0
- package/registry/callout/components/Callout.tsx +34 -0
- package/registry/callout/components/callout.llms.txt +19 -0
- package/registry/callout/editor/Callout.json +15 -0
- package/registry/callout/registry.item.json +13 -0
- package/registry/comments/graft/comments.llms.txt +14 -0
- package/registry/comments/graft/comments.ts +113 -0
- package/registry/comments/registry.item.json +10 -0
- package/registry/commerce/graft/commerce.llms.txt +35 -0
- package/registry/commerce/graft/commerce.ts +294 -0
- package/registry/commerce/registry.item.json +13 -0
- package/registry/faq/components/Faq.tsx +38 -0
- package/registry/faq/components/faq.llms.txt +22 -0
- package/registry/faq/editor/Faq.json +5 -0
- package/registry/faq/graft/fields/faq.ts +21 -0
- package/registry/faq/registry.item.json +14 -0
- package/registry/scoped-access/graft/scoped-access.llms.txt +9 -0
- package/registry/scoped-access/graft/scoped-access.ts +19 -0
- package/registry/scoped-access/registry.item.json +14 -0
- package/registry/seo/graft/fields/seo.llms.txt +19 -0
- package/registry/seo/graft/fields/seo.ts +24 -0
- package/registry/seo/registry.item.json +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RegistryItemDescriptor } from '@usegraft/contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The registry item manifest — `registry.item.json` inside each bundled item.
|
|
6
|
+
*
|
|
7
|
+
* A registry item is a unit of owned, copy-in code (a shadcn-style primitive):
|
|
8
|
+
* some files, the npm/registry things it needs first, a semver range against
|
|
9
|
+
* @usegraft/core, and an optional llms.txt teaching fragment. Validated by this
|
|
10
|
+
* Zod schema on load, so a malformed item is a REGISTRY_ITEM_INVALID up front —
|
|
11
|
+
* never a half-written project. (See docs/design-notes/registry.md.)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** What kind of primitive this is — decides where its files conventionally land. */
|
|
15
|
+
declare const ITEM_TYPES: readonly ["block", "field", "access", "bundle"];
|
|
16
|
+
type ItemType = (typeof ITEM_TYPES)[number];
|
|
17
|
+
/**
|
|
18
|
+
* A file the item ships. `role` decides how `graft add` treats it:
|
|
19
|
+
* - `module` → a `graft/*.ts` primitive that the barrel aggregates
|
|
20
|
+
* - `component`→ a UI component (e.g. under components/)
|
|
21
|
+
* - `content` → an authored MDX file
|
|
22
|
+
* - `env` → an env-var sample (printed, not merged, for now)
|
|
23
|
+
* - `editor` → an `EditorComponentSpec` JSON saying how the component presents
|
|
24
|
+
* in the Studio canvas. Owned like everything else an item ships:
|
|
25
|
+
* it lands in `graft/editor/` and the Studio reads it from the
|
|
26
|
+
* project, never from the registry it came from.
|
|
27
|
+
*/
|
|
28
|
+
declare const FILE_ROLES: readonly ["module", "component", "content", "env", "editor"];
|
|
29
|
+
type FileRole = (typeof FILE_ROLES)[number];
|
|
30
|
+
declare const registryFileSchema: z.ZodObject<{
|
|
31
|
+
source: z.ZodString;
|
|
32
|
+
target: z.ZodString;
|
|
33
|
+
role: z.ZodDefault<z.ZodEnum<{
|
|
34
|
+
module: "module";
|
|
35
|
+
component: "component";
|
|
36
|
+
content: "content";
|
|
37
|
+
env: "env";
|
|
38
|
+
editor: "editor";
|
|
39
|
+
}>>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
type RegistryFile = z.infer<typeof registryFileSchema>;
|
|
42
|
+
declare const registryItemSchema: z.ZodObject<{
|
|
43
|
+
name: z.ZodString;
|
|
44
|
+
type: z.ZodEnum<{
|
|
45
|
+
block: "block";
|
|
46
|
+
field: "field";
|
|
47
|
+
access: "access";
|
|
48
|
+
bundle: "bundle";
|
|
49
|
+
}>;
|
|
50
|
+
description: z.ZodString;
|
|
51
|
+
graftVersion: z.ZodDefault<z.ZodString>;
|
|
52
|
+
dependencies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
53
|
+
registryDependencies: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
54
|
+
files: z.ZodArray<z.ZodObject<{
|
|
55
|
+
source: z.ZodString;
|
|
56
|
+
target: z.ZodString;
|
|
57
|
+
role: z.ZodDefault<z.ZodEnum<{
|
|
58
|
+
module: "module";
|
|
59
|
+
component: "component";
|
|
60
|
+
content: "content";
|
|
61
|
+
env: "env";
|
|
62
|
+
editor: "editor";
|
|
63
|
+
}>>;
|
|
64
|
+
}, z.core.$strip>>;
|
|
65
|
+
llms: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
type RegistryItemManifest = z.infer<typeof registryItemSchema>;
|
|
68
|
+
/** A loaded item: its validated manifest plus the absolute directory it lives in. */
|
|
69
|
+
interface RegistryItem extends RegistryItemManifest {
|
|
70
|
+
/** Absolute path to the item's directory in the bundled registry. */
|
|
71
|
+
dir: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A deliberately tiny semver check — enough to gate a registry item's
|
|
76
|
+
* `graftVersion` against the installed @usegraft/core, with no dependency.
|
|
77
|
+
*
|
|
78
|
+
* Supported:
|
|
79
|
+
* - "*" / "" / "x" → any version
|
|
80
|
+
* - space-separated AND of comparators: `>=1.2.0 <2.0.0`, `>1.0.0`, `=1.2.3`, `1.2.3` (exact)
|
|
81
|
+
*
|
|
82
|
+
* Caret/tilde/prerelease ranges are out of scope until an item actually needs
|
|
83
|
+
* them (bundled Tier-1 items use "*" pre-1.0). Unparseable input fails closed.
|
|
84
|
+
*/
|
|
85
|
+
type Triple = [number, number, number];
|
|
86
|
+
declare function parseVersion(v: string): Triple | null;
|
|
87
|
+
declare function satisfies(version: string, range: string): boolean;
|
|
88
|
+
|
|
89
|
+
/** A `graft/<basename>.ts` basename → a safe, camelCase JS identifier. */
|
|
90
|
+
declare function moduleIdentifier(basename: string): string;
|
|
91
|
+
/** Barrel source for the given primitive module basenames (deduped + sorted). */
|
|
92
|
+
declare function barrelSource(basenames: readonly string[]): string;
|
|
93
|
+
|
|
94
|
+
/** `Callout.tsx` / `callout.tsx` → export identifier `Callout` (PascalCase). */
|
|
95
|
+
declare function componentExportName(filename: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* MDX map source for the given component basenames (no extension).
|
|
98
|
+
* Empty list → empty map object (valid; pages render markdown-only MDX).
|
|
99
|
+
*/
|
|
100
|
+
declare function mdxComponentsSource(basenames: readonly string[]): string;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The bundled registry root — the sibling `registry/` dir (one level up from src
|
|
104
|
+
* or dist). Built from `import.meta.url` via path.join rather than
|
|
105
|
+
* `new URL("../registry", import.meta.url)` so bundlers (Turbopack in the Next
|
|
106
|
+
* example) don't try to statically resolve the directory as a module asset; it
|
|
107
|
+
* stays plain runtime code. The package is server-external where it runs bundled.
|
|
108
|
+
*/
|
|
109
|
+
declare function registryRoot(): string;
|
|
110
|
+
/** Names of every bundled item (a subdirectory holding a manifest), sorted. */
|
|
111
|
+
declare function listItemNames(root?: string): string[];
|
|
112
|
+
/** Load + validate one item by name. Throws REGISTRY_ITEM_NOT_FOUND / _INVALID. */
|
|
113
|
+
declare function loadItem(name: string, root?: string): RegistryItem;
|
|
114
|
+
interface ResolveOptions {
|
|
115
|
+
/** Registry root override (tests point this at a fixture dir). */
|
|
116
|
+
root?: string;
|
|
117
|
+
/** Installed @usegraft/core version, to gate each item's graftVersion. Skipped if omitted. */
|
|
118
|
+
coreVersion?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve items plus their `registryDependencies` — transitively, deduped,
|
|
122
|
+
* dependency-first (post-order). Cycles are broken (an item already being
|
|
123
|
+
* visited is skipped). A version mismatch throws REGISTRY_ITEM_INVALID.
|
|
124
|
+
*/
|
|
125
|
+
declare function resolveItems(names: readonly string[], options?: ResolveOptions): RegistryItem[];
|
|
126
|
+
/**
|
|
127
|
+
* The agent-facing description of one item (the `describe_item` MCP shape).
|
|
128
|
+
* Drops the machine-specific absolute `dir` and reduces files to what an agent
|
|
129
|
+
* needs: where they land and their role. `llms` becomes a boolean flag.
|
|
130
|
+
*/
|
|
131
|
+
declare function describeItem(item: RegistryItem): RegistryItemDescriptor;
|
|
132
|
+
/**
|
|
133
|
+
* Every bundled item as a descriptor, sorted by name — the `list_registry`
|
|
134
|
+
* source. Loads + validates each manifest, so a malformed bundled item surfaces
|
|
135
|
+
* as REGISTRY_ITEM_INVALID here too.
|
|
136
|
+
*/
|
|
137
|
+
declare function listItems(root?: string): RegistryItemDescriptor[];
|
|
138
|
+
|
|
139
|
+
interface PlannedFile {
|
|
140
|
+
/** Absolute path to write. */
|
|
141
|
+
targetPath: string;
|
|
142
|
+
/** Path relative to the project root — for display. */
|
|
143
|
+
relPath: string;
|
|
144
|
+
content: string;
|
|
145
|
+
role: string;
|
|
146
|
+
/** A file already exists at targetPath. */
|
|
147
|
+
exists: boolean;
|
|
148
|
+
/** The existing file is byte-identical to `content` (a no-op re-add). */
|
|
149
|
+
identical: boolean;
|
|
150
|
+
}
|
|
151
|
+
interface AddPlan {
|
|
152
|
+
items: RegistryItem[];
|
|
153
|
+
files: PlannedFile[];
|
|
154
|
+
/** The regenerated barrel — always written (generated infra, never a conflict). */
|
|
155
|
+
barrel: {
|
|
156
|
+
targetPath: string;
|
|
157
|
+
relPath: string;
|
|
158
|
+
content: string;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* The regenerated MDX component map. Present when the project has (or this
|
|
162
|
+
* plan adds) any component-role files, or when components/ already exists.
|
|
163
|
+
*/
|
|
164
|
+
mdxMap: {
|
|
165
|
+
targetPath: string;
|
|
166
|
+
relPath: string;
|
|
167
|
+
content: string;
|
|
168
|
+
} | null;
|
|
169
|
+
/** llms.txt fragments (one per item that ships one), appended if not already present. */
|
|
170
|
+
llms: {
|
|
171
|
+
targetPath: string;
|
|
172
|
+
fragments: string[];
|
|
173
|
+
};
|
|
174
|
+
/** npm packages to ensure installed (union across items) — printed, not run. */
|
|
175
|
+
npmDependencies: Record<string, string>;
|
|
176
|
+
/** Existing files that DIFFER and would be overwritten (guarded at apply time). */
|
|
177
|
+
conflicts: string[];
|
|
178
|
+
}
|
|
179
|
+
interface PlanAddOptions extends ResolveOptions {
|
|
180
|
+
/** Project root — the directory holding graft.config.ts. */
|
|
181
|
+
targetDir: string;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Component basenames under components/ for the MDX map.
|
|
185
|
+
* Convention: only PascalCase `*.tsx` files (Callout.tsx, Faq.tsx) — skips
|
|
186
|
+
* app helpers like page.tsx / contact-form.tsx and the generated map itself.
|
|
187
|
+
*/
|
|
188
|
+
declare function listMdxComponents(componentsDir: string): string[];
|
|
189
|
+
declare function planAdd(names: readonly string[], options: PlanAddOptions): AddPlan;
|
|
190
|
+
interface ApplyResult {
|
|
191
|
+
/** Files newly written or overwritten. */
|
|
192
|
+
written: string[];
|
|
193
|
+
/** Existing identical files left untouched. */
|
|
194
|
+
skipped: string[];
|
|
195
|
+
barrelPath: string;
|
|
196
|
+
mdxMapPath: string | null;
|
|
197
|
+
llmsAppended: boolean;
|
|
198
|
+
}
|
|
199
|
+
declare function applyPlan(plan: AddPlan, options?: {
|
|
200
|
+
overwrite?: boolean;
|
|
201
|
+
}): ApplyResult;
|
|
202
|
+
|
|
203
|
+
export { type AddPlan, type ApplyResult, FILE_ROLES, type FileRole, ITEM_TYPES, type ItemType, type PlanAddOptions, type PlannedFile, type RegistryFile, type RegistryItem, type RegistryItemManifest, type ResolveOptions, applyPlan, barrelSource, componentExportName, describeItem, listItemNames, listItems, listMdxComponents, loadItem, mdxComponentsSource, moduleIdentifier, parseVersion, planAdd, registryFileSchema, registryItemSchema, registryRoot, resolveItems, satisfies };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// src/manifest.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var ITEM_TYPES = ["block", "field", "access", "bundle"];
|
|
4
|
+
var FILE_ROLES = ["module", "component", "content", "env", "editor"];
|
|
5
|
+
var registryFileSchema = z.object({
|
|
6
|
+
/** Path to the file inside the item directory. */
|
|
7
|
+
source: z.string().min(1),
|
|
8
|
+
/** Path to write, relative to the target project root. */
|
|
9
|
+
target: z.string().min(1),
|
|
10
|
+
role: z.enum(FILE_ROLES).default("module")
|
|
11
|
+
});
|
|
12
|
+
var registryItemSchema = z.object({
|
|
13
|
+
/** Item id — must equal its directory name. */
|
|
14
|
+
name: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase kebab-case"),
|
|
15
|
+
type: z.enum(ITEM_TYPES),
|
|
16
|
+
description: z.string().min(1),
|
|
17
|
+
/** Semver range against the installed @usegraft/core. "*" = any (pre-1.0 default). */
|
|
18
|
+
graftVersion: z.string().default("*"),
|
|
19
|
+
/** npm packages the target needs; printed for the operator to install (not run). */
|
|
20
|
+
dependencies: z.record(z.string(), z.string()).default({}),
|
|
21
|
+
/** Other registry items to add first (transitive, deduped, deps-first). */
|
|
22
|
+
registryDependencies: z.array(z.string()).default([]),
|
|
23
|
+
files: z.array(registryFileSchema).min(1),
|
|
24
|
+
/** Path (inside the item dir) to an llms.txt fragment appended on add. */
|
|
25
|
+
llms: z.string().min(1).optional()
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// src/version.ts
|
|
29
|
+
function parseVersion(v) {
|
|
30
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
33
|
+
}
|
|
34
|
+
function compare(a, b) {
|
|
35
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
36
|
+
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
|
|
37
|
+
if (a[2] !== b[2]) return a[2] < b[2] ? -1 : 1;
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
function satisfies(version, range) {
|
|
41
|
+
const trimmed = range.trim();
|
|
42
|
+
if (trimmed === "" || trimmed === "*" || trimmed === "x") return true;
|
|
43
|
+
const v = parseVersion(version);
|
|
44
|
+
if (!v) return false;
|
|
45
|
+
for (const comparator of trimmed.split(/\s+/)) {
|
|
46
|
+
const m = /^(>=|<=|>|<|=)?(.+)$/.exec(comparator);
|
|
47
|
+
const target = m ? parseVersion(m[2] ?? "") : null;
|
|
48
|
+
if (!m || !target) return false;
|
|
49
|
+
const cmp = compare(v, target);
|
|
50
|
+
const op = m[1] ?? "=";
|
|
51
|
+
const ok = op === ">=" ? cmp >= 0 : op === "<=" ? cmp <= 0 : op === ">" ? cmp > 0 : op === "<" ? cmp < 0 : cmp === 0;
|
|
52
|
+
if (!ok) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/barrel.ts
|
|
58
|
+
var HEADER = [
|
|
59
|
+
"// Generated by `graft add` \u2014 do not edit.",
|
|
60
|
+
"// Aggregates the owned primitives under graft/ into merged collections/functions.",
|
|
61
|
+
"// Safe to delete: `graft add` regenerates it from whatever modules live in graft/."
|
|
62
|
+
];
|
|
63
|
+
function moduleIdentifier(basename2) {
|
|
64
|
+
const camel = basename2.replace(
|
|
65
|
+
/[^a-zA-Z0-9]+(.)?/g,
|
|
66
|
+
(_all, next) => next ? next.toUpperCase() : ""
|
|
67
|
+
);
|
|
68
|
+
return /^[a-zA-Z_$]/.test(camel) ? camel : `_${camel}`;
|
|
69
|
+
}
|
|
70
|
+
function barrelSource(basenames) {
|
|
71
|
+
const entries = [...new Set(basenames)].sort().map((base) => ({ base, id: moduleIdentifier(base) }));
|
|
72
|
+
const imports = entries.map((e) => `import * as ${e.id} from "./${e.base}";`);
|
|
73
|
+
const list = entries.map((e) => e.id).join(", ");
|
|
74
|
+
return [
|
|
75
|
+
...HEADER,
|
|
76
|
+
'import { mergePrimitives } from "@usegraft/core";',
|
|
77
|
+
...imports,
|
|
78
|
+
"",
|
|
79
|
+
`export const { collections, functions } = mergePrimitives([${list}]);`,
|
|
80
|
+
""
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/mdx-map.ts
|
|
85
|
+
var HEADER2 = [
|
|
86
|
+
"// Generated by `graft add` \u2014 do not edit.",
|
|
87
|
+
"// MDX component map for @usegraft/sdk-next MdxBody. Safe to delete: `graft add`",
|
|
88
|
+
"// regenerates it from whatever PascalCase *.tsx files live in components/."
|
|
89
|
+
];
|
|
90
|
+
function componentExportName(filename) {
|
|
91
|
+
const base = filename.replace(/\.tsx?$/, "");
|
|
92
|
+
if (!base) return "Component";
|
|
93
|
+
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
94
|
+
}
|
|
95
|
+
function mdxComponentsSource(basenames) {
|
|
96
|
+
const entries = [...new Set(basenames)].map((base) => {
|
|
97
|
+
const fileBase = base.replace(/\.tsx?$/, "");
|
|
98
|
+
return { fileBase, exportName: componentExportName(fileBase) };
|
|
99
|
+
}).sort((a, b) => a.exportName.localeCompare(b.exportName));
|
|
100
|
+
if (entries.length === 0) {
|
|
101
|
+
return [
|
|
102
|
+
...HEADER2,
|
|
103
|
+
'import type { MdxComponents } from "@usegraft/sdk-next";',
|
|
104
|
+
"",
|
|
105
|
+
"export const mdxComponents: MdxComponents = {};",
|
|
106
|
+
""
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
const imports = entries.map((e) => `import { ${e.exportName} } from "./${e.fileBase}";`);
|
|
110
|
+
const props = entries.map((e) => ` ${e.exportName},`).join("\n");
|
|
111
|
+
return [
|
|
112
|
+
...HEADER2,
|
|
113
|
+
'import type { MdxComponents } from "@usegraft/sdk-next";',
|
|
114
|
+
...imports,
|
|
115
|
+
"",
|
|
116
|
+
"export const mdxComponents: MdxComponents = {",
|
|
117
|
+
props,
|
|
118
|
+
"};",
|
|
119
|
+
""
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/registry.ts
|
|
124
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
125
|
+
import { dirname, join } from "path";
|
|
126
|
+
import { fileURLToPath } from "url";
|
|
127
|
+
import { GraftError } from "@usegraft/contracts";
|
|
128
|
+
var MANIFEST_FILE = "registry.item.json";
|
|
129
|
+
function registryRoot() {
|
|
130
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "registry");
|
|
131
|
+
}
|
|
132
|
+
function listItemNames(root = registryRoot()) {
|
|
133
|
+
if (!existsSync(root)) return [];
|
|
134
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, MANIFEST_FILE))).map((entry) => entry.name).sort();
|
|
135
|
+
}
|
|
136
|
+
function loadItem(name, root = registryRoot()) {
|
|
137
|
+
const dir = join(root, name);
|
|
138
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
139
|
+
if (!existsSync(manifestPath)) {
|
|
140
|
+
const available = listItemNames(root);
|
|
141
|
+
throw new GraftError({
|
|
142
|
+
code: "REGISTRY_ITEM_NOT_FOUND",
|
|
143
|
+
message: `No registry item named "${name}".`,
|
|
144
|
+
fix: `Add one of the available items instead: ${available.join(", ") || "(none bundled)"}.`,
|
|
145
|
+
details: { name, available }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new GraftError({
|
|
153
|
+
code: "REGISTRY_ITEM_INVALID",
|
|
154
|
+
message: `Item "${name}" has an unparseable ${MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
+
fix: "This is a registry bug \u2014 the manifest must be valid JSON. Fix the item or report it.",
|
|
156
|
+
details: { name }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const parsed = registryItemSchema.safeParse(raw);
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
const issues = parsed.error.issues.map((i) => ({
|
|
162
|
+
path: i.path.join("."),
|
|
163
|
+
message: i.message
|
|
164
|
+
}));
|
|
165
|
+
throw new GraftError({
|
|
166
|
+
code: "REGISTRY_ITEM_INVALID",
|
|
167
|
+
message: `Item "${name}" has an invalid ${MANIFEST_FILE}.`,
|
|
168
|
+
fix: "This is a registry bug \u2014 the manifest does not match the item schema (see details.issues). Fix the item or report it.",
|
|
169
|
+
details: { name, issues }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (parsed.data.name !== name) {
|
|
173
|
+
throw new GraftError({
|
|
174
|
+
code: "REGISTRY_ITEM_INVALID",
|
|
175
|
+
message: `Item in "${name}/" declares name "${parsed.data.name}" \u2014 it must match its directory.`,
|
|
176
|
+
fix: "Rename the directory or the manifest `name` so they agree.",
|
|
177
|
+
details: { name, declared: parsed.data.name }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return { ...parsed.data, dir };
|
|
181
|
+
}
|
|
182
|
+
function resolveItems(names, options = {}) {
|
|
183
|
+
const root = options.root ?? registryRoot();
|
|
184
|
+
const resolved = [];
|
|
185
|
+
const done = /* @__PURE__ */ new Set();
|
|
186
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
187
|
+
const visit = (name) => {
|
|
188
|
+
if (done.has(name) || visiting.has(name)) return;
|
|
189
|
+
visiting.add(name);
|
|
190
|
+
const item = loadItem(name, root);
|
|
191
|
+
if (options.coreVersion && !satisfies(options.coreVersion, item.graftVersion)) {
|
|
192
|
+
throw new GraftError({
|
|
193
|
+
code: "REGISTRY_ITEM_INVALID",
|
|
194
|
+
message: `Item "${name}" needs @usegraft/core ${item.graftVersion}, but ${options.coreVersion} is installed.`,
|
|
195
|
+
fix: `Move @usegraft/core into ${item.graftVersion}, or use a version of "${name}" compatible with ${options.coreVersion}.`,
|
|
196
|
+
details: { name, needs: item.graftVersion, installed: options.coreVersion }
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
for (const dep of item.registryDependencies) visit(dep);
|
|
200
|
+
visiting.delete(name);
|
|
201
|
+
done.add(name);
|
|
202
|
+
resolved.push(item);
|
|
203
|
+
};
|
|
204
|
+
for (const name of names) visit(name);
|
|
205
|
+
return resolved;
|
|
206
|
+
}
|
|
207
|
+
function describeItem(item) {
|
|
208
|
+
return {
|
|
209
|
+
name: item.name,
|
|
210
|
+
type: item.type,
|
|
211
|
+
description: item.description,
|
|
212
|
+
graftVersion: item.graftVersion,
|
|
213
|
+
dependencies: item.dependencies,
|
|
214
|
+
registryDependencies: item.registryDependencies,
|
|
215
|
+
files: item.files.map((file) => ({ target: file.target, role: file.role })),
|
|
216
|
+
llms: item.llms !== void 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function listItems(root = registryRoot()) {
|
|
220
|
+
return listItemNames(root).map((name) => describeItem(loadItem(name, root)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/add.ts
|
|
224
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
225
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
226
|
+
import { GraftError as GraftError2 } from "@usegraft/contracts";
|
|
227
|
+
var GRAFT_DIR = "graft";
|
|
228
|
+
var COMPONENTS_DIR = "components";
|
|
229
|
+
var MDX_MAP_FILE = "mdx-components.ts";
|
|
230
|
+
function listGraftModules(graftDir) {
|
|
231
|
+
if (!existsSync2(graftDir)) return [];
|
|
232
|
+
return readdirSync2(graftDir).filter((f) => f.endsWith(".ts") && f !== "index.ts").map((f) => f.slice(0, -".ts".length));
|
|
233
|
+
}
|
|
234
|
+
function listMdxComponents(componentsDir) {
|
|
235
|
+
if (!existsSync2(componentsDir)) return [];
|
|
236
|
+
return readdirSync2(componentsDir).filter((f) => f.endsWith(".tsx") && /^[A-Z]/.test(f)).map((f) => f.replace(/\.tsx$/, ""));
|
|
237
|
+
}
|
|
238
|
+
function planAdd(names, options) {
|
|
239
|
+
const { targetDir } = options;
|
|
240
|
+
const items = resolveItems(names, options);
|
|
241
|
+
const files = [];
|
|
242
|
+
const npmDependencies = {};
|
|
243
|
+
const fragments = [];
|
|
244
|
+
const newModules = [];
|
|
245
|
+
const newComponents = [];
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
Object.assign(npmDependencies, item.dependencies);
|
|
248
|
+
for (const file of item.files) {
|
|
249
|
+
const content = readFileSync2(join2(item.dir, file.source), "utf8");
|
|
250
|
+
const targetPath = join2(targetDir, file.target);
|
|
251
|
+
const exists = existsSync2(targetPath);
|
|
252
|
+
const identical = exists && readFileSync2(targetPath, "utf8") === content;
|
|
253
|
+
files.push({ targetPath, relPath: file.target, content, role: file.role, exists, identical });
|
|
254
|
+
if (file.role === "module") {
|
|
255
|
+
const parts = file.target.replace(/\\/g, "/").split("/");
|
|
256
|
+
if (parts.length === 2 && parts[0] === GRAFT_DIR && parts[1]?.endsWith(".ts")) {
|
|
257
|
+
newModules.push(basename(file.target).replace(/\.ts$/, ""));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (file.role === "component" && file.target.replace(/\\/g, "/").startsWith(`${COMPONENTS_DIR}/`)) {
|
|
261
|
+
const base = basename(file.target).replace(/\.tsx$/, "");
|
|
262
|
+
if (base !== "mdx-components") newComponents.push(base);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (item.llms) fragments.push(readFileSync2(join2(item.dir, item.llms), "utf8").trimEnd());
|
|
266
|
+
}
|
|
267
|
+
const graftDir = join2(targetDir, GRAFT_DIR);
|
|
268
|
+
const modules = [.../* @__PURE__ */ new Set([...listGraftModules(graftDir), ...newModules])];
|
|
269
|
+
const componentsDir = join2(targetDir, COMPONENTS_DIR);
|
|
270
|
+
const componentNames = [.../* @__PURE__ */ new Set([...listMdxComponents(componentsDir), ...newComponents])];
|
|
271
|
+
const touchesComponents = newComponents.length > 0 || existsSync2(componentsDir) || existsSync2(join2(componentsDir, MDX_MAP_FILE));
|
|
272
|
+
return {
|
|
273
|
+
items,
|
|
274
|
+
files,
|
|
275
|
+
barrel: {
|
|
276
|
+
targetPath: join2(graftDir, "index.ts"),
|
|
277
|
+
relPath: join2(GRAFT_DIR, "index.ts"),
|
|
278
|
+
content: barrelSource(modules)
|
|
279
|
+
},
|
|
280
|
+
mdxMap: touchesComponents ? {
|
|
281
|
+
targetPath: join2(componentsDir, MDX_MAP_FILE),
|
|
282
|
+
relPath: join2(COMPONENTS_DIR, MDX_MAP_FILE),
|
|
283
|
+
content: mdxComponentsSource(componentNames)
|
|
284
|
+
} : null,
|
|
285
|
+
llms: { targetPath: join2(targetDir, "llms.txt"), fragments },
|
|
286
|
+
npmDependencies,
|
|
287
|
+
conflicts: files.filter((f) => f.exists && !f.identical).map((f) => f.relPath)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function fragmentHeading(fragment) {
|
|
291
|
+
const lines = fragment.split("\n");
|
|
292
|
+
return lines.find((l) => l.startsWith("## ")) ?? lines[0] ?? fragment.slice(0, 24);
|
|
293
|
+
}
|
|
294
|
+
function applyPlan(plan, options = {}) {
|
|
295
|
+
if (!options.overwrite && plan.conflicts.length > 0) {
|
|
296
|
+
throw new GraftError2({
|
|
297
|
+
code: "REGISTRY_FILE_EXISTS",
|
|
298
|
+
message: `Adding ${plan.items.map((i) => i.name).join(", ")} would overwrite ${plan.conflicts.length} existing file(s) that differ: ${plan.conflicts.join(", ")}.`,
|
|
299
|
+
fix: "Re-run with --overwrite to replace them, or move/rename those files first. `graft add --dry-run <item>` previews every path an item writes.",
|
|
300
|
+
details: { conflicts: plan.conflicts }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const written = [];
|
|
304
|
+
const skipped = [];
|
|
305
|
+
for (const file of plan.files) {
|
|
306
|
+
if (file.identical) {
|
|
307
|
+
skipped.push(file.relPath);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
mkdirSync(dirname2(file.targetPath), { recursive: true });
|
|
311
|
+
writeFileSync(file.targetPath, file.content, "utf8");
|
|
312
|
+
written.push(file.relPath);
|
|
313
|
+
}
|
|
314
|
+
const graftDir = dirname2(plan.barrel.targetPath);
|
|
315
|
+
mkdirSync(graftDir, { recursive: true });
|
|
316
|
+
writeFileSync(plan.barrel.targetPath, barrelSource(listGraftModules(graftDir)), "utf8");
|
|
317
|
+
let mdxMapPath = null;
|
|
318
|
+
if (plan.mdxMap) {
|
|
319
|
+
const componentsDir = dirname2(plan.mdxMap.targetPath);
|
|
320
|
+
mkdirSync(componentsDir, { recursive: true });
|
|
321
|
+
writeFileSync(
|
|
322
|
+
plan.mdxMap.targetPath,
|
|
323
|
+
mdxComponentsSource(listMdxComponents(componentsDir)),
|
|
324
|
+
"utf8"
|
|
325
|
+
);
|
|
326
|
+
mdxMapPath = plan.mdxMap.relPath;
|
|
327
|
+
}
|
|
328
|
+
let llmsAppended = false;
|
|
329
|
+
const existing = existsSync2(plan.llms.targetPath) ? readFileSync2(plan.llms.targetPath, "utf8") : "";
|
|
330
|
+
const fresh = plan.llms.fragments.filter(
|
|
331
|
+
(f) => f.trim().length > 0 && !existing.includes(fragmentHeading(f))
|
|
332
|
+
);
|
|
333
|
+
if (fresh.length > 0) {
|
|
334
|
+
const separator = existing.trim().length > 0 ? "\n\n" : "";
|
|
335
|
+
writeFileSync(
|
|
336
|
+
plan.llms.targetPath,
|
|
337
|
+
`${existing.trimEnd()}${separator}${fresh.join("\n\n")}
|
|
338
|
+
`,
|
|
339
|
+
"utf8"
|
|
340
|
+
);
|
|
341
|
+
llmsAppended = true;
|
|
342
|
+
}
|
|
343
|
+
return { written, skipped, barrelPath: plan.barrel.relPath, mdxMapPath, llmsAppended };
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
FILE_ROLES,
|
|
347
|
+
ITEM_TYPES,
|
|
348
|
+
applyPlan,
|
|
349
|
+
barrelSource,
|
|
350
|
+
componentExportName,
|
|
351
|
+
describeItem,
|
|
352
|
+
listItemNames,
|
|
353
|
+
listItems,
|
|
354
|
+
listMdxComponents,
|
|
355
|
+
loadItem,
|
|
356
|
+
mdxComponentsSource,
|
|
357
|
+
moduleIdentifier,
|
|
358
|
+
parseVersion,
|
|
359
|
+
planAdd,
|
|
360
|
+
registryFileSchema,
|
|
361
|
+
registryItemSchema,
|
|
362
|
+
registryRoot,
|
|
363
|
+
resolveItems,
|
|
364
|
+
satisfies
|
|
365
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
8
|
+
"directory": "packages/registry"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"registry"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"zod": "^4.1.0",
|
|
29
|
+
"@usegraft/contracts": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.16"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
36
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
39
|
+
"lint": "oxlint ."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callout — MDX block (owned code; edit freely).
|
|
3
|
+
*
|
|
4
|
+
* Use in authored MDX bodies:
|
|
5
|
+
*
|
|
6
|
+
* <Callout type="info">Agents edit this file; git is the version history.</Callout>
|
|
7
|
+
*
|
|
8
|
+
* Wired into the generated `components/mdx-components.ts` map by `graft add`.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
export type CalloutType = "info" | "warning" | "tip";
|
|
13
|
+
|
|
14
|
+
export interface CalloutProps {
|
|
15
|
+
type?: CalloutType;
|
|
16
|
+
title?: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LABELS: Record<CalloutType, string> = {
|
|
21
|
+
info: "Note",
|
|
22
|
+
warning: "Warning",
|
|
23
|
+
tip: "Tip",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function Callout({ type = "info", title, children }: CalloutProps) {
|
|
27
|
+
const label = title ?? LABELS[type];
|
|
28
|
+
return (
|
|
29
|
+
<aside className={`callout callout-${type}`} data-callout={type} role="note">
|
|
30
|
+
<strong className="callout-label">{label}</strong>
|
|
31
|
+
<div className="callout-body">{children}</div>
|
|
32
|
+
</aside>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## callout (primitive)
|
|
2
|
+
|
|
3
|
+
MDX block at `components/Callout.tsx`, registered in the generated
|
|
4
|
+
`components/mdx-components.ts` map. Use inside any page body:
|
|
5
|
+
|
|
6
|
+
<Callout type="info">
|
|
7
|
+
Agents edit MDX; git is the version history.
|
|
8
|
+
</Callout>
|
|
9
|
+
|
|
10
|
+
<Callout type="warning" title="Breaking">
|
|
11
|
+
Pre-1.0 APIs may change for the better long-run design.
|
|
12
|
+
</Callout>
|
|
13
|
+
|
|
14
|
+
<Callout type="tip">
|
|
15
|
+
Run `graft compile` after editing content.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
`type` is `info` | `warning` | `tip` (default `info`). Optional `title`
|
|
19
|
+
overrides the default label. Style via `.callout`, `.callout-info`, etc.
|