@fiyuu/core 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@fiyuu/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
- "main": "./src/index.js",
6
- "types": "./src/index.d.ts",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
7
  "exports": {
8
- ".": "./src/index.js",
9
- "./client": "./src/client.js"
8
+ ".": "./src/index.ts",
9
+ "./client": "./src/client.ts"
10
10
  },
11
11
  "dependencies": {
12
12
  "zod": "^3.24.2"
@@ -0,0 +1,328 @@
1
+ import { existsSync, promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createProjectGraph, type ProjectGraph } from "./scanner.js";
4
+
5
+ export async function syncProjectArtifacts(rootDirectory: string, appDirectory: string): Promise<ProjectGraph> {
6
+ const graph = await createProjectGraph(appDirectory);
7
+ const outputDirectory = path.join(rootDirectory, ".fiyuu");
8
+
9
+ await fs.mkdir(outputDirectory, { recursive: true });
10
+ await fs.writeFile(path.join(outputDirectory, "graph.json"), `${JSON.stringify(graph, null, 2)}\n`);
11
+
12
+ const skills = await loadSkillSummaries(rootDirectory);
13
+ const docs = createAiDocs(graph, rootDirectory, skills);
14
+
15
+ await Promise.all([
16
+ fs.writeFile(path.join(outputDirectory, "PROJECT.md"), docs.project),
17
+ fs.writeFile(path.join(outputDirectory, "PATHS.md"), docs.paths),
18
+ fs.writeFile(path.join(outputDirectory, "STATES.md"), docs.states),
19
+ fs.writeFile(path.join(outputDirectory, "FEATURES.md"), docs.features),
20
+ fs.writeFile(path.join(outputDirectory, "WARNINGS.md"), docs.warnings),
21
+ fs.writeFile(path.join(outputDirectory, "SKILLS.md"), docs.skills),
22
+ fs.writeFile(path.join(outputDirectory, "EXECUTION.md"), docs.execution),
23
+ fs.writeFile(path.join(outputDirectory, "INTERVENTIONS.md"), docs.interventions),
24
+ fs.writeFile(path.join(outputDirectory, "DOCTOR.md"), docs.doctor),
25
+ ]);
26
+
27
+ return graph;
28
+ }
29
+
30
+ interface SkillSummary {
31
+ name: string;
32
+ description: string;
33
+ tags: string[];
34
+ filePath: string;
35
+ }
36
+
37
+ async function loadSkillSummaries(rootDirectory: string): Promise<SkillSummary[]> {
38
+ const skillsDir = path.join(rootDirectory, "skills");
39
+ if (!existsSync(skillsDir)) return [];
40
+
41
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
42
+ const skillFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"));
43
+
44
+ return Promise.all(
45
+ skillFiles.map(async (entry) => {
46
+ const filePath = path.join(skillsDir, entry.name);
47
+ const source = await fs.readFile(filePath, "utf8");
48
+
49
+ const nameMatch = source.match(/name\s*:\s*["'`]([^"'`]+)["'`]/);
50
+ const descMatch = source.match(/description\s*:\s*["'`]([^"'`]+)["'`]/);
51
+ const tagsMatch = source.match(/tags\s*:\s*\[([^\]]*)\]/);
52
+ const tags = tagsMatch
53
+ ? tagsMatch[1].match(/["'`]([^"'`]+)["'`]/g)?.map((t) => t.replace(/["'`]/g, "")) ?? []
54
+ : [];
55
+
56
+ return {
57
+ name: nameMatch?.[1] ?? entry.name.replace(/\.ts$/, ""),
58
+ description: descMatch?.[1] ?? "no description",
59
+ tags,
60
+ filePath: `skills/${entry.name}`,
61
+ };
62
+ }),
63
+ );
64
+ }
65
+
66
+ export function createAiDocs(
67
+ graph: ProjectGraph,
68
+ rootDirectory: string,
69
+ skills: SkillSummary[] = [],
70
+ ): {
71
+ project: string;
72
+ paths: string;
73
+ states: string;
74
+ features: string;
75
+ warnings: string;
76
+ skills: string;
77
+ execution: string;
78
+ interventions: string;
79
+ doctor: string;
80
+ } {
81
+ const project = `# Fiyuu Project Context
82
+
83
+ This file is auto-generated for AI systems reading the project.
84
+ Do not edit manually — run \`fiyuu sync\` to regenerate.
85
+
86
+ ## Summary
87
+
88
+ - root: ${rootDirectory}
89
+ - routes: ${graph.routes.length}
90
+ - features: ${graph.features.length}
91
+ - actions: ${graph.actions.length}
92
+ - queries: ${graph.queries.length}
93
+ - skills: ${skills.length}
94
+
95
+ ## Architecture
96
+
97
+ Fiyuu is a file-based fullstack framework built on GEA (@geajs/core).
98
+ Each route is a feature directory under \`app/\` with these fixed files:
99
+ - \`meta.ts\` — intent, title, render mode, SEO (required)
100
+ - \`schema.ts\` — Zod input/output contracts (required)
101
+ - \`page.tsx\` — GEA component for UI (optional)
102
+ - \`query.ts\` — data fetching with \`execute()\` (optional)
103
+ - \`action.ts\` — server mutations with \`execute()\` (optional)
104
+ - \`layout.tsx\` — route-scoped layout wrapper (optional)
105
+ - \`middleware.ts\` — request middleware (optional)
106
+ - \`route.ts\` — raw API route (optional, for \`/api/*\`)
107
+
108
+ ## Route Overview
109
+
110
+ ${graph.features
111
+ .map(
112
+ (feature) =>
113
+ `- route: ${feature.route}\n intent: ${feature.intent ?? "missing"}\n render: ${feature.render}\n missing: ${feature.missingRequiredFiles.join(", ") || "none"}`,
114
+ )
115
+ .join("\n")}
116
+ `;
117
+
118
+ const paths = `# Fiyuu Paths
119
+
120
+ Maps route folders to their fixed files.
121
+ Auto-generated — run \`fiyuu sync\` to update.
122
+
123
+ ${graph.features
124
+ .map((feature) => {
125
+ const entries = Object.entries(feature.files)
126
+ .filter(([, value]) => Boolean(value))
127
+ .map(([name, value]) => ` - ${name}: ${value}`)
128
+ .join("\n");
129
+ return `## ${feature.route}\n- directory: ${feature.directory}\n${entries}`;
130
+ })
131
+ .join("\n\n")}
132
+ `;
133
+
134
+ const states = `# Fiyuu States
135
+
136
+ Current structural state snapshot for AI review.
137
+ Auto-generated — run \`fiyuu sync\` to update.
138
+
139
+ ## Render Modes
140
+
141
+ ${graph.features.map((feature) => `- ${feature.route}: ${feature.render}`).join("\n")}
142
+
143
+ ## Intent State
144
+
145
+ ${graph.features.map((feature) => `- ${feature.route}: ${feature.intent ?? "MISSING"}`).join("\n")}
146
+
147
+ ## Schema Descriptions
148
+
149
+ ${graph.features.map((feature) => `- ${feature.route}: ${feature.descriptions.join(" | ") || "MISSING"}`).join("\n")}
150
+ `;
151
+
152
+ const warnings = `# Fiyuu Warnings
153
+
154
+ Auto-generated — run \`fiyuu sync\` to update.
155
+
156
+ ${
157
+ graph.features.some((feature) => feature.warnings.length > 0)
158
+ ? graph.features
159
+ .filter((feature) => feature.warnings.length > 0)
160
+ .map((feature) => `## ${feature.route}\n${feature.warnings.map((w) => `- ${w}`).join("\n")}`)
161
+ .join("\n\n")
162
+ : "No structural warnings detected.\n"
163
+ }`;
164
+
165
+ const features = `# Fiyuu Features
166
+
167
+ ## Runtime Capabilities
168
+
169
+ - route count: ${graph.routes.length}
170
+ - action count: ${graph.actions.length}
171
+ - query count: ${graph.queries.length}
172
+
173
+ ## Per-Route Capabilities
174
+
175
+ ${graph.features
176
+ .map(
177
+ (feature) =>
178
+ `- ${feature.route}:\n render=${feature.render}, page=${Boolean(feature.files["page.tsx"])}, action=${Boolean(feature.files["action.ts"])}, query=${Boolean(feature.files["query.ts"])}`,
179
+ )
180
+ .join("\n")}
181
+ `;
182
+
183
+ const skillsDoc =
184
+ skills.length === 0
185
+ ? `# Fiyuu Skills
186
+
187
+ No skills found. Create one with \`fiyuu skill new <name>\`.
188
+
189
+ Skills are project-aware scripts in the \`skills/\` directory.
190
+ AI assistants can read and run them for automated project tasks.
191
+ `
192
+ : `# Fiyuu Skills
193
+
194
+ Skills are project-aware scripts AI assistants can read and execute.
195
+ Run a skill: \`fiyuu skill run <name>\`
196
+
197
+ ## Available Skills
198
+
199
+ ${skills
200
+ .map(
201
+ (skill) =>
202
+ `### ${skill.name}\n- file: ${skill.filePath}\n- description: ${skill.description}\n- tags: ${skill.tags.join(", ") || "none"}`,
203
+ )
204
+ .join("\n\n")}
205
+ `;
206
+
207
+ const execution = buildExecutionDoc(graph);
208
+ const interventions = buildInterventionsDoc();
209
+ const doctor = buildDoctorDoc();
210
+
211
+ return {
212
+ project: `${project.trim()}\n`,
213
+ paths: `${paths.trim()}\n`,
214
+ states: `${states.trim()}\n`,
215
+ features: `${features.trim()}\n`,
216
+ warnings: `${warnings.trim()}\n`,
217
+ skills: skillsDoc,
218
+ execution,
219
+ interventions,
220
+ doctor,
221
+ };
222
+ }
223
+
224
+ function buildExecutionDoc(graph: ProjectGraph): string {
225
+ const header = `# Fiyuu Execution Graph
226
+
227
+ Shows the server-side execution chain for every route.
228
+ Auto-generated — run \`fiyuu sync\` to update.
229
+
230
+ Each request follows this deterministic order:
231
+ 1. Middleware (if middleware.ts exists)
232
+ 2. Query (if query.ts exists) → fetches data, optional cache
233
+ 3. Layout (if layout.tsx exists) → wraps page content
234
+ 4. Page (page.tsx) → renders HTML
235
+ 5. Document → injects meta, client runtime, scripts
236
+ 6. Response → sent to browser
237
+
238
+ `;
239
+
240
+ const routeSections = graph.features
241
+ .map((feature) => {
242
+ const steps: string[] = [];
243
+ const hasMiddleware = Boolean((feature.files as Record<string, string | undefined>)["middleware.ts"]);
244
+ const hasQuery = Boolean(feature.files["query.ts"]);
245
+ const hasLayout = Boolean((feature.files as Record<string, string | undefined>)["layout.tsx"]);
246
+ const hasPage = Boolean(feature.files["page.tsx"]);
247
+
248
+ if (hasMiddleware) steps.push(` → middleware.ts intercept / auth / headers`);
249
+ if (hasQuery) steps.push(` → query.ts::execute() fetch data → injected as page props`);
250
+ if (hasLayout) steps.push(` → layout.tsx::template() wrap content`);
251
+ if (hasPage) steps.push(` → page.tsx::Page() render HTML [${feature.render.toUpperCase()}]`);
252
+ steps.push(` → renderDocument() inject meta, runtime, scripts`);
253
+ steps.push(` → HTTP 200 text/html`);
254
+
255
+ const flags: string[] = [];
256
+ if (feature.render === "ssg") flags.push("cached after first render");
257
+ if (feature.render === "csr") flags.push("body empty — client bundle renders");
258
+
259
+ return [
260
+ `## ${feature.route}`,
261
+ `- render: ${feature.render}`,
262
+ `- intent: ${feature.intent ?? "missing"}`,
263
+ flags.length > 0 ? `- notes: ${flags.join(", ")}` : null,
264
+ ``,
265
+ `\`\`\``,
266
+ `Request GET ${feature.route}`,
267
+ ...steps,
268
+ `\`\`\``,
269
+ ]
270
+ .filter(Boolean)
271
+ .join("\n");
272
+ })
273
+ .join("\n\n");
274
+
275
+ return `${header}${routeSections}\n`;
276
+ }
277
+
278
+ function buildInterventionsDoc(): string {
279
+ return `# Fiyuu Interventions
280
+
281
+ Safe framework interventions that can be applied automatically.
282
+
283
+ ## Supported via \`fiyuu doctor --fix\`
284
+
285
+ - Replace \`className=\` with \`class=\` in route source files.
286
+ - Add missing \`export async function execute()\` in \`action.ts\` and \`query.ts\`.
287
+ - Create missing \`app/not-found.tsx\` and \`app/error.tsx\` fallback pages.
288
+ - Add missing \`seo.title\` and \`seo.description\` fields in route \`meta.ts\`.
289
+
290
+ ## Not auto-fixed (manual review required)
291
+
292
+ - \`dangerouslySetInnerHTML\` usage.
293
+ - React imports or hook usage in GEA routes.
294
+ - noJs violations where \`meta.ts\` sets \`noJs: true\` and page uses \`<script>\`.
295
+
296
+ ## SEO baseline
297
+
298
+ - Use route-specific \`seo.title\`.
299
+ - Keep \`seo.description\` around 12-28 words.
300
+ `;
301
+ }
302
+
303
+ function buildDoctorDoc(): string {
304
+ return `# Fiyuu Doctor
305
+
306
+ Command reference for deterministic checks and safe fixes.
307
+
308
+ ## Check only
309
+
310
+ \`fiyuu doctor\`
311
+
312
+ - Reports structure violations and quality warnings.
313
+
314
+ ## Check + safe fixes
315
+
316
+ \`fiyuu doctor --fix\`
317
+
318
+ - Applies only deterministic, low-risk fixes.
319
+ - Prints each fixed item with \`(fixed)\` marker.
320
+
321
+ ## Typical workflow
322
+
323
+ 1. \`fiyuu sync\`
324
+ 2. \`fiyuu doctor --fix\`
325
+ 3. \`fiyuu doctor\`
326
+ 4. \`fiyuu build\`
327
+ `;
328
+ }
package/src/config.ts ADDED
@@ -0,0 +1,260 @@
1
+ import { existsSync } from "node:fs";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ export interface FiyuuConfig {
7
+ app?: {
8
+ name?: string;
9
+ runtime?: "node" | "bun";
10
+ port?: number;
11
+ };
12
+ ai?: {
13
+ enabled?: boolean;
14
+ skillsDirectory?: string;
15
+ defaultSkills?: string[];
16
+ graphContext?: boolean;
17
+ inspector?: {
18
+ enabled?: boolean;
19
+ localModelCommand?: string;
20
+ timeoutMs?: number;
21
+ autoSetupPrompt?: boolean;
22
+ };
23
+ };
24
+ fullstack?: {
25
+ client?: boolean;
26
+ serverActions?: boolean;
27
+ serverQueries?: boolean;
28
+ sockets?: boolean;
29
+ };
30
+ data?: {
31
+ driver?: string;
32
+ path?: string;
33
+ autosave?: boolean;
34
+ autosaveIntervalMs?: number;
35
+ tables?: string[];
36
+ };
37
+ security?: {
38
+ requestEncryption?: boolean;
39
+ serverSecretFile?: string;
40
+ };
41
+ websocket?: {
42
+ enabled?: boolean;
43
+ path?: string;
44
+ heartbeatMs?: number;
45
+ maxPayloadBytes?: number;
46
+ };
47
+ realtime?: {
48
+ enabled?: boolean;
49
+ transports?: ("websocket" | "nats")[];
50
+ websocket?: {
51
+ path?: string;
52
+ heartbeatMs?: number;
53
+ maxPayloadBytes?: number;
54
+ };
55
+ nats?: {
56
+ url?: string;
57
+ name?: string;
58
+ };
59
+ };
60
+ services?: {
61
+ enabled?: boolean;
62
+ directory?: string;
63
+ failFast?: boolean;
64
+ };
65
+ middleware?: {
66
+ enabled?: boolean;
67
+ };
68
+ developerTools?: {
69
+ enabled?: boolean;
70
+ renderTiming?: boolean;
71
+ };
72
+ observability?: {
73
+ requestId?: boolean;
74
+ warningsAsOverlay?: boolean;
75
+ };
76
+ auth?: {
77
+ enabled?: boolean;
78
+ sessionStrategy?: "cookie" | "token";
79
+ };
80
+ analytics?: {
81
+ enabled?: boolean;
82
+ provider?: string;
83
+ };
84
+ featureFlags?: {
85
+ enabled?: boolean;
86
+ defaults?: Record<string, boolean>;
87
+ };
88
+ errors?: {
89
+ /**
90
+ * Called for every unhandled server error.
91
+ * Use this to send errors to Sentry, Datadog, etc.
92
+ */
93
+ handler?: (error: Error, context: { route: string; method: string; requestId: string }) => void | Promise<void>;
94
+ /**
95
+ * Whether to expose error details (stack trace, message) in responses.
96
+ * Defaults to true in dev, false in production.
97
+ */
98
+ expose?: boolean;
99
+ };
100
+ deploy?: {
101
+ /**
102
+ * Enables `fiyuu deploy` workflow.
103
+ * If undefined, deploy command still works when `deploy.ssh` is configured.
104
+ */
105
+ enabled?: boolean;
106
+ /**
107
+ * Build locally before upload. Defaults to true.
108
+ */
109
+ localBuild?: boolean;
110
+ /**
111
+ * Extra archive exclude patterns for `tar`.
112
+ */
113
+ excludes?: string[];
114
+ ssh?: {
115
+ host: string;
116
+ user: string;
117
+ port?: number;
118
+ privateKeyPath?: string;
119
+ /**
120
+ * Absolute path on remote server (for example: /var/www/my-app)
121
+ */
122
+ destinationPath: string;
123
+ /**
124
+ * Number of old releases to keep on the server.
125
+ */
126
+ keepReleases?: number;
127
+ };
128
+ remote?: {
129
+ /**
130
+ * Command executed on remote server in release directory.
131
+ */
132
+ installCommand?: string;
133
+ /**
134
+ * Command executed on remote server after install.
135
+ */
136
+ buildCommand?: string;
137
+ /**
138
+ * Command executed on remote server to start/reload the app.
139
+ */
140
+ startCommand?: string;
141
+ /**
142
+ * Optional validation command after start.
143
+ */
144
+ healthcheckCommand?: string;
145
+ };
146
+ pm2?: {
147
+ enabled?: boolean;
148
+ /**
149
+ * Auto-generate ecosystem file in project root if missing.
150
+ */
151
+ autoCreate?: boolean;
152
+ ecosystemFile?: string;
153
+ appName?: string;
154
+ instances?: number | "max";
155
+ execMode?: "fork" | "cluster";
156
+ maxMemoryRestart?: string;
157
+ env?: Record<string, string>;
158
+ };
159
+ };
160
+ cloud?: {
161
+ /**
162
+ * Control-plane API endpoint for `fiyuu cloud`.
163
+ * Example: https://api.fiyuu.work
164
+ */
165
+ endpoint?: string;
166
+ /**
167
+ * Default project slug used by `fiyuu cloud deploy`.
168
+ */
169
+ project?: string;
170
+ /**
171
+ * Extra archive exclude patterns used by cloud deploy packaging.
172
+ */
173
+ excludes?: string[];
174
+ };
175
+ seo?: {
176
+ /**
177
+ * Base URL used in sitemap.xml and canonical links.
178
+ * Example: https://hacimertgokhan.me
179
+ */
180
+ baseUrl?: string;
181
+ /**
182
+ * Enable automatic sitemap.xml generation from scanned routes.
183
+ * Defaults to false.
184
+ */
185
+ sitemap?: boolean;
186
+ /**
187
+ * Enable automatic robots.txt generation.
188
+ * Defaults to false.
189
+ */
190
+ robots?: boolean;
191
+ };
192
+ }
193
+
194
+ export interface LoadedFiyuuConfig {
195
+ config: FiyuuConfig;
196
+ env: Record<string, string>;
197
+ }
198
+
199
+ export async function loadFiyuuConfig(rootDirectory: string, mode: "dev" | "start" = "dev"): Promise<LoadedFiyuuConfig> {
200
+ const env = await loadFiyuuEnvironment(rootDirectory, mode);
201
+ const configPath = path.join(rootDirectory, "fiyuu.config.ts");
202
+
203
+ if (!existsSync(configPath)) {
204
+ applyEnv(env);
205
+ return { config: {}, env };
206
+ }
207
+
208
+ const moduleUrl = pathToFileURL(configPath).href;
209
+ const loaded = await import(`${moduleUrl}?t=${Date.now()}`);
210
+ const config = ((loaded.default ?? loaded.config ?? {}) as FiyuuConfig) ?? {};
211
+ applyEnv(env);
212
+ return { config, env };
213
+ }
214
+
215
+ async function loadFiyuuEnvironment(rootDirectory: string, mode: "dev" | "start"): Promise<Record<string, string>> {
216
+ const directory = path.join(rootDirectory, ".fiyuu");
217
+ const files = [
218
+ path.join(directory, "env"),
219
+ path.join(directory, `${mode}.env`),
220
+ path.join(directory, "SECRET"),
221
+ ];
222
+ const env: Record<string, string> = {};
223
+
224
+ for (const filePath of files) {
225
+ if (!existsSync(filePath)) {
226
+ continue;
227
+ }
228
+
229
+ const name = path.basename(filePath);
230
+ if (name === "SECRET") {
231
+ env.FIYUU_SECRET = (await fs.readFile(filePath, "utf8")).trim();
232
+ continue;
233
+ }
234
+
235
+ const content = await fs.readFile(filePath, "utf8");
236
+ for (const line of content.split(/\r?\n/)) {
237
+ const trimmed = line.trim();
238
+ if (!trimmed || trimmed.startsWith("#")) {
239
+ continue;
240
+ }
241
+ const separator = trimmed.indexOf("=");
242
+ if (separator <= 0) {
243
+ continue;
244
+ }
245
+ const key = trimmed.slice(0, separator).trim();
246
+ const value = trimmed.slice(separator + 1).trim();
247
+ env[key] = value;
248
+ }
249
+ }
250
+
251
+ return env;
252
+ }
253
+
254
+ function applyEnv(env: Record<string, string>): void {
255
+ for (const [key, value] of Object.entries(env)) {
256
+ if (process.env[key] === undefined) {
257
+ process.env[key] = value;
258
+ }
259
+ }
260
+ }