@spfn/cli 0.0.8 → 0.1.0-alpha.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.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Example Routes: CRUD Operations
3
+ *
4
+ * File naming patterns:
5
+ * - routes/index/index.ts -> / (root route)
6
+ * - routes/users/index.ts -> /users
7
+ * - routes/users/[id]/index.ts -> /users/:id (dynamic parameter)
8
+ *
9
+ * Contracts are co-located in the same directory
10
+ */
11
+
12
+ import { createApp } from '@spfn/core/route';
13
+ import {
14
+ getExamplesContract,
15
+ getExampleContract,
16
+ createExampleContract,
17
+ updateExampleContract,
18
+ deleteExampleContract
19
+ } from './contract.js';
20
+
21
+ const app = createApp();
22
+
23
+ // GET /examples - List examples with pagination
24
+ app.bind(getExamplesContract, async (c) => {
25
+ const { limit = 10, offset = 0 } = c.query;
26
+
27
+ // Mock data - replace with actual database queries
28
+ const examples = [
29
+ {
30
+ id: '1',
31
+ name: 'File-based Routing',
32
+ description: 'Next.js style automatic route registration'
33
+ },
34
+ {
35
+ id: '2',
36
+ name: 'Contract-based Validation',
37
+ description: 'TypeBox schemas for end-to-end type safety'
38
+ },
39
+ {
40
+ id: '3',
41
+ name: 'Auto-generated Types',
42
+ description: 'Client code generation from contracts'
43
+ }
44
+ ];
45
+
46
+ return c.json({
47
+ examples: examples.slice(offset, offset + limit),
48
+ total: examples.length,
49
+ limit,
50
+ offset
51
+ });
52
+ });
53
+
54
+ // GET /examples/:id - Get single example
55
+ app.bind(getExampleContract, async (c) => {
56
+ const { id } = c.params;
57
+
58
+ // Mock data - replace with actual database query
59
+ const example = {
60
+ id,
61
+ name: 'Example ' + id,
62
+ description: 'This is an example',
63
+ createdAt: Date.now(),
64
+ updatedAt: Date.now()
65
+ };
66
+
67
+ return c.json(example);
68
+ });
69
+
70
+ // POST /examples - Create example
71
+ app.bind(createExampleContract, async (c) => {
72
+ const body = await c.data();
73
+
74
+ // Mock data - replace with actual database insert
75
+ const example = {
76
+ id: Math.random().toString(36).substring(7),
77
+ name: body.name,
78
+ description: body.description,
79
+ createdAt: Date.now()
80
+ };
81
+
82
+ return c.json(example);
83
+ });
84
+
85
+ // PUT /examples/:id - Update example
86
+ app.bind(updateExampleContract, async (c) => {
87
+ const { id } = c.params;
88
+ const body = await c.data();
89
+
90
+ // Mock data - replace with actual database update
91
+ const example = {
92
+ id,
93
+ name: body.name || 'Updated Example',
94
+ description: body.description || 'Updated description',
95
+ updatedAt: Date.now()
96
+ };
97
+
98
+ return c.json(example);
99
+ });
100
+
101
+ // DELETE /examples/:id - Delete example
102
+ app.bind(deleteExampleContract, async (c) => {
103
+ const { id } = c.params;
104
+
105
+ // Mock data - replace with actual database delete
106
+ return c.json({
107
+ success: true,
108
+ id
109
+ });
110
+ });
111
+
112
+ export default app;
@@ -0,0 +1,13 @@
1
+ import { Type } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Health Check Contract
5
+ */
6
+ export const healthContract = {
7
+ method: 'GET' as const,
8
+ path: '/',
9
+ response: Type.Object({
10
+ status: Type.String(),
11
+ timestamp: Type.Number()
12
+ })
13
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Health Check Route
3
+ *
4
+ * Minimal endpoint for monitoring systems, load balancers, and orchestrators.
5
+ * Used by Kubernetes probes, uptime monitors, etc.
6
+ *
7
+ * Example: Using createApp() with separate contracts
8
+ */
9
+
10
+ import { createApp } from '@spfn/core/route';
11
+ import { healthContract } from './contract.js';
12
+
13
+ const app = createApp();
14
+
15
+ app.bind(healthContract, async (c) => {
16
+ return c.json({
17
+ status: 'ok',
18
+ timestamp: Date.now(),
19
+ uptime: process.uptime()
20
+ });
21
+ });
22
+
23
+ export default app;
@@ -0,0 +1,13 @@
1
+ import { Type } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Root Contract
5
+ */
6
+ export const rootContract = {
7
+ method: 'GET' as const,
8
+ path: '/',
9
+ response: Type.Object({
10
+ message: Type.String(),
11
+ version: Type.String()
12
+ })
13
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Root Route: GET /
3
+ *
4
+ * Welcome page for SPFN API
5
+ */
6
+
7
+ import { createApp } from '@spfn/core/route';
8
+ import { rootContract } from './contract.js';
9
+
10
+ const app = createApp();
11
+
12
+ app.bind(rootContract, async (c) =>
13
+ {
14
+ return c.json(
15
+ {
16
+ name: 'SPFN API',
17
+ version: '1.0.0',
18
+ status: 'running',
19
+ endpoints:
20
+ {
21
+ health: '/health',
22
+ examples: '/examples',
23
+ },
24
+ message: 'Welcome to SPFN! Visit /examples for usage examples.',
25
+ });
26
+ });
27
+
28
+ export default app;
package/package.json CHANGED
@@ -1,49 +1,69 @@
1
1
  {
2
- "name": "@spfn/cli",
3
- "version": "0.0.8",
4
- "description": "Superfunction CLI",
5
- "scripts": {
6
- "start": "node lib/index.js",
7
- "start:dev": "NODE_ENV=development node lib/index.js",
8
- "start:prod": "NODE_ENV=production node lib/index.js"
9
- },
10
- "keywords": [
11
- "cli",
12
- "superfunction",
13
- "sf",
14
- "devtools",
15
- "automation"
16
- ],
17
- "homepage": "https://console.superfunctions.ai",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://git.superfunctions.ai/spfn/sf-cli.git"
21
- },
22
- "license": "MIT",
23
- "author": "Superfunction Ray <rayim@superfunctions.ai>",
24
- "type": "module",
25
- "main": "lib/index.js",
26
- "bin": {
27
- "sf": "lib/index.js"
28
- },
29
- "directories": {
30
- "lib": "lib"
31
- },
32
- "files": [
33
- "lib/**/*"
34
- ],
35
- "dependencies": {
36
- "commander": "^13.1.0",
37
- "open": "^10.1.1"
38
- },
39
- "devDependencies": {
40
- "@types/node": "^22.15.3",
41
- "typescript": "^5.0.0"
42
- },
43
- "engines": {
44
- "node": ">=18"
45
- },
46
- "publishConfig": {
47
- "access": "public"
2
+ "name": "@spfn/cli",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "SPFN CLI - Add SPFN to your Next.js project",
5
+ "type": "module",
6
+ "bin": {
7
+ "spfn": "./bin/spfn.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin"
12
+ ],
13
+ "keywords": [
14
+ "spfn",
15
+ "nextjs",
16
+ "hono",
17
+ "cli",
18
+ "backend"
19
+ ],
20
+ "author": "Ray Im <rayim@inflike.com>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/spfn/spfn.git",
25
+ "directory": "packages/cli"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/spfn/spfn/issues"
29
+ },
30
+ "homepage": "https://github.com/spfn/spfn/tree/main/packages/cli#readme",
31
+ "engines": {
32
+ "node": ">=18.18.0"
33
+ },
34
+ "peerDependencies": {
35
+ "typescript": "^5.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "typescript": {
39
+ "optional": true
48
40
  }
49
- }
41
+ },
42
+ "dependencies": {
43
+ "chalk": "^5.3.0",
44
+ "commander": "^11.1.0",
45
+ "concurrently": "^9.0.0",
46
+ "execa": "^8.0.1",
47
+ "fs-extra": "^11.2.0",
48
+ "ora": "^7.0.1",
49
+ "prompts": "^2.4.2",
50
+ "tsx": "^4.20.6",
51
+ "@spfn/core": "0.1.0-alpha.1"
52
+ },
53
+ "devDependencies": {
54
+ "@types/fs-extra": "^11.0.4",
55
+ "@types/node": "^20.11.0",
56
+ "@types/prompts": "^2.4.9",
57
+ "tsup": "^8.0.0",
58
+ "typescript": "^5.3.3"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "tag": "alpha"
63
+ },
64
+ "scripts": {
65
+ "build": "tsup && node scripts/copy-templates.js",
66
+ "dev": "tsup --watch",
67
+ "type-check": "tsc --noEmit"
68
+ }
69
+ }
package/lib/index.js DELETED
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { login } from "./login.js";
4
-
5
- const program = new Command();
6
-
7
- program
8
- .name("sf")
9
- .description("Superfunction CLI")
10
- .version("0.1.0");
11
-
12
- program
13
- .command("login")
14
- .description("Authenticate via browser login")
15
- .action(() => {
16
- (async () => { await login(); })();
17
- });
18
-
19
- program.parse();
package/lib/login.js DELETED
@@ -1,167 +0,0 @@
1
- import http from "http";
2
- import open from "open";
3
- import { execSync } from "child_process";
4
-
5
- const isDev = process.env.NODE_ENV === "development" || process.env.SF_ENV === "local";
6
- const GIT_HOST = isDev ? "gitea:13000" : "git.superfunctions.ai";
7
- const GIT_PROTOCOL = isDev ? "http" : "https";
8
-
9
- /**
10
- * Git credential helper를 이용해 특정 레포에 대한 인증 정보를 저장합니다.
11
- * @param username Git 사용자명
12
- * @param token Git 토큰
13
- * @param repoPath 호스트 이후의 경로 예: "my-org/my-repo"
14
- */
15
- function installGitCredentials(
16
- username,
17
- token,
18
- {
19
- protocol = "https",
20
- host,
21
- path // optional
22
- }
23
- )
24
- {
25
- // credential helper 설정 (글로벌, 1회면 충분)
26
- if (process.platform === "win32")
27
- {
28
- execSync("git config --global credential.helper manager-core");
29
- }
30
- else
31
- {
32
- execSync("git config --global credential.helper store");
33
- }
34
-
35
- // git credential approve 입력 생성
36
- const cred = [
37
- `protocol=${protocol}`,
38
- `host=${host}`,
39
- path ? `path=${path}` : null,
40
- `username=${username}`,
41
- `password=${token}`,
42
- ""
43
- ].filter(Boolean).join("\n");
44
-
45
- execSync("git credential approve", {
46
- input: cred,
47
- stdio: ["pipe", "ignore", "inherit"]
48
- });
49
-
50
- if (path)
51
- {
52
- console.log(`✔️ Stored credentials for ${protocol}://${host}/${path}`);
53
- }
54
- else
55
- {
56
- console.log(`✔️ Stored credentials for ${protocol}://${host}`);
57
- }
58
- }
59
-
60
- export async function login()
61
- {
62
- try
63
- {
64
- const server = http.createServer((req, res) =>
65
- {
66
- res.setHeader("Access-Control-Allow-Origin", "*");
67
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
68
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
69
-
70
- if (req.method === "OPTIONS")
71
- {
72
- res.writeHead(200);
73
- res.end();
74
- return;
75
- }
76
-
77
- if (req.url === "/callback" && req.method === "POST")
78
- {
79
- let body = "";
80
- req.on("data", (chunk) => (body += chunk));
81
- req.on("end", () =>
82
- {
83
- try
84
- {
85
- const { username, token } = JSON.parse(body);
86
- if (!username || !token)
87
- {
88
- res.writeHead(400).end("Missing username or token");
89
- return;
90
- }
91
-
92
- // npm registry 설정 (생략)
93
- try
94
- {
95
- execSync(
96
- `npm config set @sf:registry ${GIT_PROTOCOL}://${GIT_HOST}/api/packages/spfn/npm/`,
97
- { stdio: "inherit", env: process.env }
98
- );
99
- execSync(
100
- `npm config set //${GIT_HOST}/api/packages/spfn/npm/:_authToken \"${token}\"`,
101
- { stdio: "inherit", env: process.env }
102
- );
103
- console.log("✔️ npm registry and authToken set via npm config");
104
- }
105
- catch (e)
106
- {
107
- console.error("❌ npm config set failed:", e);
108
- }
109
-
110
- // 2개 레포에 대해 각각 자격증명 저장
111
- installGitCredentials(username, token, {
112
- protocol: GIT_PROTOCOL,
113
- host: GIT_HOST
114
- });
115
-
116
- res.writeHead(200, { "Content-Type": "text/plain" });
117
- res.end("✅ Login successful! You can now close this window.");
118
-
119
- server.close();
120
- }
121
- catch (err)
122
- {
123
- console.error(err);
124
- res.writeHead(500).end("Error during login callback");
125
- server.close();
126
- }
127
- });
128
- }
129
- else
130
- {
131
- res.writeHead(404).end("Not Found");
132
- }
133
- });
134
-
135
- const port = await getAvailablePort();
136
-
137
- // 로그인 UI 주소도 분기
138
- const isDev = process.env.NODE_ENV === "development" || process.env.SF_ENV === "local";
139
- const LOGIN_UI_URL = isDev
140
- ? `http://localhost:3000/cli-login?port=${port}`
141
- : `https://console.superfunctions.ai/cli-login?port=${port}`;
142
-
143
- server.listen(port, () =>
144
- {
145
- console.log("🌐 Opening browser for login...");
146
- open(LOGIN_UI_URL);
147
- });
148
- }
149
- catch (err)
150
- {
151
- console.error(err);
152
- }
153
- }
154
-
155
- function getAvailablePort(defaultPort = 5678) {
156
- return new Promise((resolve) => {
157
- const srv = http.createServer();
158
- srv.listen(defaultPort, () => {
159
- srv.close(() => resolve(defaultPort));
160
- });
161
- srv.on("error", () => {
162
- // 사용 중이면 10000~20000 사이 랜덤 포트
163
- const randomPort = Math.floor(Math.random() * 10000) + 10000;
164
- resolve(randomPort);
165
- });
166
- });
167
- }