@stratify/cli 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Jean Michelet <jean.antoine.michelet@gmail.com>
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/README.md ADDED
@@ -0,0 +1,31 @@
1
+
2
+ # Stratify CLI
3
+
4
+ Command-line tool for [Stratify](https://www.npmjs.com/package/@stratify/core), a modular Fastify framework with DI and TypeScript support.
5
+ It scaffolds a ready-to-use Stratify project with TypeScript, ESLint, and Prettier.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g @stratify/cli
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ npx stratify-cli new <app-name>
17
+ ```
18
+
19
+ Creates a new project with:
20
+
21
+ * TypeScript + ESM
22
+ * ESLint and Prettier setup
23
+ * Example module under `/src/tasks` (controllers, providers, hooks, installers, adapters)
24
+
25
+ Then:
26
+
27
+ ```bash
28
+ cd <app-name>
29
+ npm install
30
+ npm run dev
31
+ ```
package/dist/bin.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { runNew } from "./new.js";
4
+ async function main() {
5
+ const [cmd, name] = process.argv.slice(2);
6
+ if (!cmd || cmd !== "new") {
7
+ console.error(`Usage:
8
+ npx stratify-cli new <app-name>
9
+
10
+ Commands:
11
+ new Scaffold a new Stratify app
12
+ `);
13
+ process.exit(1);
14
+ }
15
+ if (!name) {
16
+ console.error("Please provide an app name, e.g. npx stratify-cli new my-app");
17
+ process.exit(1);
18
+ }
19
+ const templateDir = resolve(new URL("../template", import.meta.url).pathname);
20
+ await runNew({ appName: name, templateDir });
21
+ }
22
+ main().catch((err) => {
23
+ console.error(err?.stack || String(err));
24
+ process.exit(1);
25
+ });
package/dist/new.js ADDED
@@ -0,0 +1,48 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ async function pathExists(p) {
4
+ try {
5
+ await fs.access(p);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function copyDir(src, dest) {
13
+ await fs.mkdir(dest, { recursive: true });
14
+ const entries = await fs.readdir(src, { withFileTypes: true });
15
+ for (const e of entries) {
16
+ const s = join(src, e.name);
17
+ const d = join(dest, e.name);
18
+ if (e.isDirectory()) {
19
+ await copyDir(s, d);
20
+ }
21
+ else if (e.isFile()) {
22
+ await fs.copyFile(s, d);
23
+ }
24
+ }
25
+ }
26
+ async function replaceFileContent(p, replacer) {
27
+ const s = await fs.readFile(p, "utf8");
28
+ await fs.writeFile(p, replacer(s), "utf8");
29
+ }
30
+ export async function runNew({ appName, templateDir }) {
31
+ const target = resolve(process.cwd(), appName);
32
+ if (await pathExists(target)) {
33
+ throw new Error(`Directory "${appName}" already exists.`);
34
+ }
35
+ await copyDir(templateDir, target);
36
+ const pkgPath = join(target, "package.json");
37
+ await replaceFileContent(pkgPath, (s) => s.replace(/"name":\s*".+?"/, `"name": "${appName}"`));
38
+ console.log(`\nScaffolded "${appName}" successfully.
39
+
40
+ Next steps:
41
+
42
+ cd ${appName}
43
+ npm install
44
+ npm run dev
45
+
46
+ Happy hacking with Stratify!
47
+ `);
48
+ }
@@ -0,0 +1,54 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { promises as fs } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { mkdtemp, rm } from "node:fs/promises";
7
+ import { runNew } from "../new";
8
+ const TEMPLATE_FIXTURE = resolve("template");
9
+ async function pathExists(p) {
10
+ try {
11
+ await fs.access(p);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ describe("runNew()", () => {
19
+ test("copies template and replaces package name", async () => {
20
+ const tmpRoot = await mkdtemp(join(tmpdir(), "stratify-cli-"));
21
+ const appName = "my-app";
22
+ const target = join(tmpRoot, appName);
23
+ const prevCwd = process.cwd();
24
+ process.chdir(tmpRoot);
25
+ try {
26
+ await runNew({ appName, templateDir: TEMPLATE_FIXTURE });
27
+ const pkgPath = join(target, "package.json");
28
+ assert.ok(await pathExists(pkgPath), "package.json should exist");
29
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
30
+ assert.equal(pkg.name, appName);
31
+ const srcDir = join(target, "src");
32
+ assert.ok(await pathExists(srcDir), "src/ should exist");
33
+ }
34
+ finally {
35
+ process.chdir(prevCwd);
36
+ await rm(tmpRoot, { recursive: true, force: true });
37
+ }
38
+ });
39
+ test("throws if target directory already exists", async () => {
40
+ const tmpRoot = await mkdtemp(join(tmpdir(), "stratify-cli-"));
41
+ const appName = "existing-app";
42
+ const target = join(tmpRoot, appName);
43
+ await fs.mkdir(target, { recursive: true });
44
+ const prevCwd = process.cwd();
45
+ process.chdir(tmpRoot);
46
+ try {
47
+ await assert.rejects(() => runNew({ appName, templateDir: TEMPLATE_FIXTURE }), /already exists/);
48
+ }
49
+ finally {
50
+ process.chdir(prevCwd);
51
+ await rm(tmpRoot, { recursive: true, force: true });
52
+ }
53
+ });
54
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@stratify/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "stratify-cli": "./dist/bin.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "template"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/bin.ts",
18
+ "test": "tsx --test src/**/*.test.ts",
19
+ "prepare": "npm run build",
20
+ "prepublishOnly": "npm run build && npm run test"
21
+ },
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@types/node": "^24.7.2",
25
+ "tsx": "^4.16.2",
26
+ "typescript": "^5.6.3"
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "trailingComma": "all"
5
+ }
@@ -0,0 +1,20 @@
1
+ # Stratify App (Tasks)
2
+
3
+ Generated with `@stratify/cli`.
4
+
5
+ ## Scripts
6
+ - `npm run dev` — run with tsx
7
+ - `npm run build` — compile TypeScript to `dist/`
8
+ - `npm start` — run compiled app
9
+ - `npm test` — run integration tests with tsx
10
+
11
+ ## Structure
12
+ - `src/main.ts` — app entrypoint
13
+ - `src/tasks` — domain components
14
+ - `tasks.module.ts` — module composition
15
+ - `providers/tasks-repository.provider.ts` — fake in-memory repository
16
+ - `controllers/tasks.controller.ts` — routes (GET/POST)
17
+ - `hooks/http.hooks.ts` — trivial HTTP hook
18
+ - `installers/cors.installer.ts` — enables CORS
19
+ - `adapters/version.adapter.ts` — exposes fastify.version
20
+ - `test/tasks.integration.test.ts` — integration tests for tasks endpoints
@@ -0,0 +1,15 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import pluginImport from "eslint-plugin-import";
4
+
5
+ export default [
6
+ { ignores: ["dist", "node_modules"] },
7
+ js.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ {
10
+ plugins: { import: pluginImport },
11
+ rules: {
12
+ "import/no-unresolved": "off"
13
+ }
14
+ }
15
+ ];
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "APP_NAME_WILL_BE_REPLACED",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/main.ts",
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/main.js",
10
+ "lint": "eslint .",
11
+ "test": "tsx --test"
12
+ },
13
+ "dependencies": {
14
+ "@fastify/cors": "^10.0.0",
15
+ "@sinclair/typebox": "^0.32.30",
16
+ "@stratify/core": "^0.1.1",
17
+ "close-with-grace": "2.3.0"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.13.0",
21
+ "eslint": "^9.13.0",
22
+ "eslint-plugin-import": "^2.31.0",
23
+ "prettier": "^3.3.3",
24
+ "tsx": "^4.16.2",
25
+ "typescript": "^5.6.3",
26
+ "typescript-eslint": "^8.8.1",
27
+ "@types/node": "24.7.2"
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import { createApp } from "@stratify/core";
2
+ import closeWithGrace from "close-with-grace";
3
+
4
+ import { tasksModule } from "./tasks/tasks.module.js";
5
+
6
+ const app = await createApp({
7
+ root: tasksModule,
8
+ serverOptions: {
9
+ logger: true,
10
+ },
11
+ });
12
+
13
+ closeWithGrace({ delay: 500 }, async ({ err }) => {
14
+ if (err != null) {
15
+ app.log.error(err);
16
+ }
17
+
18
+ await app.close();
19
+ });
20
+
21
+ try {
22
+ await app.listen({ port: 3000 });
23
+ } catch (err) {
24
+ app.log.error(err);
25
+ process.exit(1);
26
+ }
@@ -0,0 +1,11 @@
1
+ import { createAdapter } from "@stratify/core";
2
+
3
+ /**
4
+ * Exposes Fastify version for the current instance context.
5
+ */
6
+ export const versionAdapter = createAdapter({
7
+ name: "version",
8
+ expose: ({ fastify }) => ({
9
+ version: fastify.version
10
+ })
11
+ });
@@ -0,0 +1,50 @@
1
+ import { createController } from "@stratify/core";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { tasksRepository } from "../providers/tasks-repository.provider.js";
4
+
5
+ export const tasksController = createController({
6
+ name: "tasks", // optional, used by tree printer
7
+ deps: { tasksRepository },
8
+ build: async ({ builder, deps }) => {
9
+ // GET /tasks
10
+ builder.addRoute({
11
+ method: "GET",
12
+ url: "/tasks",
13
+ handler: async () => {
14
+ return { tasks: await deps.tasksRepository.list() };
15
+ }
16
+ });
17
+
18
+ // GET /tasks/:id
19
+ builder.addRoute({
20
+ method: "GET",
21
+ url: "/tasks/:id",
22
+ schema: {
23
+ params: Type.Object({ id: Type.String() })
24
+ },
25
+ handler: async (req, reply) => {
26
+ const task = await deps.tasksRepository.get(req.params.id);
27
+ if (!task) {
28
+ return reply.code(404).send({ error: "Not Found" });
29
+ }
30
+ return task;
31
+ }
32
+ });
33
+
34
+ // POST /tasks
35
+ builder.addRoute({
36
+ method: "POST",
37
+ url: "/tasks",
38
+ schema: {
39
+ body: Type.Object({
40
+ title: Type.String({ minLength: 1 })
41
+ })
42
+ },
43
+ handler: async (req) => {
44
+ const { title } = req.body;
45
+ const created = await deps.tasksRepository.create(title);
46
+ return created;
47
+ }
48
+ });
49
+ }
50
+ });
@@ -0,0 +1,13 @@
1
+ import { createHooks } from "@stratify/core";
2
+
3
+ export const httpHooks = createHooks({
4
+ type: "http",
5
+ name: "http-core", // optional, used by tree printer
6
+ build: async ({ builder }) => {
7
+ // Example: add a simple security header for all responses
8
+ builder.addHook("onRequest", async (_req, reply) => {
9
+ reply.header("x-content-type-options", "nosniff");
10
+ });
11
+ // You can add more lifecycle hooks as needed.
12
+ }
13
+ });
@@ -0,0 +1,15 @@
1
+ import { createInstaller } from "@stratify/core";
2
+ import cors from "@fastify/cors";
3
+
4
+ /**
5
+ * @see https://github.com/fastify/fastify-cors
6
+ */
7
+ export const corsInstaller = createInstaller({
8
+ name: "cors", // optional, used by tree printer
9
+ install: async ({ fastify }) => {
10
+ await fastify.register(cors, {
11
+ origin: true, // allow all origins for now
12
+ methods: ["GET", "POST", "PUT", "DELETE"]
13
+ });
14
+ }
15
+ });
@@ -0,0 +1,32 @@
1
+ import { createProvider } from "@stratify/core";
2
+
3
+ export interface Task {
4
+ id: string;
5
+ title: string;
6
+ done: boolean;
7
+ };
8
+
9
+ function makeId() {
10
+ return Math.random().toString(36).slice(2, 9);
11
+ }
12
+
13
+ export const tasksRepository = createProvider({
14
+ name: "tasksRepository",
15
+ expose: async () => {
16
+ // Fake in-memory store
17
+ const store: Task[] = [
18
+ { id: "t1", title: "Read Stratify docs", done: false }
19
+ ];
20
+
21
+ return {
22
+ list: async (): Promise<Task[]> => store,
23
+ get: async (id: string): Promise<Task | undefined> =>
24
+ store.find((t) => t.id === id),
25
+ create: async (title: string): Promise<Task> => {
26
+ const task = { id: makeId(), title, done: false };
27
+ store.push(task);
28
+ return task;
29
+ }
30
+ };
31
+ }
32
+ });
@@ -0,0 +1,13 @@
1
+ import { createModule } from "@stratify/core";
2
+ import { tasksController } from "./controllers/tasks.controller.js";
3
+ import { httpHooks } from "./hooks/http.hooks.js";
4
+ import { corsInstaller } from "./installers/cors.installer.js";
5
+
6
+ export const tasksModule = createModule({
7
+ name: "tasks",
8
+ encapsulate: true,
9
+ controllers: [tasksController],
10
+ hooks: [httpHooks],
11
+ installers: [corsInstaller],
12
+ subModules: []
13
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createApp } from "@stratify/core";
4
+ import { tasksModule } from "../src/tasks/tasks.module.ts";
5
+
6
+ describe("tasks routes", () => {
7
+ test("GET /tasks returns initial list", async () => {
8
+ const app = await createApp({ root: tasksModule });
9
+ const res = await app.inject({ method: "GET", url: "/tasks" });
10
+ assert.strictEqual(res.statusCode, 200);
11
+ const json = res.json();
12
+ assert.ok(Array.isArray(json.tasks));
13
+ assert.ok(json.tasks.length >= 1);
14
+ await app.close();
15
+ });
16
+
17
+ test("POST /tasks creates a task, then GET /tasks/:id retrieves it", async () => {
18
+ const app = await createApp({ root: tasksModule });
19
+
20
+ const createRes = await app.inject({
21
+ method: "POST",
22
+ url: "/tasks",
23
+ payload: { title: "Write tests" }
24
+ });
25
+ assert.strictEqual(createRes.statusCode, 200);
26
+ const created = createRes.json();
27
+ assert.ok(created.id);
28
+ assert.strictEqual(created.title, "Write tests");
29
+ assert.strictEqual(created.done, false);
30
+
31
+ const getRes = await app.inject({
32
+ method: "GET",
33
+ url: `/tasks/${created.id}`
34
+ });
35
+ assert.strictEqual(getRes.statusCode, 200);
36
+ const fetched = getRes.json();
37
+ assert.deepStrictEqual(fetched, created);
38
+
39
+ await app.close();
40
+ });
41
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "declaration": false,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }