@spfn/cli 0.0.9 → 0.1.0-alpha.2

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.9",
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.2",
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.2"
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,206 +0,0 @@
1
- import http from "http";
2
- import open from "open";
3
- import fs from "fs";
4
- import os from "os";
5
- import path from "path";
6
- import { execSync } from "child_process";
7
-
8
- const isDev = process.env.NODE_ENV === "development" || process.env.SF_ENV === "local";
9
- const GIT_HOST = isDev ? "gitea:13000" : "git.superfunctions.ai";
10
- const GIT_PROTOCOL = isDev ? "http" : "https";
11
-
12
- /**
13
- * Git credential helper를 이용해 특정 레포에 대한 인증 정보를 저장합니다.
14
- * @param username Git 사용자명
15
- * @param token Git 토큰
16
- * @param repoPath 호스트 이후의 경로 예: "my-org/my-repo"
17
- */
18
- function installGitCredentials(
19
- username,
20
- token,
21
- {
22
- protocol = "https",
23
- host,
24
- path // optional
25
- }
26
- )
27
- {
28
- // credential helper 설정 (글로벌, 1회면 충분)
29
- if (process.platform === "win32")
30
- {
31
- execSync("git config --global credential.helper manager-core");
32
- }
33
- else
34
- {
35
- execSync("git config --global credential.helper store");
36
- }
37
-
38
- // git credential approve 입력 생성
39
- const cred = [
40
- `protocol=${protocol}`,
41
- `host=${host}`,
42
- path ? `path=${path}` : null,
43
- `username=${username}`,
44
- `password=${token}`,
45
- ""
46
- ].filter(Boolean).join("\n");
47
-
48
- execSync("git credential approve", {
49
- input: cred,
50
- stdio: ["pipe", "ignore", "inherit"]
51
- });
52
-
53
- if (path)
54
- {
55
- console.log(`✔️ Stored credentials for ${protocol}://${host}/${path}`);
56
- }
57
- else
58
- {
59
- console.log(`✔️ Stored credentials for ${protocol}://${host}`);
60
- }
61
- }
62
-
63
- function persistGradleCredentials(SF_CI_BOT_USER, SF_CI_BOT_TOKEN)
64
- {
65
- const gradlePropsPath = path.join(os.homedir(), ".gradle", "gradle.properties");
66
-
67
- // 기존 내용 읽기
68
- let lines = [];
69
- if (fs.existsSync(gradlePropsPath)) {
70
- lines = fs.readFileSync(gradlePropsPath, "utf8").split(/\r?\n/);
71
- }
72
-
73
- // 기존 SF_CI_BOT_USER, SF_CI_BOT_TOKEN 라인 삭제
74
- const filtered = lines.filter(line =>
75
- !line.startsWith("SF_CI_BOT_USER=") &&
76
- !line.startsWith("SF_CI_BOT_TOKEN=") &&
77
- !line.startsWith("# Superfunctions CLI")
78
- );
79
-
80
- // 새 라인 추가
81
- const exportBlock = [
82
- "",
83
- "# Superfunctions CLI - Gitea Maven Auth",
84
- `SF_CI_BOT_USER=${SF_CI_BOT_USER}`,
85
- `SF_CI_BOT_TOKEN=${SF_CI_BOT_TOKEN}`,
86
- ""
87
- ];
88
-
89
- const updated = filtered.concat(exportBlock);
90
- fs.mkdirSync(path.dirname(gradlePropsPath), { recursive: true });
91
- fs.writeFileSync(gradlePropsPath, updated.join(os.EOL), { encoding: "utf8" });
92
-
93
- console.log(`✔️ ~/.gradle/gradle.properties에 SF_CI_BOT_USER, SF_CI_BOT_TOKEN 등록/갱신 완료`);
94
- console.log(` 모든 Gradle 프로젝트에서 Superfunction Maven 인증 자동 적용`);
95
- }
96
-
97
- export async function login()
98
- {
99
- try
100
- {
101
- const server = http.createServer((req, res) =>
102
- {
103
- res.setHeader("Access-Control-Allow-Origin", "*");
104
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
105
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
106
-
107
- if (req.method === "OPTIONS")
108
- {
109
- res.writeHead(200);
110
- res.end();
111
- return;
112
- }
113
-
114
- if (req.url === "/callback" && req.method === "POST")
115
- {
116
- let body = "";
117
- req.on("data", (chunk) => (body += chunk));
118
- req.on("end", () =>
119
- {
120
- try
121
- {
122
- const { username, token } = JSON.parse(body);
123
- if (!username || !token)
124
- {
125
- res.writeHead(400).end("Missing username or token");
126
- return;
127
- }
128
-
129
- persistGradleCredentials(username, token);
130
-
131
- // npm registry 설정 (생략)
132
- try
133
- {
134
- execSync(
135
- `npm config set @spfn:registry ${GIT_PROTOCOL}://${GIT_HOST}/api/packages/spfn/npm/`,
136
- { stdio: "inherit", env: process.env }
137
- );
138
- execSync(
139
- `npm config set //${GIT_HOST}/api/packages/spfn/npm/:_authToken \"${token}\"`,
140
- { stdio: "inherit", env: process.env }
141
- );
142
- console.log("✔️ npm registry and authToken set via npm config");
143
- }
144
- catch (e)
145
- {
146
- console.error("❌ npm config set failed:", e);
147
- }
148
-
149
- // 2개 레포에 대해 각각 자격증명 저장
150
- installGitCredentials(username, token, {
151
- protocol: GIT_PROTOCOL,
152
- host: GIT_HOST
153
- });
154
-
155
- res.writeHead(200, { "Content-Type": "text/plain" });
156
- res.end("✅ Login successful! You can now close this window.");
157
-
158
- server.close();
159
- }
160
- catch (err)
161
- {
162
- console.error(err);
163
- res.writeHead(500).end("Error during login callback");
164
- server.close();
165
- }
166
- });
167
- }
168
- else
169
- {
170
- res.writeHead(404).end("Not Found");
171
- }
172
- });
173
-
174
- const port = await getAvailablePort();
175
-
176
- // 로그인 UI 주소도 분기
177
- const isDev = process.env.NODE_ENV === "development" || process.env.SF_ENV === "local";
178
- const LOGIN_UI_URL = isDev
179
- ? `http://localhost:3000/cli-login?port=${port}`
180
- : `https://console.superfunctions.ai/cli-login?port=${port}`;
181
-
182
- server.listen(port, () =>
183
- {
184
- console.log("🌐 Opening browser for login...");
185
- open(LOGIN_UI_URL);
186
- });
187
- }
188
- catch (err)
189
- {
190
- console.error(err);
191
- }
192
- }
193
-
194
- function getAvailablePort(defaultPort = 5678) {
195
- return new Promise((resolve) => {
196
- const srv = http.createServer();
197
- srv.listen(defaultPort, () => {
198
- srv.close(() => resolve(defaultPort));
199
- });
200
- srv.on("error", () => {
201
- // 사용 중이면 10000~20000 사이 랜덤 포트
202
- const randomPort = Math.floor(Math.random() * 10000) + 10000;
203
- resolve(randomPort);
204
- });
205
- });
206
- }