@spfn/cli 0.0.9 → 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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/bin/spfn.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +829 -0
- package/dist/templates/.guide/api-routes.md +388 -0
- package/dist/templates/server/entities/README.md +131 -0
- package/dist/templates/server/routes/examples/contract.ts +101 -0
- package/dist/templates/server/routes/examples/index.ts +112 -0
- package/dist/templates/server/routes/health/contract.ts +13 -0
- package/dist/templates/server/routes/health/index.ts +23 -0
- package/dist/templates/server/routes/index/contract.ts +13 -0
- package/dist/templates/server/routes/index/index.ts +28 -0
- package/package.json +67 -47
- package/lib/index.js +0 -19
- package/lib/login.js +0 -206
@@ -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,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,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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
"
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"
|
18
|
-
"
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"
|
24
|
-
"
|
25
|
-
"
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
"
|
33
|
-
|
34
|
-
|
35
|
-
"
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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,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
|
-
}
|