@fragno-dev/cli 0.1.17 → 0.1.19

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.
@@ -5,6 +5,9 @@ import {
5
5
  getAllSubjects,
6
6
  getSubjectParent,
7
7
  getSubjectChildren,
8
+ getAllSubjectIdsInOrder,
9
+ isCategory,
10
+ getCategoryTitle,
8
11
  } from "@fragno-dev/corpus";
9
12
  import type { Subject, Example } from "@fragno-dev/corpus";
10
13
  import { marked } from "marked";
@@ -25,7 +28,7 @@ interface PrintOptions {
25
28
  /**
26
29
  * Build markdown content for multiple subjects
27
30
  */
28
- function buildSubjectsMarkdown(subjects: Subject[]): string {
31
+ export function buildSubjectsMarkdown(subjects: Subject[]): string {
29
32
  let fullMarkdown = "";
30
33
 
31
34
  for (const subject of subjects) {
@@ -61,7 +64,7 @@ function buildSubjectsMarkdown(subjects: Subject[]): string {
61
64
  /**
62
65
  * Add line numbers to content
63
66
  */
64
- function addLineNumbers(content: string, startFrom: number = 1): string {
67
+ export function addLineNumbers(content: string, startFrom: number = 1): string {
65
68
  const lines = content.split("\n");
66
69
  const maxDigits = String(startFrom + lines.length - 1).length;
67
70
 
@@ -77,7 +80,7 @@ function addLineNumbers(content: string, startFrom: number = 1): string {
77
80
  /**
78
81
  * Filter content by line range
79
82
  */
80
- function filterByLineRange(content: string, startLine: number, endLine: number): string {
83
+ export function filterByLineRange(content: string, startLine: number, endLine: number): string {
81
84
  const lines = content.split("\n");
82
85
  // Convert to 0-based index
83
86
  const start = Math.max(0, startLine - 1);
@@ -88,7 +91,7 @@ function filterByLineRange(content: string, startLine: number, endLine: number):
88
91
  /**
89
92
  * Extract headings and code block information with line numbers
90
93
  */
91
- function extractHeadingsAndBlocks(subjects: Subject[]): string {
94
+ export function extractHeadingsAndBlocks(subjects: Subject[]): string {
92
95
  let output = "";
93
96
  let currentLine = 1;
94
97
  let lastOutputLine = 0;
@@ -442,6 +445,52 @@ async function printCodeBlockById(
442
445
  }
443
446
  }
444
447
 
448
+ /**
449
+ * Print only the topic tree
450
+ */
451
+ function printTopicTree(): void {
452
+ const subjects = getSubjects();
453
+ const subjectMap = new Map(subjects.map((s) => [s.id, s]));
454
+
455
+ // Helper function to get title for any subject ID (including categories)
456
+ function getTitle(subjectId: string): string {
457
+ if (isCategory(subjectId)) {
458
+ return getCategoryTitle(subjectId);
459
+ }
460
+ const subject = subjectMap.get(subjectId);
461
+ return subject ? subject.title : subjectId;
462
+ }
463
+
464
+ // Helper function to recursively display tree
465
+ function displayNode(subjectId: string, indent: string, isLast: boolean, isRoot: boolean): void {
466
+ const title = getTitle(subjectId);
467
+
468
+ if (isRoot) {
469
+ console.log(` ${subjectId.padEnd(30)} ${title}`);
470
+ } else {
471
+ const connector = isLast ? "└─" : "├─";
472
+ console.log(`${indent}${connector} ${subjectId.padEnd(26)} ${title}`);
473
+ }
474
+
475
+ const children = getSubjectChildren(subjectId);
476
+ if (children.length > 0) {
477
+ const childIndent = isRoot ? " " : indent + (isLast ? " " : "│ ");
478
+ for (let i = 0; i < children.length; i++) {
479
+ displayNode(children[i], childIndent, i === children.length - 1, false);
480
+ }
481
+ }
482
+ }
483
+
484
+ // Get all root subject IDs (including categories)
485
+ const allIds = getAllSubjectIdsInOrder();
486
+ const rootIds = allIds.filter((id) => !getSubjectParent(id));
487
+
488
+ // Display root subjects
489
+ for (const subjectId of rootIds) {
490
+ displayNode(subjectId, "", false, true);
491
+ }
492
+ }
493
+
445
494
  /**
446
495
  * Print information about the corpus command
447
496
  */
@@ -456,9 +505,11 @@ function printCorpusHelp(): void {
456
505
  console.log(" -e, --end N Ending line number to display to");
457
506
  console.log(" --headings Show only headings and code block IDs");
458
507
  console.log(" --id <id> Retrieve a specific code block by ID");
508
+ console.log(" --tree Show only the topic tree");
459
509
  console.log("");
460
510
  console.log("Examples:");
461
511
  console.log(" fragno-cli corpus # List all available topics");
512
+ console.log(" fragno-cli corpus --tree # Show only the topic tree");
462
513
  console.log(" fragno-cli corpus defining-routes # Show route definition examples");
463
514
  console.log(" fragno-cli corpus --headings database-querying");
464
515
  console.log(" # Show structure overview");
@@ -470,42 +521,7 @@ function printCorpusHelp(): void {
470
521
  console.log("");
471
522
  console.log("Available topics:");
472
523
 
473
- const subjects = getSubjects();
474
-
475
- // Group subjects by their tree structure
476
- const rootSubjects: Array<{
477
- id: string;
478
- title: string;
479
- children: Array<{ id: string; title: string }>;
480
- }> = [];
481
- const subjectMap = new Map(subjects.map((s) => [s.id, s]));
482
-
483
- for (const subject of subjects) {
484
- const parent = getSubjectParent(subject.id);
485
- if (!parent) {
486
- // This is a root subject
487
- const children = getSubjectChildren(subject.id);
488
- rootSubjects.push({
489
- id: subject.id,
490
- title: subject.title,
491
- children: children.map((childId) => ({
492
- id: childId,
493
- title: subjectMap.get(childId)?.title || childId,
494
- })),
495
- });
496
- }
497
- }
498
-
499
- // Display in tree format
500
- for (const root of rootSubjects) {
501
- console.log(` ${root.id.padEnd(30)} ${root.title}`);
502
- for (let i = 0; i < root.children.length; i++) {
503
- const child = root.children[i];
504
- const isLast = i === root.children.length - 1;
505
- const connector = isLast ? "└─" : "├─";
506
- console.log(` ${connector} ${child.id.padEnd(26)} ${child.title}`);
507
- }
508
- }
524
+ printTopicTree();
509
525
  }
510
526
 
511
527
  export const corpusCommand = define({
@@ -535,6 +551,10 @@ export const corpusCommand = define({
535
551
  type: "string",
536
552
  description: "Retrieve a specific code block by ID",
537
553
  },
554
+ tree: {
555
+ type: "boolean",
556
+ description: "Show only the topic tree (without help text)",
557
+ },
538
558
  },
539
559
  run: async (ctx) => {
540
560
  const topics = ctx.positionals;
@@ -543,6 +563,7 @@ export const corpusCommand = define({
543
563
  const endLine = ctx.values.end;
544
564
  const headingsOnly = ctx.values.headings ?? false;
545
565
  const codeBlockId = ctx.values.id;
566
+ const treeOnly = ctx.values.tree ?? false;
546
567
 
547
568
  // Handle --id flag
548
569
  if (codeBlockId) {
@@ -550,6 +571,12 @@ export const corpusCommand = define({
550
571
  return;
551
572
  }
552
573
 
574
+ // Handle --tree flag
575
+ if (treeOnly) {
576
+ printTopicTree();
577
+ return;
578
+ }
579
+
553
580
  // No topics provided - show help
554
581
  if (topics.length === 0) {
555
582
  printCorpusHelp();
@@ -573,8 +600,32 @@ export const corpusCommand = define({
573
600
  headingsOnly,
574
601
  });
575
602
  } catch (error) {
576
- console.error("Error loading topics:", error instanceof Error ? error.message : error);
577
- console.log("\nRun 'fragno-cli corpus' to see available topics.");
603
+ if (error instanceof Error && error.message.includes("ENOENT")) {
604
+ // Extract the subject name from the error message or use the topics array
605
+ const missingTopics = topics.filter((topic) => {
606
+ try {
607
+ getSubject(topic);
608
+ return false;
609
+ } catch {
610
+ return true;
611
+ }
612
+ });
613
+
614
+ if (missingTopics.length === 1) {
615
+ console.error(`Error: Subject '${missingTopics[0]}' not found.`);
616
+ } else if (missingTopics.length > 1) {
617
+ console.error(
618
+ `Error: Subjects not found: ${missingTopics.map((t) => `'${t}'`).join(", ")}`,
619
+ );
620
+ } else {
621
+ console.error("Error: One or more subjects not found.");
622
+ }
623
+ console.log("\nAvailable topics:");
624
+ printTopicTree();
625
+ } else {
626
+ console.error("Error loading topics:", error instanceof Error ? error.message : error);
627
+ console.log("\nRun 'fragno-cli corpus' to see available topics.");
628
+ }
578
629
  process.exit(1);
579
630
  }
580
631
  },
@@ -4,10 +4,8 @@ import {
4
4
  fragnoDatabaseAdapterVersionFakeSymbol,
5
5
  } from "@fragno-dev/db/adapters";
6
6
  import type { AnySchema } from "@fragno-dev/db/schema";
7
- import {
8
- instantiatedFragmentFakeSymbol,
9
- type FragnoInstantiatedFragment,
10
- } from "@fragno-dev/core/api/fragment-instantiation";
7
+ import { instantiatedFragmentFakeSymbol } from "@fragno-dev/core/internal/symbols";
8
+ import { type FragnoInstantiatedFragment } from "@fragno-dev/core";
11
9
  import { loadConfig } from "c12";
12
10
  import { relative } from "node:path";
13
11
 
@@ -126,9 +124,17 @@ export async function importFragmentFiles(paths: string[]): Promise<{
126
124
  };
127
125
  }
128
126
 
129
- function isFragnoInstantiatedFragment(
127
+ function isNewFragnoInstantiatedFragment(
130
128
  value: unknown,
131
- ): value is FragnoInstantiatedFragment<[], {}, {}, {}> {
129
+ ): value is FragnoInstantiatedFragment<
130
+ [],
131
+ unknown,
132
+ Record<string, unknown>,
133
+ Record<string, unknown>,
134
+ Record<string, unknown>,
135
+ unknown,
136
+ Record<string, unknown>
137
+ > {
132
138
  return (
133
139
  typeof value === "object" &&
134
140
  value !== null &&
@@ -137,20 +143,6 @@ function isFragnoInstantiatedFragment(
137
143
  );
138
144
  }
139
145
 
140
- function additionalContextIsDatabaseContext(additionalContext: unknown): additionalContext is {
141
- databaseSchema: AnySchema;
142
- databaseNamespace: string;
143
- databaseAdapter: DatabaseAdapter;
144
- } {
145
- return (
146
- typeof additionalContext === "object" &&
147
- additionalContext !== null &&
148
- "databaseSchema" in additionalContext &&
149
- "databaseNamespace" in additionalContext &&
150
- "databaseAdapter" in additionalContext
151
- );
152
- }
153
-
154
146
  /**
155
147
  * Finds all FragnoDatabase instances in a module, including those embedded
156
148
  * in instantiated fragments.
@@ -163,20 +155,34 @@ export function findFragnoDatabases(
163
155
  for (const [_key, value] of Object.entries(targetModule)) {
164
156
  if (isFragnoDatabase(value)) {
165
157
  fragnoDatabases.push(value);
166
- } else if (isFragnoInstantiatedFragment(value)) {
167
- const additionalContext = value.additionalContext;
158
+ } else if (isNewFragnoInstantiatedFragment(value)) {
159
+ // Handle new fragment API
160
+ const internal = value.$internal;
161
+ const deps = internal.deps as Record<string, unknown>;
162
+ const options = internal.options as Record<string, unknown>;
163
+
164
+ // Check if this is a database fragment by looking for implicit database dependencies
165
+ if (!deps["db"] || !deps["schema"]) {
166
+ continue;
167
+ }
168
+
169
+ const schema = deps["schema"] as AnySchema;
170
+ const databaseAdapter = options["databaseAdapter"] as DatabaseAdapter | undefined;
168
171
 
169
- if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {
172
+ if (!databaseAdapter) {
173
+ console.warn(
174
+ `Warning: Fragment '${value.name}' appears to be a database fragment but no databaseAdapter found in options.`,
175
+ );
170
176
  continue;
171
177
  }
172
178
 
173
- // Extract database schema, namespace, and adapter from instantiated fragment's additionalContext
174
- const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;
179
+ // Derive namespace from fragment name (follows convention: fragmentName + "-db")
180
+ const namespace = value.name + "-db";
175
181
 
176
182
  fragnoDatabases.push(
177
183
  new FragnoDatabase({
178
- namespace: databaseNamespace,
179
- schema: databaseSchema,
184
+ namespace,
185
+ schema,
180
186
  adapter: databaseAdapter,
181
187
  }),
182
188
  );