create-stitchkit 0.1.0 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +11 -4
- package/dist/cli.js +99 -12
- package/examples/repository/_env.append +3 -0
- package/examples/repository/_env.example.append +4 -0
- package/examples/repository/e2e/repository.spec.ts +19 -0
- package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.ts +2 -1
- package/examples/repository/packages/backend/src/surface.ts +14 -0
- package/examples/repository/packages/config/src/features.ts +7 -0
- package/examples/repository/packages/db/schema.prisma +31 -0
- package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +45 -0
- package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
- package/examples/repository/packages/frontend/src/providers/index.tsx +18 -0
- package/examples/repository/packages/shared/src/index.ts +3 -0
- package/examples/repository/scripts/runtime-smoke.ts +91 -0
- package/package.json +10 -1
- package/template/AGENTS.md +50 -0
- package/template/README.md +18 -5
- package/template/_env +1 -3
- package/template/_env.example +1 -4
- package/template/app.config.json +9 -0
- package/template/bun.lock +13 -141
- package/template/docs/ADDING_A_FEATURE.md +101 -0
- package/template/docs/LAN_HTTPS.md +28 -0
- package/template/e2e/starter.spec.ts +17 -22
- package/template/ecosystem.config.cjs +3 -2
- package/template/ecosystem.dev.config.cjs +19 -4
- package/template/package.json +4 -2
- package/template/packages/backend/package.json +1 -1
- package/template/packages/backend/src/cli.ts +2 -1
- package/template/packages/backend/src/index.ts +22 -6
- package/template/packages/backend/src/surface-manifest.test.ts +62 -0
- package/template/packages/backend/src/surface-manifest.ts +117 -0
- package/template/packages/backend/src/surface.ts +3 -9
- package/template/packages/backend/src/transport/lan-onboarding.ts +29 -0
- package/template/packages/config/package.json +2 -1
- package/template/packages/config/src/features.ts +1 -0
- package/template/packages/config/src/identity.ts +18 -0
- package/template/packages/config/src/server.ts +13 -3
- package/template/packages/db/schema.prisma +0 -23
- package/template/packages/frontend/messages/en.json +0 -1
- package/template/packages/frontend/messages/ru.json +0 -1
- package/template/packages/frontend/package.json +1 -0
- package/template/packages/frontend/src/app/[locale]/page.tsx +8 -19
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +3 -4
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +3 -2
- package/template/packages/frontend/src/lib/seo/pages.ts +4 -5
- package/template/packages/frontend/src/providers/index.tsx +1 -4
- package/template/packages/frontend/src/theme/config.ts +2 -1
- package/template/packages/shared/src/index.ts +1 -3
- package/template/scripts/dev-lan.test.ts +58 -0
- package/template/scripts/dev-lan.ts +176 -0
- package/template/scripts/dev.ts +37 -7
- package/template/scripts/runtime-smoke.ts +13 -57
- package/template/scripts/surface-conformance.ts +105 -0
- /package/{template → examples/repository}/packages/backend/src/domain/errors.ts +0 -0
- /package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.test.ts +0 -0
- /package/{template → examples/repository}/packages/backend/src/transport/repository-service.ts +0 -0
- /package/{template → examples/repository}/packages/db/migrations/20260808000000_init/migration.sql +0 -0
- /package/{template → examples/repository}/packages/db/migrations/20260808170000_repository_visibility/migration.sql +0 -0
- /package/{template → examples/repository}/packages/frontend/src/components/repository-summary.tsx +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/api/client.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/api/queries.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/lib/realtime/repository.ts +0 -0
- /package/{template → examples/repository}/packages/frontend/src/providers/realtime.tsx +0 -0
- /package/{template → examples/repository}/packages/shared/src/contracts/repository.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/events/repository.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/schemas/repository.test.ts +0 -0
- /package/{template → examples/repository}/packages/shared/src/schemas/repository.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ is declared in the template root catalog.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] — 2026-08-10
|
|
10
|
+
|
|
11
|
+
### ⚠️ Breaking changes
|
|
12
|
+
|
|
13
|
+
- **The default scaffold is now domain-free.** The repository example is explicit
|
|
14
|
+
so a new product does not need to dismantle demo code.
|
|
15
|
+
`bun create stitchkit my-app` now creates the blank base; use
|
|
16
|
+
`bun create stitchkit my-app --example repository` for the previous runnable example.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **One generated application identity.** A validated `app.config.json` now drives
|
|
21
|
+
package, PM2, MCP, OpenAPI, CLI, SEO and theme identity from one edit point.
|
|
22
|
+
- **Contract-derived surface conformance.** Generated runtime smoke compares the
|
|
23
|
+
registered HTTP and tool manifest with live OpenAPI and MCP discovery, while
|
|
24
|
+
explicit typed probes remain application-owned.
|
|
25
|
+
- **Optional trusted LAN HTTPS.** `bun run dev:lan` uses mkcert, exact-origin CORS
|
|
26
|
+
and a development-only onboarding route for secure-context testing on physical
|
|
27
|
+
devices without changing ordinary development or production behavior.
|
|
28
|
+
- **Agent-first extension guide.** Generated applications include an application-
|
|
29
|
+
scoped `AGENTS.md` and a complete schema-to-runtime feature workflow in
|
|
30
|
+
`docs/ADDING_A_FEATURE.md`.
|
|
31
|
+
|
|
32
|
+
## [0.1.1] — 2026-08-10
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Starter MCP uses Stitchkit 0.44 and the split MCP TypeScript SDK v2.** The
|
|
37
|
+
generated backend mounts the framework-owned HTTP route, closes its handler
|
|
38
|
+
during graceful shutdown and validates the modern protocol with the v2 client.
|
|
39
|
+
|
|
9
40
|
## [0.1.0] — 2026-08-08
|
|
10
41
|
|
|
11
42
|
### Added
|
package/README.md
CHANGED
|
@@ -14,11 +14,18 @@ API, Prisma/PostgreSQL, typed shared contracts, Socket.IO, MCP, CLI tools and a
|
|
|
14
14
|
complete production UI system.
|
|
15
15
|
|
|
16
16
|
It uses one conventional `packages/*` namespace: `backend`, `frontend`,
|
|
17
|
-
`config`, `db` and `shared`. The
|
|
18
|
-
|
|
17
|
+
`config`, `db` and `shared`. The destination name becomes the generated slug;
|
|
18
|
+
`--display-name` sets the human title. Both are recorded once in
|
|
19
|
+
`app.config.json` and drive package, process, transport, UI and SEO identity.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
The default scaffold is domain-free. To add the runnable repository example:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun create stitchkit my-app --example repository
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The optional example uses one configurable GitHub repository persisted through a
|
|
28
|
+
server-side PostgreSQL cache, including a Prisma-backed
|
|
22
29
|
visibility enum that flows through the shared schema and typed client. `/en/ui`
|
|
23
30
|
presents every reusable primitive and composition. Every public page ships with
|
|
24
31
|
localized metadata, canonical and language alternatives, sitemap coverage and a
|
package/dist/cli.js
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { basename as
|
|
5
|
+
import { basename as basename3, resolve as resolve2 } from "path";
|
|
6
6
|
var {spawn } = globalThis.Bun;
|
|
7
7
|
|
|
8
8
|
// src/options.ts
|
|
9
9
|
var HELP = `Create a production-shaped Stitchkit application.
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
bun create stitchkit <directory> [--no-install]
|
|
12
|
+
bun create stitchkit <directory> [--display-name "Product Name"] [--example repository] [--no-install]
|
|
13
13
|
|
|
14
14
|
Options:
|
|
15
15
|
--no-install Generate files without installing dependencies
|
|
16
|
+
--example Add an isolated runnable example (supported: repository)
|
|
17
|
+
--display-name Set the initial public application name
|
|
16
18
|
--help Show this help
|
|
17
19
|
`;
|
|
18
20
|
function helpText() {
|
|
@@ -21,11 +23,26 @@ function helpText() {
|
|
|
21
23
|
function parseOptions(args) {
|
|
22
24
|
if (args.includes("--help") || args.includes("-h"))
|
|
23
25
|
return "help";
|
|
24
|
-
const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install");
|
|
26
|
+
const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install" && arg !== "--example" && arg !== "--display-name");
|
|
25
27
|
if (unknown.length > 0) {
|
|
26
28
|
throw new Error(`Unknown option: ${unknown[0]}`);
|
|
27
29
|
}
|
|
28
|
-
const
|
|
30
|
+
const exampleFlagIndex = args.indexOf("--example");
|
|
31
|
+
const example = exampleFlagIndex === -1 ? undefined : args[exampleFlagIndex + 1];
|
|
32
|
+
if (exampleFlagIndex !== -1 && example === undefined) {
|
|
33
|
+
throw new Error("--example requires a value");
|
|
34
|
+
}
|
|
35
|
+
if (example !== undefined && example !== "repository") {
|
|
36
|
+
throw new Error(`Unknown example: ${example}`);
|
|
37
|
+
}
|
|
38
|
+
const displayNameFlagIndex = args.indexOf("--display-name");
|
|
39
|
+
const displayName = displayNameFlagIndex === -1 ? undefined : args[displayNameFlagIndex + 1];
|
|
40
|
+
if (displayNameFlagIndex !== -1 && displayName === undefined) {
|
|
41
|
+
throw new Error("--display-name requires a value");
|
|
42
|
+
}
|
|
43
|
+
const consumedExampleIndex = exampleFlagIndex === -1 ? -1 : exampleFlagIndex + 1;
|
|
44
|
+
const consumedDisplayNameIndex = displayNameFlagIndex === -1 ? -1 : displayNameFlagIndex + 1;
|
|
45
|
+
const positionals = args.filter((arg, index) => !arg.startsWith("-") && index !== consumedExampleIndex && index !== consumedDisplayNameIndex);
|
|
29
46
|
if (positionals.length !== 1) {
|
|
30
47
|
throw new Error("Exactly one destination directory is required");
|
|
31
48
|
}
|
|
@@ -34,14 +51,48 @@ function parseOptions(args) {
|
|
|
34
51
|
throw new Error("Destination directory is required");
|
|
35
52
|
return {
|
|
36
53
|
destination,
|
|
37
|
-
install: !args.includes("--no-install")
|
|
54
|
+
install: !args.includes("--no-install"),
|
|
55
|
+
...example && { example },
|
|
56
|
+
...displayName && { displayName }
|
|
38
57
|
};
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
// src/scaffold.ts
|
|
42
61
|
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
43
62
|
import { homedir } from "os";
|
|
44
|
-
import { basename, dirname, join, parse, relative, resolve, sep } from "path";
|
|
63
|
+
import { basename as basename2, dirname, join, parse, relative, resolve, sep } from "path";
|
|
64
|
+
|
|
65
|
+
// src/identity.ts
|
|
66
|
+
import { basename } from "path";
|
|
67
|
+
import { z } from "zod";
|
|
68
|
+
var ApplicationSlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers and single hyphens (for example: talk-control)");
|
|
69
|
+
var ApplicationIdentitySchema = z.object({
|
|
70
|
+
slug: ApplicationSlugSchema,
|
|
71
|
+
name: z.string().trim().min(1).max(80),
|
|
72
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, "Use a semantic version such as 0.1.0"),
|
|
73
|
+
description: z.object({
|
|
74
|
+
en: z.string().trim().min(1),
|
|
75
|
+
ru: z.string().trim().min(1)
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
function displayNameFromSlug(slug) {
|
|
79
|
+
return slug.split("-").map((part) => `${part[0]?.toUpperCase()}${part.slice(1)}`).join(" ");
|
|
80
|
+
}
|
|
81
|
+
function createApplicationIdentity(destination, displayName) {
|
|
82
|
+
const slug = ApplicationSlugSchema.parse(basename(destination));
|
|
83
|
+
const name = displayName?.trim() || displayNameFromSlug(slug);
|
|
84
|
+
return ApplicationIdentitySchema.parse({
|
|
85
|
+
slug,
|
|
86
|
+
name,
|
|
87
|
+
version: "0.1.0",
|
|
88
|
+
description: {
|
|
89
|
+
en: `${name} is a production application built with Stitchkit.`,
|
|
90
|
+
ru: `${name} \u2014 production-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043D\u0430 Stitchkit.`
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/scaffold.ts
|
|
45
96
|
var TEXT_EXTENSIONS = new Set([
|
|
46
97
|
".cjs",
|
|
47
98
|
".css",
|
|
@@ -62,7 +113,9 @@ var TEXT_EXTENSIONS = new Set([
|
|
|
62
113
|
]);
|
|
63
114
|
var TEMPLATE_RENAMES = new Map([
|
|
64
115
|
["_env", ".env"],
|
|
116
|
+
["_env.append", ".env"],
|
|
65
117
|
["_env.example", ".env.example"],
|
|
118
|
+
["_env.example.append", ".env.example"],
|
|
66
119
|
["_gitignore", ".gitignore"]
|
|
67
120
|
]);
|
|
68
121
|
var IGNORED_DIRECTORIES = new Set([
|
|
@@ -82,7 +135,7 @@ function isTemplateSourcePathIncluded(sourcePath) {
|
|
|
82
135
|
return false;
|
|
83
136
|
if (normalized === "packages/db/src/generated" || normalized.startsWith("packages/db/src/generated/"))
|
|
84
137
|
return false;
|
|
85
|
-
const name =
|
|
138
|
+
const name = basename2(normalized);
|
|
86
139
|
return name !== ".env" && name !== "next-env.d.ts" && !name.endsWith(".log") && !name.endsWith(".tsbuildinfo");
|
|
87
140
|
}
|
|
88
141
|
function shouldIncludeTemplatePath(templateDirectory, sourcePath) {
|
|
@@ -142,7 +195,8 @@ async function collectMaterialisedFiles(templateDirectory, directory, files) {
|
|
|
142
195
|
files.push({
|
|
143
196
|
sourcePath: portablePath(sourceRelativePath),
|
|
144
197
|
outputPath: portablePath(outputRelativePath),
|
|
145
|
-
content
|
|
198
|
+
content,
|
|
199
|
+
append: entry.name.endsWith(".append")
|
|
146
200
|
});
|
|
147
201
|
}
|
|
148
202
|
}
|
|
@@ -155,15 +209,43 @@ async function writeMaterialisedFiles(destination, files) {
|
|
|
155
209
|
for (const file of files) {
|
|
156
210
|
const targetPath = join(destination, file.outputPath);
|
|
157
211
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
158
|
-
|
|
212
|
+
if (file.append) {
|
|
213
|
+
if (typeof file.content !== "string") {
|
|
214
|
+
throw new Error(`Append template entries must contain text: ${file.sourcePath}`);
|
|
215
|
+
}
|
|
216
|
+
const existing = await readFile(targetPath, "utf8");
|
|
217
|
+
await writeFile(targetPath, `${existing.trimEnd()}
|
|
218
|
+
${file.content.trimStart()}`);
|
|
219
|
+
} else {
|
|
220
|
+
await writeFile(targetPath, file.content);
|
|
221
|
+
}
|
|
159
222
|
}
|
|
160
223
|
}
|
|
161
|
-
async function scaffoldProject(templateDirectory, destination) {
|
|
224
|
+
async function scaffoldProject(templateDirectory, destination, options = {}) {
|
|
162
225
|
const resolvedDestination = resolve(destination);
|
|
226
|
+
const identity = createApplicationIdentity(resolvedDestination, options.displayName);
|
|
163
227
|
const destinationExisted = await assertDestinationAvailable(resolvedDestination);
|
|
164
228
|
await mkdir(resolvedDestination, { recursive: true });
|
|
165
229
|
try {
|
|
166
230
|
await writeMaterialisedFiles(resolvedDestination, await materialiseTemplateFiles(templateDirectory));
|
|
231
|
+
if (options.overlayDirectory) {
|
|
232
|
+
await writeMaterialisedFiles(resolvedDestination, await materialiseTemplateFiles(options.overlayDirectory));
|
|
233
|
+
}
|
|
234
|
+
await writeFile(join(resolvedDestination, "app.config.json"), `${JSON.stringify(identity, undefined, 2)}
|
|
235
|
+
`);
|
|
236
|
+
const manifestPath = join(resolvedDestination, "package.json");
|
|
237
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
238
|
+
manifest.name = identity.slug;
|
|
239
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, undefined, 2)}
|
|
240
|
+
`);
|
|
241
|
+
const lockPath = join(resolvedDestination, "bun.lock");
|
|
242
|
+
const lock = await readFile(lockPath, "utf8");
|
|
243
|
+
await writeFile(lockPath, lock.replace(/"name"\s*:\s*"stitchkit-starter"/, `"name": "${identity.slug}"`));
|
|
244
|
+
for (const environmentPath of [".env", ".env.example"]) {
|
|
245
|
+
const fullPath = join(resolvedDestination, environmentPath);
|
|
246
|
+
const environment = await readFile(fullPath, "utf8");
|
|
247
|
+
await writeFile(fullPath, environment.replaceAll("stitchkit_starter", identity.slug.replaceAll("-", "_")));
|
|
248
|
+
}
|
|
167
249
|
} catch (error) {
|
|
168
250
|
if (!destinationExisted) {
|
|
169
251
|
await rm(resolvedDestination, { recursive: true, force: true });
|
|
@@ -185,7 +267,11 @@ async function run(args) {
|
|
|
185
267
|
}
|
|
186
268
|
const destination = resolve2(options.destination);
|
|
187
269
|
const templateDirectory = resolve2(import.meta.dir, "../template");
|
|
188
|
-
|
|
270
|
+
const overlayDirectory = options.example ? resolve2(import.meta.dir, `../examples/${options.example}`) : undefined;
|
|
271
|
+
await scaffoldProject(templateDirectory, destination, {
|
|
272
|
+
...overlayDirectory && { overlayDirectory },
|
|
273
|
+
...options.displayName && { displayName: options.displayName }
|
|
274
|
+
});
|
|
189
275
|
if (options.install) {
|
|
190
276
|
const install = spawn(["bun", "install"], {
|
|
191
277
|
cwd: destination,
|
|
@@ -197,8 +283,9 @@ async function run(args) {
|
|
|
197
283
|
if (exitCode !== 0)
|
|
198
284
|
throw new Error(`bun install failed with exit code ${exitCode}`);
|
|
199
285
|
}
|
|
286
|
+
const mode = options.example ? ` with the ${options.example} example` : "";
|
|
200
287
|
process.stdout.write(`
|
|
201
|
-
Created
|
|
288
|
+
Created ${options.displayName ?? basename3(destination)}${mode}
|
|
202
289
|
|
|
203
290
|
`);
|
|
204
291
|
process.stdout.write(` cd ${options.destination}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test('renders and refreshes the repository example', async ({ page }) => {
|
|
4
|
+
await page.route('**/api/repository/refresh', async (route) => {
|
|
5
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
6
|
+
await route.continue();
|
|
7
|
+
});
|
|
8
|
+
await page.goto('/en');
|
|
9
|
+
await expect(page.getByText('max-listov/stitchkit')).toBeVisible();
|
|
10
|
+
|
|
11
|
+
const refresh = page.getByRole('button', { name: 'Refresh repository data' });
|
|
12
|
+
await expect(refresh).toHaveCSS('height', '32px');
|
|
13
|
+
await expect(refresh).toHaveCSS('width', '32px');
|
|
14
|
+
await expect(refresh.locator('.tabler-icon-refresh')).toHaveCount(1);
|
|
15
|
+
await refresh.click();
|
|
16
|
+
await expect(refresh).toHaveAttribute('aria-busy', 'true');
|
|
17
|
+
await expect(refresh.locator('svg')).toHaveCount(1);
|
|
18
|
+
await expect(refresh.locator('.tabler-icon-refresh')).toHaveClass(/animate-spin/);
|
|
19
|
+
});
|
package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.ts
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { env } from '@app/config';
|
|
2
|
+
import { appIdentity } from '@app/config/identity';
|
|
2
3
|
import { RepositoryVisibility } from '@app/db';
|
|
3
4
|
import type { RepositorySnapshot } from '@app/shared';
|
|
4
5
|
import { z } from 'zod';
|
|
@@ -80,7 +81,7 @@ const snapshotStore: RepositorySnapshotStore = {
|
|
|
80
81
|
function githubHeaders(): Headers {
|
|
81
82
|
const headers = new Headers({
|
|
82
83
|
Accept: 'application/vnd.github+json',
|
|
83
|
-
'User-Agent':
|
|
84
|
+
'User-Agent': appIdentity.slug,
|
|
84
85
|
'X-GitHub-Api-Version': '2026-03-10',
|
|
85
86
|
});
|
|
86
87
|
if (env.GITHUB_TOKEN) headers.set('Authorization', `Bearer ${env.GITHUB_TOKEN}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { env } from '@app/config';
|
|
2
|
+
import type { ClientToServerEvents, ServerToClientEvents } from '@app/shared';
|
|
3
|
+
import { createSocketIOServer } from 'stitchkit/server';
|
|
4
|
+
import { createRepositoryService } from './transport/repository-service';
|
|
5
|
+
|
|
6
|
+
export async function createSurface() {
|
|
7
|
+
const socket = await createSocketIOServer<ServerToClientEvents, ClientToServerEvents>({
|
|
8
|
+
cors: { origin: env.CORS_ORIGIN },
|
|
9
|
+
});
|
|
10
|
+
const repositoryService = createRepositoryService((snapshot) =>
|
|
11
|
+
socket.io.emit('repository:refreshed', snapshot),
|
|
12
|
+
);
|
|
13
|
+
return { socket, services: [repositoryService] };
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const featureServerSchema = {
|
|
4
|
+
GITHUB_REPOSITORY: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
|
|
5
|
+
GITHUB_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(900),
|
|
6
|
+
GITHUB_TOKEN: z.string().min(1).optional(),
|
|
7
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client"
|
|
3
|
+
output = "./src/generated"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
datasource db {
|
|
7
|
+
provider = "postgresql"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum RepositoryVisibility {
|
|
11
|
+
PUBLIC
|
|
12
|
+
PRIVATE
|
|
13
|
+
INTERNAL
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model RepositorySnapshot {
|
|
17
|
+
fullName String @id
|
|
18
|
+
description String?
|
|
19
|
+
htmlUrl String
|
|
20
|
+
language String?
|
|
21
|
+
visibility RepositoryVisibility @default(PUBLIC)
|
|
22
|
+
stars Int
|
|
23
|
+
forks Int
|
|
24
|
+
openIssues Int
|
|
25
|
+
commitCount Int
|
|
26
|
+
latestCommitSha String?
|
|
27
|
+
latestCommitMessage String?
|
|
28
|
+
latestCommittedAt DateTime?
|
|
29
|
+
fetchedAt DateTime
|
|
30
|
+
expiresAt DateTime
|
|
31
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { appIdentity } from '@app/config/identity';
|
|
2
|
+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
|
3
|
+
import type { Metadata } from 'next';
|
|
4
|
+
import { getTranslations } from 'next-intl/server';
|
|
5
|
+
import { LocaleSchema } from '@/i18n/locales';
|
|
6
|
+
import { createServerRepositoryApi } from '@/lib/api/client';
|
|
7
|
+
import { useRepository } from '@/lib/api/queries';
|
|
8
|
+
import { getQueryClient } from '@/lib/query-client';
|
|
9
|
+
import { createPageMetadata } from '@/lib/seo/metadata';
|
|
10
|
+
import { StarterPage } from './starter-page';
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
export async function generateMetadata({
|
|
15
|
+
params,
|
|
16
|
+
}: {
|
|
17
|
+
params: Promise<{ locale: string }>;
|
|
18
|
+
}): Promise<Metadata> {
|
|
19
|
+
const { locale } = await params;
|
|
20
|
+
return createPageMetadata('home', LocaleSchema.parse(locale));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
|
|
24
|
+
const { locale } = await params;
|
|
25
|
+
const appLocale = LocaleSchema.parse(locale);
|
|
26
|
+
const queryClient = getQueryClient();
|
|
27
|
+
const api = createServerRepositoryApi();
|
|
28
|
+
const t = await getTranslations('App');
|
|
29
|
+
await queryClient.prefetchQuery({
|
|
30
|
+
queryKey: useRepository.getKey(),
|
|
31
|
+
queryFn: () => api.read(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
36
|
+
<StarterPage
|
|
37
|
+
applicationName={appIdentity.name}
|
|
38
|
+
applicationDescription={appIdentity.description[appLocale]}
|
|
39
|
+
heroTitle={t('heroTitle')}
|
|
40
|
+
catalogueLabel={t('ui')}
|
|
41
|
+
locale={appLocale}
|
|
42
|
+
/>
|
|
43
|
+
</HydrationBoundary>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { IconArrowUpRight } from '@tabler/icons-react';
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import { BrandMark } from '@/components/brand-mark';
|
|
4
|
+
import { RepositorySummary } from '@/components/repository-summary';
|
|
5
|
+
import { LanguageSwitcher, ThemeToggle } from '@/components/system-controls';
|
|
6
|
+
import { buttonVariants } from '@/components/ui';
|
|
7
|
+
import type { AppLocale } from '@/i18n/locales';
|
|
8
|
+
import { Link } from '@/i18n/navigation';
|
|
9
|
+
import { absoluteSiteUrl } from '@/lib/seo/metadata';
|
|
10
|
+
import { getSeoPage, SITE_NAME } from '@/lib/seo/pages';
|
|
11
|
+
import { cn } from '@/lib/utils/cn';
|
|
12
|
+
|
|
13
|
+
interface StarterPageProps {
|
|
14
|
+
applicationName: string;
|
|
15
|
+
applicationDescription: string;
|
|
16
|
+
heroTitle: string;
|
|
17
|
+
catalogueLabel: string;
|
|
18
|
+
locale: AppLocale;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const architecture = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Frontend',
|
|
24
|
+
detail: 'Web',
|
|
25
|
+
technologies: [
|
|
26
|
+
{ name: 'Next.js', icon: '/icons/next.svg' },
|
|
27
|
+
{ name: 'React', icon: '/icons/react.svg' },
|
|
28
|
+
{ name: 'Tailwind CSS', icon: '/icons/tailwind.svg' },
|
|
29
|
+
{ name: 'React Query', icon: '/icons/react-query.svg' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Backend',
|
|
34
|
+
detail: 'API',
|
|
35
|
+
technologies: [
|
|
36
|
+
{ name: 'Stitchkit', icon: '/mascot-stitch.png' },
|
|
37
|
+
{ name: 'Bun', icon: '/icons/bun.svg' },
|
|
38
|
+
{ name: 'Zod', icon: '/icons/zod.svg' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Data',
|
|
43
|
+
detail: 'Storage',
|
|
44
|
+
technologies: [
|
|
45
|
+
{ name: 'PostgreSQL', icon: '/icons/postgresql.svg' },
|
|
46
|
+
{ name: 'Prisma', icon: '/icons/prisma.svg', invertInDark: true },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Tooling',
|
|
51
|
+
detail: 'Quality',
|
|
52
|
+
technologies: [
|
|
53
|
+
{ name: 'TypeScript', icon: '/icons/typescript.svg' },
|
|
54
|
+
{ name: 'TSConfig', icon: '/icons/tsconfig.svg' },
|
|
55
|
+
{ name: 'Biome', icon: '/icons/biome.svg' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function StarterPage({
|
|
61
|
+
applicationName,
|
|
62
|
+
applicationDescription,
|
|
63
|
+
heroTitle,
|
|
64
|
+
catalogueLabel,
|
|
65
|
+
locale,
|
|
66
|
+
}: StarterPageProps) {
|
|
67
|
+
const homeSeo = getSeoPage('home', locale);
|
|
68
|
+
const structuredData = {
|
|
69
|
+
'@context': 'https://schema.org',
|
|
70
|
+
'@type': 'SoftwareApplication',
|
|
71
|
+
name: SITE_NAME,
|
|
72
|
+
applicationCategory: 'DeveloperApplication',
|
|
73
|
+
operatingSystem: 'Web',
|
|
74
|
+
url: absoluteSiteUrl(`/${locale}`),
|
|
75
|
+
description: homeSeo.description,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<main className='h-svh overflow-y-auto bg-background lg:overflow-hidden'>
|
|
80
|
+
<script type='application/ld+json'>{JSON.stringify(structuredData)}</script>
|
|
81
|
+
<div className='mx-auto flex min-h-full w-full max-w-7xl flex-col px-5 lg:h-full lg:px-8'>
|
|
82
|
+
<header className='flex h-16 shrink-0 items-center justify-between'>
|
|
83
|
+
<div className='flex min-w-0 items-center gap-2 font-medium'>
|
|
84
|
+
<BrandMark priority />
|
|
85
|
+
<span className='truncate'>{applicationName}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className='flex items-center gap-2'>
|
|
88
|
+
<LanguageSwitcher />
|
|
89
|
+
<ThemeToggle />
|
|
90
|
+
<Link
|
|
91
|
+
aria-label={catalogueLabel}
|
|
92
|
+
className={cn(buttonVariants({ variant: 'outline', size: 'sm' }))}
|
|
93
|
+
href='/ui'
|
|
94
|
+
>
|
|
95
|
+
<span className='hidden sm:inline'>{catalogueLabel}</span>
|
|
96
|
+
<IconArrowUpRight />
|
|
97
|
+
</Link>
|
|
98
|
+
</div>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
<div className='flex flex-1 flex-col items-center justify-center py-8 text-center lg:min-h-0 lg:py-5'>
|
|
102
|
+
<section className='flex w-full min-w-0 flex-col items-center'>
|
|
103
|
+
<h1 className='max-w-4xl font-display text-5xl font-medium tracking-tight sm:text-6xl lg:text-7xl lg:leading-[0.96]'>
|
|
104
|
+
{heroTitle}
|
|
105
|
+
</h1>
|
|
106
|
+
<p className='mt-5 max-w-3xl text-base leading-7 text-muted-foreground lg:text-lg'>
|
|
107
|
+
{applicationDescription}
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<div className='mt-8 w-full max-w-4xl'>
|
|
111
|
+
<div className='grid grid-cols-2 overflow-hidden rounded-xl border border-border bg-card text-left sm:grid-cols-4'>
|
|
112
|
+
{architecture.map((item) => (
|
|
113
|
+
<div
|
|
114
|
+
className='min-w-0 border-border px-3 py-3 odd:border-r first:border-b second:border-b sm:border-r sm:border-b-0 sm:last:border-r-0 sm:px-4'
|
|
115
|
+
key={item.name}
|
|
116
|
+
>
|
|
117
|
+
<div className='flex min-w-0 items-baseline gap-2'>
|
|
118
|
+
<p className='truncate text-sm font-medium'>{item.name}</p>
|
|
119
|
+
<p className='text-xs text-muted-foreground'>{item.detail}</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div className='mt-2 flex min-h-6 items-center gap-2'>
|
|
122
|
+
{item.technologies.map((technology) => (
|
|
123
|
+
<Image
|
|
124
|
+
alt={technology.name}
|
|
125
|
+
className={
|
|
126
|
+
technology.invertInDark
|
|
127
|
+
? 'technology-icon-invert-dark size-5 shrink-0 object-contain'
|
|
128
|
+
: 'size-5 shrink-0 object-contain'
|
|
129
|
+
}
|
|
130
|
+
height={20}
|
|
131
|
+
key={technology.icon}
|
|
132
|
+
src={technology.icon}
|
|
133
|
+
title={technology.name}
|
|
134
|
+
width={20}
|
|
135
|
+
/>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className='mt-5 w-full'>
|
|
144
|
+
<RepositorySummary />
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</main>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { Toaster } from '@/components/ui/toaster';
|
|
6
|
+
import { ReactQueryProvider } from './react-query';
|
|
7
|
+
import { RealtimeProvider } from './realtime';
|
|
8
|
+
|
|
9
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<ReactQueryProvider>
|
|
12
|
+
<RealtimeProvider>
|
|
13
|
+
<TooltipProvider delayDuration={250}>{children}</TooltipProvider>
|
|
14
|
+
</RealtimeProvider>
|
|
15
|
+
<Toaster />
|
|
16
|
+
</ReactQueryProvider>
|
|
17
|
+
);
|
|
18
|
+
}
|