contentbase 0.0.1

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 (89) hide show
  1. package/README.md +460 -0
  2. package/bun.lock +473 -0
  3. package/examples/sdlc-queries.ts +161 -0
  4. package/package.json +41 -0
  5. package/showcases/national-parks/models.ts +74 -0
  6. package/showcases/national-parks/parks/acadia.mdx +40 -0
  7. package/showcases/national-parks/parks/yosemite.mdx +44 -0
  8. package/showcases/national-parks/parks/zion.mdx +44 -0
  9. package/showcases/national-parks/queries.ts +103 -0
  10. package/showcases/national-parks/trails/angels-landing.mdx +19 -0
  11. package/showcases/national-parks/trails/cathedral-lakes.mdx +19 -0
  12. package/showcases/national-parks/trails/half-dome.mdx +19 -0
  13. package/showcases/national-parks/trails/jordan-pond-path.mdx +19 -0
  14. package/showcases/national-parks/trails/mist-trail.mdx +19 -0
  15. package/showcases/national-parks/trails/observation-point.mdx +19 -0
  16. package/showcases/national-parks/trails/precipice-trail.mdx +19 -0
  17. package/showcases/national-parks/trails/the-narrows.mdx +19 -0
  18. package/showcases/recipes/cuisines/chinese.mdx +28 -0
  19. package/showcases/recipes/cuisines/italian.mdx +32 -0
  20. package/showcases/recipes/cuisines/mexican.mdx +28 -0
  21. package/showcases/recipes/models.ts +77 -0
  22. package/showcases/recipes/queries.ts +89 -0
  23. package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +43 -0
  24. package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +47 -0
  25. package/showcases/recipes/recipes/italian/bruschetta.mdx +38 -0
  26. package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +39 -0
  27. package/showcases/recipes/recipes/italian/tiramisu.mdx +43 -0
  28. package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +44 -0
  29. package/showcases/recipes/recipes/mexican/guacamole.mdx +39 -0
  30. package/showcases/vinyl-collection/albums/bitches-brew.mdx +36 -0
  31. package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +35 -0
  32. package/showcases/vinyl-collection/albums/in-rainbows.mdx +35 -0
  33. package/showcases/vinyl-collection/albums/kind-of-blue.mdx +32 -0
  34. package/showcases/vinyl-collection/albums/ok-computer.mdx +37 -0
  35. package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +35 -0
  36. package/showcases/vinyl-collection/artists/miles-davis.mdx +27 -0
  37. package/showcases/vinyl-collection/artists/nina-simone.mdx +26 -0
  38. package/showcases/vinyl-collection/artists/radiohead.mdx +27 -0
  39. package/showcases/vinyl-collection/models.ts +73 -0
  40. package/showcases/vinyl-collection/queries.ts +87 -0
  41. package/src/ast-query.ts +132 -0
  42. package/src/cli/commands/action.ts +44 -0
  43. package/src/cli/commands/create.ts +59 -0
  44. package/src/cli/commands/export.ts +24 -0
  45. package/src/cli/commands/init.ts +75 -0
  46. package/src/cli/commands/inspect.ts +46 -0
  47. package/src/cli/commands/validate.ts +75 -0
  48. package/src/cli/index.ts +20 -0
  49. package/src/cli/load-collection.ts +53 -0
  50. package/src/collection.ts +399 -0
  51. package/src/define-model.ts +80 -0
  52. package/src/document.ts +468 -0
  53. package/src/index.ts +47 -0
  54. package/src/model-instance.ts +227 -0
  55. package/src/node-shortcuts.ts +87 -0
  56. package/src/parse.ts +123 -0
  57. package/src/query/collection-query.ts +149 -0
  58. package/src/query/index.ts +5 -0
  59. package/src/query/operators.ts +37 -0
  60. package/src/query/query-builder.ts +109 -0
  61. package/src/relationships/belongs-to.ts +50 -0
  62. package/src/relationships/has-many.ts +136 -0
  63. package/src/relationships/index.ts +57 -0
  64. package/src/relationships/types.ts +7 -0
  65. package/src/section.ts +29 -0
  66. package/src/types.ts +221 -0
  67. package/src/utils/index.ts +11 -0
  68. package/src/utils/inflect.ts +82 -0
  69. package/src/utils/normalize-headings.ts +31 -0
  70. package/src/utils/parse-table.ts +30 -0
  71. package/src/utils/read-directory.ts +35 -0
  72. package/src/utils/stringify-ast.ts +9 -0
  73. package/src/validator.ts +52 -0
  74. package/test/ast-query.test.ts +128 -0
  75. package/test/collection.test.ts +99 -0
  76. package/test/define-model.test.ts +78 -0
  77. package/test/document.test.ts +225 -0
  78. package/test/fixtures/sdlc/epics/authentication.mdx +42 -0
  79. package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +21 -0
  80. package/test/fixtures/sdlc/models.ts +89 -0
  81. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +20 -0
  82. package/test/helpers.ts +21 -0
  83. package/test/model-instance.test.ts +197 -0
  84. package/test/query.test.ts +167 -0
  85. package/test/relationships.test.ts +84 -0
  86. package/test/section.test.ts +99 -0
  87. package/test/validator.test.ts +62 -0
  88. package/tsconfig.json +18 -0
  89. package/vitest.config.ts +11 -0
@@ -0,0 +1,32 @@
1
+ ---
2
+ artist: miles-davis
3
+ year: 1959
4
+ genre: Modal Jazz
5
+ format: LP
6
+ rating: 5
7
+ condition: very-good
8
+ ---
9
+
10
+ # Kind of Blue
11
+
12
+ Recorded over two sessions in spring 1959, this album defined modal jazz. Miles handed his sextet sketches and scales instead of chord changes, and they improvised nearly everything on the first take.
13
+
14
+ ## Tracklist
15
+
16
+ | # | Title | Duration |
17
+ | --- | --- | --- |
18
+ | 1 | So What | 9:22 |
19
+ | 2 | Freddie Freeloader | 9:46 |
20
+ | 3 | Blue in Green | 5:27 |
21
+ | 4 | All Blues | 11:33 |
22
+ | 5 | Flamenco Sketches | 9:26 |
23
+
24
+ ## Personnel
25
+
26
+ - Miles Davis — trumpet
27
+ - John Coltrane — tenor saxophone
28
+ - Cannonball Adderley — alto saxophone
29
+ - Bill Evans — piano (tracks 1, 3, 4, 5)
30
+ - Wynton Kelly — piano (track 2)
31
+ - Paul Chambers — bass
32
+ - Jimmy Cobb — drums
@@ -0,0 +1,37 @@
1
+ ---
2
+ artist: radiohead
3
+ year: 1997
4
+ genre: Alternative Rock
5
+ format: 2xLP
6
+ rating: 5
7
+ condition: near-mint
8
+ ---
9
+
10
+ # OK Computer
11
+
12
+ Radiohead's third album traded the angst of their earlier work for something bigger: a panoramic meditation on disconnection in the digital age. Dense, layered, and somehow more relevant with every passing year.
13
+
14
+ ## Tracklist
15
+
16
+ | # | Title | Duration |
17
+ | --- | --- | --- |
18
+ | 1 | Airbag | 4:44 |
19
+ | 2 | Paranoid Android | 6:23 |
20
+ | 3 | Subterranean Homesick Alien | 4:27 |
21
+ | 4 | Exit Music (For a Film) | 4:24 |
22
+ | 5 | Let Down | 4:59 |
23
+ | 6 | Karma Police | 4:21 |
24
+ | 7 | Fitter Happier | 1:57 |
25
+ | 8 | Electioneering | 3:50 |
26
+ | 9 | Climbing Up the Walls | 4:45 |
27
+ | 10 | No Surprises | 3:48 |
28
+ | 11 | Lucky | 4:19 |
29
+ | 12 | The Tourist | 5:24 |
30
+
31
+ ## Personnel
32
+
33
+ - Thom Yorke — vocals, guitar, keyboards
34
+ - Jonny Greenwood — guitar, keyboards, ondes Martenot
35
+ - Ed O'Brien — guitar, vocals
36
+ - Colin Greenwood — bass
37
+ - Phil Selway — drums
@@ -0,0 +1,35 @@
1
+ ---
2
+ artist: nina-simone
3
+ year: 1966
4
+ genre: Jazz / Blues
5
+ format: LP
6
+ rating: 4
7
+ condition: very-good
8
+ ---
9
+
10
+ # Wild Is the Wind
11
+
12
+ Stripped back compared to its predecessor, this album foregrounds Nina's piano and voice. The material ranges from tender ballads to biting social commentary, often within the same song.
13
+
14
+ ## Tracklist
15
+
16
+ | # | Title | Duration |
17
+ | --- | --- | --- |
18
+ | 1 | I Love Your Lovin' Ways | 2:34 |
19
+ | 2 | Four Women | 4:09 |
20
+ | 3 | What More Can I Say | 3:11 |
21
+ | 4 | Lilac Wine | 4:17 |
22
+ | 5 | That's All I Ask | 3:01 |
23
+ | 6 | Break Down and Let It All Out | 2:39 |
24
+ | 7 | Why Keep On Breaking My Heart | 2:41 |
25
+ | 8 | Wild Is the Wind | 6:49 |
26
+ | 9 | Black Is the Color of My True Love's Hair | 3:07 |
27
+ | 10 | If I Should Lose You | 2:22 |
28
+ | 11 | Either Way I Lose | 4:38 |
29
+
30
+ ## Personnel
31
+
32
+ - Nina Simone — vocals, piano
33
+ - Rudy Stevenson — guitar, flute
34
+ - Lisle Atkinson — bass
35
+ - Bobby Hamilton — drums
@@ -0,0 +1,27 @@
1
+ ---
2
+ genre: Jazz
3
+ origin: Alton, Illinois
4
+ active: "1944–1991"
5
+ ---
6
+
7
+ # Miles Davis
8
+
9
+ Trumpeter, bandleader, and relentless innovator. Miles didn't just play jazz — he bent it into new shapes every decade, from bebop to cool jazz to fusion and beyond.
10
+
11
+ ## Influences
12
+
13
+ - Charlie Parker
14
+ - Dizzy Gillespie
15
+ - Duke Ellington
16
+ - Ahmad Jamal
17
+ - Jimi Hendrix
18
+
19
+ ## Discography
20
+
21
+ ### Kind of Blue
22
+
23
+ The best-selling jazz album of all time. Modal jazz distilled to its essence.
24
+
25
+ ### Bitches Brew
26
+
27
+ The album that invented jazz fusion. Chaotic, electric, and revolutionary.
@@ -0,0 +1,26 @@
1
+ ---
2
+ genre: Jazz / Soul
3
+ origin: Tryon, North Carolina
4
+ active: "1954–2003"
5
+ ---
6
+
7
+ # Nina Simone
8
+
9
+ Classically trained pianist who became one of the most powerful voices in American music. Her work fused jazz, blues, folk, and classical into something entirely her own — always political, always personal.
10
+
11
+ ## Influences
12
+
13
+ - Johann Sebastian Bach
14
+ - Billie Holiday
15
+ - Langston Hughes
16
+ - Lorraine Hansberry
17
+
18
+ ## Discography
19
+
20
+ ### I Put a Spell on You
21
+
22
+ Lush orchestral arrangements meet Nina's incomparable voice. Sophisticated, powerful, and hauntingly intimate.
23
+
24
+ ### Wild Is the Wind
25
+
26
+ A stripped-down collection that showcases her piano and voice at their most raw and emotive.
@@ -0,0 +1,27 @@
1
+ ---
2
+ genre: Alternative Rock
3
+ origin: Abingdon, Oxfordshire
4
+ active: "1985–present"
5
+ ---
6
+
7
+ # Radiohead
8
+
9
+ Five musicians from Oxfordshire who spent three decades proving that popular music could be challenging, experimental, and emotionally devastating — all at the same time.
10
+
11
+ ## Influences
12
+
13
+ - Talking Heads
14
+ - R.E.M.
15
+ - Can
16
+ - Aphex Twin
17
+ - Olivier Messiaen
18
+
19
+ ## Discography
20
+
21
+ ### OK Computer
22
+
23
+ Anxiety about technology and modern alienation, wrapped in some of the most ambitious guitar rock ever committed to tape.
24
+
25
+ ### In Rainbows
26
+
27
+ A warm, intimate counterpoint to the paranoia of their earlier work. Released via pay-what-you-want download.
@@ -0,0 +1,73 @@
1
+ import {
2
+ defineModel,
3
+ section,
4
+ hasMany,
5
+ belongsTo,
6
+ z,
7
+ } from "../../src/index";
8
+ import { toString } from "mdast-util-to-string";
9
+ import { parseTable } from "../../src/utils/parse-table";
10
+
11
+ // ─── Artist (parent) ───
12
+
13
+ export const Artist = defineModel("Artist", {
14
+ prefix: "artists",
15
+ meta: z.object({
16
+ genre: z.string(),
17
+ origin: z.string(),
18
+ active: z.string(),
19
+ }),
20
+ sections: {
21
+ influences: section("Influences", {
22
+ extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
23
+ schema: z.array(z.string()),
24
+ }),
25
+ },
26
+ relationships: {
27
+ albums: hasMany(() => Album, { heading: "Discography" }),
28
+ },
29
+ });
30
+
31
+ // ─── Album ───
32
+
33
+ export const Album = defineModel("Album", {
34
+ prefix: "albums",
35
+ meta: z.object({
36
+ artist: z.string(),
37
+ year: z.number(),
38
+ genre: z.string(),
39
+ format: z.enum(["LP", "EP", "Single", "2xLP"]).default("LP"),
40
+ rating: z.number().min(1).max(5).optional(),
41
+ condition: z
42
+ .enum(["mint", "near-mint", "very-good", "good", "fair", "poor"])
43
+ .default("very-good"),
44
+ }),
45
+ sections: {
46
+ tracklist: section("Tracklist", {
47
+ extract: (q) => {
48
+ const tables = q.selectAll("table");
49
+ if (tables.length > 0) {
50
+ return parseTable(tables[0]);
51
+ }
52
+ return q.selectAll("listItem").map((n) => toString(n));
53
+ },
54
+ schema: z.union([
55
+ z.array(z.record(z.string(), z.string())),
56
+ z.array(z.string()),
57
+ ]),
58
+ }),
59
+ personnel: section("Personnel", {
60
+ extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
61
+ schema: z.array(z.string()),
62
+ }),
63
+ },
64
+ relationships: {
65
+ artist: belongsTo(() => Artist, {
66
+ foreignKey: (doc) => doc.meta.artist as string,
67
+ }),
68
+ },
69
+ computed: {
70
+ isClassic: (self: any) => self.meta.year < 1980,
71
+ decade: (self: any) => `${Math.floor(self.meta.year / 10) * 10}s`,
72
+ },
73
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Example queries for the Vinyl Collection showcase.
3
+ *
4
+ * Run with: bun showcases/vinyl-collection/queries.ts
5
+ */
6
+ import { Collection } from "../../src/index";
7
+ import { Artist, Album } from "./models";
8
+
9
+ const collection = new Collection({
10
+ rootPath: new URL(".", import.meta.url).pathname,
11
+ });
12
+
13
+ collection.register(Artist);
14
+ collection.register(Album);
15
+ await collection.load();
16
+
17
+ // ── Full collection ──
18
+ const allAlbums = await collection.query(Album).fetchAll();
19
+ console.log(`Total albums in collection: ${allAlbums.length}`);
20
+
21
+ // ── Filter by genre ──
22
+ const jazzAlbums = await collection
23
+ .query(Album)
24
+ .where("meta.genre", "contains", "Jazz")
25
+ .fetchAll();
26
+ console.log(
27
+ `\nJazz albums: ${jazzAlbums.map((a) => `${a.title} (${a.meta.year})`).join(", ")}`
28
+ );
29
+
30
+ // ── Filter by decade (computed) ──
31
+ console.log(`\nAlbums by decade:`);
32
+ for (const album of allAlbums) {
33
+ console.log(` ${album.title} — ${album.computed.decade}`);
34
+ }
35
+
36
+ // ── 5-star records ──
37
+ const topRated = await collection
38
+ .query(Album)
39
+ .where("meta.rating", 5)
40
+ .fetchAll();
41
+ console.log(
42
+ `\n5-star albums: ${topRated.map((a) => a.title).join(", ")}`
43
+ );
44
+
45
+ // ── Classics (pre-1980) ──
46
+ console.log(`\nClassics (pre-1980):`);
47
+ for (const album of allAlbums) {
48
+ if (album.computed.isClassic) {
49
+ console.log(` ${album.title} (${album.meta.year})`);
50
+ }
51
+ }
52
+
53
+ // ── Condition report ──
54
+ const mint = await collection
55
+ .query(Album)
56
+ .whereIn("meta.condition", ["mint", "near-mint"])
57
+ .fetchAll();
58
+ console.log(
59
+ `\nMint / near-mint condition: ${mint.map((a) => a.title).join(", ")}`
60
+ );
61
+
62
+ // ── Tracklist as structured data ──
63
+ const kob = collection.getModel("albums/kind-of-blue", Album);
64
+ console.log(`\n--- ${kob.title} ---`);
65
+ console.log("Tracklist:", kob.sections.tracklist);
66
+ console.log("Personnel:", kob.sections.personnel);
67
+
68
+ // ── Artist → Albums relationship ──
69
+ const miles = collection.getModel("artists/miles-davis", Artist);
70
+ console.log(`\n--- ${miles.title} ---`);
71
+ console.log("Influences:", miles.sections.influences);
72
+ const milesAlbums = miles.relationships.albums.fetchAll();
73
+ console.log(
74
+ `Albums in discography: ${milesAlbums.map((a) => a.title).join(", ")}`
75
+ );
76
+
77
+ // ── Album → Artist relationship ──
78
+ const okc = collection.getModel("albums/ok-computer", Album);
79
+ const artist = okc.relationships.artist.fetch();
80
+ console.log(`\n${okc.title} by ${artist.title} (${artist.meta.origin})`);
81
+
82
+ // ── Serialize ──
83
+ const json = kob.toJSON({
84
+ sections: ["tracklist", "personnel"],
85
+ computed: ["isClassic", "decade"],
86
+ });
87
+ console.log(`\nKind of Blue as JSON:`, JSON.stringify(json, null, 2));
@@ -0,0 +1,132 @@
1
+ import { findBefore } from "unist-util-find-before";
2
+ import { findAfter } from "unist-util-find-after";
3
+ import { findAllBefore } from "unist-util-find-all-before";
4
+ import { findAllAfter } from "unist-util-find-all-after";
5
+ import { visit } from "unist-util-visit";
6
+ import { selectAll, select } from "unist-util-select";
7
+ import { toString } from "mdast-util-to-string";
8
+ import type { Root, Content, Heading, RootContent } from "mdast";
9
+
10
+ export class AstQuery {
11
+ readonly ast: Root;
12
+
13
+ constructor(ast: Root) {
14
+ this.ast = ast;
15
+ }
16
+
17
+ /** Find the first node matching a unist-util-select selector. */
18
+ select(selector: string): Content | null {
19
+ return select(selector, this.ast) as Content | null;
20
+ }
21
+
22
+ /** Find all nodes matching a unist-util-select selector. */
23
+ selectAll(selector: string): Content[] {
24
+ return selectAll(selector, this.ast) as Content[];
25
+ }
26
+
27
+ /** Walk the tree, calling visitor for each node. */
28
+ visit(visitor: (node: Content) => void): void {
29
+ visit(this.ast, (node) => {
30
+ visitor(node as Content);
31
+ });
32
+ }
33
+
34
+ /** Find all nodes before the given node. */
35
+ findAllBefore(
36
+ node: Content,
37
+ test?: string | ((node: Content) => boolean)
38
+ ): Content[] {
39
+ return findAllBefore(this.ast, node as RootContent, test as any) as Content[];
40
+ }
41
+
42
+ /** Find all nodes after the given node. */
43
+ findAllAfter(
44
+ node: Content,
45
+ test?: string | ((node: Content) => boolean)
46
+ ): Content[] {
47
+ return findAllAfter(this.ast, node as RootContent, test as any) as Content[];
48
+ }
49
+
50
+ /** Find the first node before the given node matching the test. */
51
+ findBefore(
52
+ node: Content,
53
+ test?: string | ((node: Content) => boolean)
54
+ ): Content | null {
55
+ return findBefore(this.ast, node as RootContent, test as any) as Content | null;
56
+ }
57
+
58
+ /** Find the first node after the given node matching the test. */
59
+ findAfter(
60
+ node: Content,
61
+ test?: string | ((node: Content) => boolean)
62
+ ): Content | null {
63
+ return findAfter(this.ast, node as RootContent, test as any) as Content | null;
64
+ }
65
+
66
+ /**
67
+ * Find all nodes between two nodes (exclusive on both ends),
68
+ * based on line position.
69
+ */
70
+ findBetween(nodeOne: Content, nodeTwo: Content): Content[] {
71
+ const startLine = nodeOne.position?.end?.line ?? 0;
72
+ const endLine = nodeTwo.position?.start?.line ?? Infinity;
73
+ return this.ast.children.filter(
74
+ (child) =>
75
+ (child.position?.start?.line ?? 0) > startLine &&
76
+ (child.position?.end?.line ?? 0) < endLine
77
+ ) as Content[];
78
+ }
79
+
80
+ /** Get the node at a given line number. */
81
+ atLine(lineNumber: number): Content | undefined {
82
+ return this.ast.children.find(
83
+ (child) => child.position?.start?.line === lineNumber
84
+ ) as Content | undefined;
85
+ }
86
+
87
+ /**
88
+ * Get all headings at a given depth (1-6).
89
+ * Fixed: original had a bug calling this.astQuery.selectAll instead of this.selectAll
90
+ */
91
+ headingsAtDepth(depth: number): Heading[] {
92
+ return (this.selectAll("heading") as Heading[]).filter(
93
+ (h) => h.depth === depth
94
+ );
95
+ }
96
+
97
+ /** Find the next heading node with the same depth as the given heading. */
98
+ findNextSiblingHeadingTo(headingNode: Heading): Heading | undefined {
99
+ const startLine = headingNode.position?.end?.line ?? 0;
100
+ return (this.selectAll("heading") as Heading[]).find(
101
+ (h) =>
102
+ h.depth === headingNode.depth &&
103
+ (h.position?.start?.line ?? 0) > startLine
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Find a heading by its text content. Case insensitive.
109
+ * Pass exact=false for substring matching.
110
+ */
111
+ findHeadingByText(text: string, exact = true): Heading | undefined {
112
+ return (this.selectAll("heading") as Heading[]).find((heading) => {
113
+ const headingText = toString(heading).toLowerCase();
114
+ return exact
115
+ ? headingText.trim() === text.toLowerCase()
116
+ : headingText.includes(text.toLowerCase());
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Find all headings matching the text. Case insensitive.
122
+ * Pass exact=false for substring matching.
123
+ */
124
+ findAllHeadingsByText(text: string, exact = true): Heading[] {
125
+ return (this.selectAll("heading") as Heading[]).filter((heading) => {
126
+ const headingText = toString(heading).toLowerCase();
127
+ return exact
128
+ ? headingText.trim() === text.toLowerCase()
129
+ : headingText.includes(text.toLowerCase());
130
+ });
131
+ }
132
+ }
@@ -0,0 +1,44 @@
1
+ import { defineCommand } from "citty";
2
+ import { loadCollection } from "../load-collection";
3
+
4
+ export default defineCommand({
5
+ meta: {
6
+ name: "action",
7
+ description: "Run a named action on the collection",
8
+ },
9
+ args: {
10
+ name: {
11
+ type: "positional",
12
+ description: "Action name",
13
+ required: true,
14
+ },
15
+ rootPath: {
16
+ type: "string",
17
+ description: "Root path for the collection",
18
+ alias: "r",
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const collection = await loadCollection({
23
+ rootPath: args.rootPath as string | undefined,
24
+ });
25
+
26
+ const actionName = args.name as string;
27
+
28
+ if (!collection.actions.has(actionName)) {
29
+ console.error(
30
+ `Action "${actionName}" not found. Available: ${collection.availableActions.join(", ") || "(none)"}`
31
+ );
32
+ process.exit(1);
33
+ }
34
+
35
+ const result = await collection.runAction(actionName);
36
+ if (result !== undefined) {
37
+ console.log(
38
+ typeof result === "string"
39
+ ? result
40
+ : JSON.stringify(result, null, 2)
41
+ );
42
+ }
43
+ },
44
+ });
@@ -0,0 +1,59 @@
1
+ import { defineCommand } from "citty";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { loadCollection } from "../load-collection";
5
+ import { kebabCase } from "../../utils/inflect";
6
+
7
+ export default defineCommand({
8
+ meta: {
9
+ name: "create",
10
+ description: "Create a new document for a model type",
11
+ },
12
+ args: {
13
+ model: {
14
+ type: "positional",
15
+ description: "Model name",
16
+ required: true,
17
+ },
18
+ title: {
19
+ type: "string",
20
+ description: "Document title",
21
+ required: true,
22
+ },
23
+ rootPath: {
24
+ type: "string",
25
+ description: "Root path for the collection",
26
+ alias: "r",
27
+ },
28
+ },
29
+ async run({ args }) {
30
+ const collection = await loadCollection({
31
+ rootPath: args.rootPath as string | undefined,
32
+ });
33
+
34
+ const modelName = args.model as string;
35
+ const title = args.title as string;
36
+ const def = collection.getModelDefinition(modelName);
37
+
38
+ if (!def) {
39
+ console.error(
40
+ `Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(", ")}`
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ const slug = kebabCase(title.toLowerCase());
46
+ const pathId = `${def.prefix}/${slug}`;
47
+ const filePath = path.resolve(
48
+ collection.rootPath,
49
+ `${pathId}.mdx`
50
+ );
51
+
52
+ const content = `---\n---\n\n# ${title}\n`;
53
+
54
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
55
+ await fs.writeFile(filePath, content, "utf8");
56
+
57
+ console.log(`Created ${filePath}`);
58
+ },
59
+ });
@@ -0,0 +1,24 @@
1
+ import { defineCommand } from "citty";
2
+ import { loadCollection } from "../load-collection";
3
+
4
+ export default defineCommand({
5
+ meta: {
6
+ name: "export",
7
+ description: "Export collection as JSON",
8
+ },
9
+ args: {
10
+ rootPath: {
11
+ type: "string",
12
+ description: "Root path for the collection",
13
+ alias: "r",
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ const collection = await loadCollection({
18
+ rootPath: args.rootPath as string | undefined,
19
+ });
20
+
21
+ const data = await collection.export();
22
+ console.log(JSON.stringify(data, null, 2));
23
+ },
24
+ });
@@ -0,0 +1,75 @@
1
+ import { defineCommand } from "citty";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ export default defineCommand({
6
+ meta: {
7
+ name: "init",
8
+ description: "Initialize a new contentbase project",
9
+ },
10
+ args: {
11
+ name: {
12
+ type: "positional",
13
+ description: "Project name",
14
+ required: false,
15
+ },
16
+ },
17
+ async run({ args }) {
18
+ const name = (args.name as string) || "my-content";
19
+ const dir = path.resolve(process.cwd(), name);
20
+
21
+ await fs.mkdir(dir, { recursive: true });
22
+ await fs.mkdir(path.join(dir, "posts"), { recursive: true });
23
+
24
+ // Create a sample model file
25
+ await fs.writeFile(
26
+ path.join(dir, "models.ts"),
27
+ `import { defineModel, z } from "contentbase";
28
+
29
+ export const Post = defineModel("Post", {
30
+ prefix: "posts",
31
+ meta: z.object({
32
+ status: z.enum(["draft", "published"]).default("draft"),
33
+ author: z.string().optional(),
34
+ }),
35
+ });
36
+ `,
37
+ "utf8"
38
+ );
39
+
40
+ // Create a sample post
41
+ await fs.writeFile(
42
+ path.join(dir, "posts", "hello-world.mdx"),
43
+ `---
44
+ status: draft
45
+ author: me
46
+ ---
47
+
48
+ # Hello World
49
+
50
+ Welcome to your contentbase project!
51
+ `,
52
+ "utf8"
53
+ );
54
+
55
+ // Create index.ts
56
+ await fs.writeFile(
57
+ path.join(dir, "index.ts"),
58
+ `import { Collection } from "contentbase";
59
+ import { Post } from "./models";
60
+
61
+ export const collection = new Collection({
62
+ rootPath: import.meta.dir,
63
+ });
64
+
65
+ collection.register(Post);
66
+ `,
67
+ "utf8"
68
+ );
69
+
70
+ console.log(`Created contentbase project at ${dir}`);
71
+ console.log(` ${name}/models.ts`);
72
+ console.log(` ${name}/index.ts`);
73
+ console.log(` ${name}/posts/hello-world.mdx`);
74
+ },
75
+ });