@mingfuyun/star-allinone 0.2.0 → 0.3.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/README.md CHANGED
@@ -17,9 +17,10 @@ pnpm add @mingfuyun/star-allinone
17
17
  公共 skill 由本仓库维护,随 `@mingfuyun/star-allinone` 发布。业务项目用 CLI 同步到自己的 `.agents/skills/shared-*` 并提交。Codex / Cursor 直接从业务仓库发现 skill,不写入 `~/.codex/skills`,团队成员也不需要本仓库源码。
18
18
 
19
19
  ```text
20
+ AGENTS.md ← 常驻约定,不要加 .cursor/rules
20
21
  .agents/skills/
21
- ├── shared-* ← npm 包同步,不能在业务项目直接改
22
- └── custom-* ← 业务项目自己维护
22
+ ├── shared-* ← npm 包同步,不能在业务项目直接改
23
+ └── custom-* ← 业务项目自己维护
23
24
  ```
24
25
 
25
26
  完整约定见 [skills/README.md](skills/README.md)。
@@ -32,9 +33,13 @@ pnpm add @mingfuyun/star-allinone
32
33
  pnpm exec star-allinone skills list
33
34
  pnpm exec star-allinone skills sync --dry-run
34
35
  pnpm exec star-allinone skills sync
36
+ pnpm exec star-allinone gen service --operation "GET /api/v1/devices/"
37
+ pnpm exec star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/"
35
38
  ```
36
39
 
37
- `list` 查看包内公共 skill。`sync` 把它们写入当前项目的 `.agents/skills/shared-*`,会整体替换过期的 `shared-*`,但不会改 `custom-*`。
40
+ `gen service` 按 OpenAPI/Swagger 写一条 `MFRequest` 方法和对应类型。`gen page` 生成标准列表页骨架,并可先生成相关 Service。
41
+
42
+ `list` 查看包内公共 skill。`sync` 把它们写入当前项目的 `.agents/skills/shared-*`,会整体替换过期的 `shared-*`,但不会改 `custom-*`。仓库根没有 `AGENTS.md` 时,会从 `examples/AGENTS.md` 补一份,已有文件不覆盖。
38
43
 
39
44
  ### 维护者:改公共 skill
40
45
 
@@ -72,25 +77,32 @@ StarLink 示例见 `examples/projects/`。
72
77
  pnpm exec star-allinone skills sync
73
78
  ```
74
79
 
75
- 4. 把真正的业务特例放到 `.agents/skills/custom-*`。审查、提交、发布、Mock 和 Service 生成不要做成 `custom-*`。
80
+ 4. 把真正的业务特例放到 `.agents/skills/custom-*`。审查、提交、发布、单接口 Mock/Service、标准列表生成不要做成 `custom-*`。全量契约重建(若需要)必须在 custom 里写明「只用于全量」。
81
+
82
+ 5. 仓库根放 `AGENTS.md`(可从包内 `examples/AGENTS.md` 复制)。`skills sync` 只在该文件缺失时补一份,不会覆盖已有内容。常驻约定只写这一份,不要加 `.cursor/rules`。
76
83
 
77
- 5. 提交配置和 skill:
84
+ 6. 提交配置和 skill:
78
85
 
79
86
  ```bash
80
- git add .star-allinone.json .agents/skills
87
+ git add .star-allinone.json AGENTS.md .agents/skills
81
88
  git commit -m "chore(skill): 接入公共 skill"
82
89
  ```
83
90
 
84
91
  ### 业务项目:日常调用
85
92
 
86
- 在 Codex / Cursor 对话里用 `$skill-name` 显式调用,例如:
93
+ 只使用下面这些**用户入口**。不要直接 `$shared-verify`、`$shared-api-mock`、`$shared-commit-current-changes` 等内部 skill。
87
94
 
88
95
  ```text
89
96
  使用 $shared-api-to-service,为 GET /api/v1/devices/ 生成类型、Service 和 JSON Mock。
90
97
  ```
91
98
 
92
99
  ```text
93
- 使用 $shared-design-to-page,根据我上传的设计稿生成页面。
100
+ 使用 $shared-page-recipe,生成 /widgets 标准列表页。
101
+ 列表接口 GET /api/v1/widgets/。
102
+ ```
103
+
104
+ ```text
105
+ 使用 $shared-design-to-page,根据我上传的设计稿生成非标准页面。
94
106
  缺少的 Service、类型和 Mock 一并生成。
95
107
  ```
96
108
 
@@ -103,7 +115,7 @@ git commit -m "chore(skill): 接入公共 skill"
103
115
  使用 $shared-publish-test,把当前分支发布到测试环境。
104
116
  ```
105
117
 
106
- 公共流程用 `shared-*`,项目专属规则用 `custom-*`。业务定制 skill 需要复用公共流程时,在 `custom-*` 的 `SKILL.md` 里写明先读取对应的 `.agents/skills/shared-*/SKILL.md`。
118
+ 公共流程用 `shared-*`,项目专属规则用 `custom-*`。`custom-*` 不能再实现一遍 Service/页面生成;星链全量 swagger 重建除外。业务定制 skill 需要复用公共流程时,在 `custom-*` 的 `SKILL.md` 里写明先读取对应的 `.agents/skills/shared-*/SKILL.md`。
107
119
 
108
120
  ### 业务项目:更新公共 skill
109
121
 
@@ -0,0 +1,29 @@
1
+ # Agent instructions
2
+
3
+ This is a Umi Max + Ant Design 5 PC admin app. Shared skills live in `.agents/skills/shared-*` and are synced from `@mingfuyun/star-allinone`. Do not edit `shared-*`. Put project-only rules in `.agents/skills/custom-*` or `.agents/project/`.
4
+
5
+ Always-on instructions are this file. Do not add `.cursor/rules`.
6
+
7
+ ## Default workflow
8
+
9
+ User-facing shared entries only:
10
+
11
+ - One endpoint: `pnpm exec star-allinone gen service --operation "METHOD /path/"` or `$shared-api-to-service`.
12
+ - Standard list: `$shared-page-recipe` / `pnpm exec star-allinone gen page`.
13
+ - Non-standard UI: `$shared-design-to-page`.
14
+ - Review and commit: `$shared-review-and-submit`. Wait for confirmation.
15
+ - Test publish: `$shared-publish-test` after explicit pipeline confirmation.
16
+
17
+ Do not invoke `shared-api-mock`, `shared-verify`, `shared-impact-analysis`, `shared-ai-review`, `shared-commit-current-changes`, or `shared-project-conventions` directly.
18
+
19
+ ## Hard rules
20
+
21
+ - For non-trivial changes, read `.agents/skills/shared-project-conventions` (do not `$` invoke it).
22
+ - Pages use `import Service from '@/services'`. Do not call axios or `src/services/request.tsx` from pages.
23
+ - Lists: `MFProTable`. Forms: `ProForm`. Overlays: `CustomDrawer`, `BaseModal`, `HocModal` from `@mingfuyun/star-allinone`.
24
+ - Keep columns and actions in `const.tsx`. Empty cells render as `-`.
25
+ - Keep DTOs in the domain `typings.d.ts` namespace. Preserve backend snake_case.
26
+ - Do not edit `src/services/request.tsx` for a single endpoint.
27
+ - Every `await` uses try/catch. Do not use `Modal.confirm`.
28
+ - After TypeScript changes run `pnpm typecheck`.
29
+ - If `.agents/project/conventions.md` or `.agents/project/review-rules.md` exists, apply them as additions only.
@@ -0,0 +1,7 @@
1
+ {
2
+ "project": "starlink-frontend",
3
+ "publicSkillPrefix": "shared-",
4
+ "serviceDir": "src/services",
5
+ "openapi": "swagger.json",
6
+ "publishPipelineId": "5245265"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "project": "starscope-frontend",
3
+ "publicSkillPrefix": "shared-",
4
+ "serviceDir": "src/services",
5
+ "openapi": ".agents/project/openapi.json",
6
+ "publishPipelineId": "5108336"
7
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: custom-device-service
3
+ description: Generate StarScope device service code.
4
+ ---
5
+
6
+ 本任务需要结合公共 skill `shared-api-to-service` 执行。
7
+
8
+ 先读取并遵循:
9
+
10
+ `.agents/skills/shared-api-to-service/SKILL.md`
11
+
12
+ 再读取当前项目的:
13
+
14
+ - OpenAPI 文件
15
+ - Service 约定
16
+ - 项目类型和权限规则
17
+
18
+ 只生成符合当前项目结构的设备相关 service。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingfuyun/star-allinone",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "星系运维前端能力包",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -30,7 +30,9 @@
30
30
  "dist",
31
31
  "README.md",
32
32
  "scripts/cli.js",
33
- "skills"
33
+ "scripts/gen",
34
+ "skills",
35
+ "examples"
34
36
  ],
35
37
  "scripts": {
36
38
  "build": "webpack --env format=umd --mode=none && webpack --env format=esm --mode=none",
@@ -39,7 +41,7 @@
39
41
  "skills:sync": "node scripts/cli.js skills sync",
40
42
  "skills:list": "node scripts/cli.js skills list",
41
43
  "prepublishOnly": "npm run build",
42
- "test": "echo \"Error: no test specified\" && exit 1",
44
+ "test": "node scripts/gen/fixtures/run.js",
43
45
  "dev": "webpack --mode=none --watch",
44
46
  "clean": "rm -rf dist"
45
47
  },
package/scripts/cli.js CHANGED
@@ -17,11 +17,17 @@ function usage() {
17
17
  console.log(`Usage:
18
18
  star-allinone skills list
19
19
  star-allinone skills sync [--target <dir>] [--dry-run]
20
+ star-allinone gen service --operation "GET /api/v1/devices/" [--spec <file>] [--domain Devices] [--mock-file] [--dry-run]
21
+ star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/" [--create-operation ...] [--update-operation ...] [--delete-operation ...] [--dry-run]
20
22
 
21
- Run the command from a business project containing .star-allinone.json.
23
+ Run from a business project containing .star-allinone.json.
22
24
 
23
- Sync writes package skills/shared/* to <project>/.agents/skills/shared-*.
24
- It never writes ~/.codex/skills and never modifies custom-* skills.`);
25
+ skills sync writes package skills/shared/* to <project>/.agents/skills/shared-*.
26
+ It never writes ~/.codex/skills and never modifies custom-* skills.
27
+ If <project>/AGENTS.md is missing, it copies examples/AGENTS.md once.
28
+
29
+ gen service writes one MFRequest method and types from OpenAPI/Swagger.
30
+ gen page scaffolds a list + drawer + confirm page and can generate services first.`);
25
31
  process.exitCode = 1;
26
32
  }
27
33
 
@@ -50,8 +56,12 @@ function parseArgs(args) {
50
56
  for (let index = 0; index < args.length; index += 1) {
51
57
  const arg = args[index];
52
58
  if (arg === '--dry-run') options.dryRun = true;
53
- else if (arg === '--target') options.target = args[++index];
54
- else positional.push(arg);
59
+ else if (arg === '--mock-file') options.mockFile = true;
60
+ else if (arg === '--force') options.force = true;
61
+ else if (arg.startsWith('--')) {
62
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
63
+ options[key] = args[++index];
64
+ } else positional.push(arg);
55
65
  }
56
66
  return { options, positional };
57
67
  }
@@ -158,17 +168,40 @@ function syncProject(options) {
158
168
  copySkill(path.join(skillsRoot, skillName), destination, skillName, prefix, variables, options);
159
169
  }
160
170
  pruneStaleShared(destination, prefix, syncedNames, options);
171
+ seedAgentsMarkdown(target, options);
161
172
  console.log(`${options.dryRun ? 'Would sync' : 'Synced'} shared skills -> ${destination}`);
162
173
  }
163
174
 
175
+ function seedAgentsMarkdown(target, options) {
176
+ const dest = path.join(target, 'AGENTS.md');
177
+ const source = path.join(root, 'examples', 'AGENTS.md');
178
+ if (fs.existsSync(dest) || !fs.existsSync(source)) return;
179
+ console.log(`${options.dryRun ? '[dry-run] ' : ''}AGENTS.md`);
180
+ if (!options.dryRun) fs.copyFileSync(source, dest);
181
+ }
182
+
164
183
  function main() {
165
184
  const [group, command, ...rest] = process.argv.slice(2);
166
- if (group !== 'skills') return usage();
167
- if (command === 'list') return listSkills();
168
- if (command !== 'sync') return usage();
169
- const { options, positional } = parseArgs(rest);
170
- if (positional.length) return usage();
171
- syncProject(options);
185
+ if (group === 'skills') {
186
+ if (command === 'list') return listSkills();
187
+ if (command !== 'sync') return usage();
188
+ const { options, positional } = parseArgs(rest);
189
+ if (positional.length) return usage();
190
+ return syncProject(options);
191
+ }
192
+ if (group === 'gen') {
193
+ const { options, positional } = parseArgs(rest);
194
+ if (positional.length) return usage();
195
+ if (command === 'service') {
196
+ if (!options.operation) return usage();
197
+ return require('./gen/service').generateService(process.cwd(), options);
198
+ }
199
+ if (command === 'page') {
200
+ if (!options.feature) return usage();
201
+ return require('./gen/page').generatePage(process.cwd(), options);
202
+ }
203
+ }
204
+ return usage();
172
205
  }
173
206
 
174
207
  try {
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { generateService } = require('../service');
5
+ const { generatePage } = require('../page');
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) throw new Error(message);
9
+ }
10
+
11
+ function makeProject() {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'star-allinone-gen-'));
13
+ fs.writeFileSync(
14
+ path.join(root, '.star-allinone.json'),
15
+ JSON.stringify(
16
+ {
17
+ project: 'fixture',
18
+ serviceDir: 'src/services',
19
+ openapi: 'openapi.json',
20
+ },
21
+ null,
22
+ 2,
23
+ ),
24
+ );
25
+ fs.copyFileSync(
26
+ path.join(__dirname, 'widgets.openapi.json'),
27
+ path.join(root, 'openapi.json'),
28
+ );
29
+ fs.mkdirSync(path.join(root, 'src/services'), { recursive: true });
30
+ fs.writeFileSync(
31
+ path.join(root, 'src/services/request.tsx'),
32
+ 'const MFRequest = { get() {}, post() {}, patch() {}, delete() {} };\nexport default MFRequest;\n',
33
+ );
34
+ fs.writeFileSync(
35
+ path.join(root, 'src/services/index.ts'),
36
+ 'export default {};\n',
37
+ );
38
+ fs.mkdirSync(path.join(root, 'config'), { recursive: true });
39
+ fs.writeFileSync(
40
+ path.join(root, 'config/route.ts'),
41
+ `export default [
42
+ { path: '/', redirect: '/home' },
43
+ { path: '/*', component: '@/pages/404', name: '404' },
44
+ ];
45
+ `,
46
+ );
47
+ fs.mkdirSync(path.join(root, 'src/components/CommonTable'), { recursive: true });
48
+ fs.mkdirSync(path.join(root, 'src/pages'), { recursive: true });
49
+ return root;
50
+ }
51
+
52
+ function main() {
53
+ const root = makeProject();
54
+ const first = generateService(root, {
55
+ operation: 'GET /api/v1/widgets/',
56
+ mockFile: true,
57
+ });
58
+ assert(first.fn === 'widgetsList', `fn ${first.fn}`);
59
+ assert(first.domain === 'Widgets', `domain ${first.domain}`);
60
+ const service = fs.readFileSync(first.servicePath, 'utf8');
61
+ assert(service.includes("MFRequest.get<WIDGETS_API.WidgetList>('/api/v1/widgets/', params)"), service);
62
+ const typings = fs.readFileSync(first.typingsPath, 'utf8');
63
+ assert(typings.includes('type Widget ='), typings);
64
+ assert(typings.includes('type WidgetList ='), typings);
65
+ assert(typings.includes('results?: Widget[]'), typings);
66
+ assert(typings.includes('status?: string | null'), typings);
67
+ assert(typings.includes('type WidgetsListQuery'), typings);
68
+ const mockFile = path.join(root, 'mock/api/v1/widgets/get.json');
69
+ assert(fs.existsSync(mockFile), 'missing mock file');
70
+ const index = fs.readFileSync(path.join(root, 'src/services/index.ts'), 'utf8');
71
+ assert(index.includes("from './Widgets/widgets'"), index);
72
+
73
+ const reused = generateService(root, { operation: 'GET /api/v1/widgets/' });
74
+ assert(reused.status === 'reuse', 'expected reuse on second run');
75
+
76
+ generateService(root, { operation: 'PATCH /api/v1/widgets/{id}/' });
77
+ const patched = fs.readFileSync(first.servicePath, 'utf8');
78
+ assert(patched.includes('widgetsPartialUpdate'), patched);
79
+ assert(patched.includes('MFRequest.patch<WIDGETS_API.Widget>'), patched);
80
+
81
+ generatePage(root, {
82
+ feature: 'Widgets',
83
+ route: '/widgets',
84
+ title: '组件管理',
85
+ listOperation: 'GET /api/v1/widgets/',
86
+ createOperation: 'POST /api/v1/widgets/',
87
+ updateOperation: 'PATCH /api/v1/widgets/{id}/',
88
+ deleteOperation: 'DELETE /api/v1/widgets/{id}/',
89
+ });
90
+ const page = fs.readFileSync(path.join(root, 'src/pages/Widgets/index.tsx'), 'utf8');
91
+ assert(page.includes('Service.widgetsList'), page);
92
+ assert(page.includes("rowKey=\"id\""), page);
93
+ const route = fs.readFileSync(path.join(root, 'config/route.ts'), 'utf8');
94
+ assert(route.includes("path: '/widgets'"), route);
95
+ fs.rmSync(root, { recursive: true, force: true });
96
+ console.log('codegen fixtures passed');
97
+ }
98
+
99
+ try {
100
+ main();
101
+ } catch (error) {
102
+ console.error(error.stack || error.message);
103
+ process.exit(1);
104
+ }
@@ -0,0 +1,194 @@
1
+ {
2
+ "openapi": "3.0.3",
3
+ "info": {
4
+ "title": "codegen fixture",
5
+ "version": "1.0.0"
6
+ },
7
+ "paths": {
8
+ "/api/v1/widgets/": {
9
+ "get": {
10
+ "operationId": "widgets_list",
11
+ "tags": ["Widgets"],
12
+ "summary": "获取组件列表",
13
+ "parameters": [
14
+ {
15
+ "in": "query",
16
+ "name": "page",
17
+ "required": true,
18
+ "schema": { "type": "integer" }
19
+ },
20
+ {
21
+ "in": "query",
22
+ "name": "page_size",
23
+ "required": true,
24
+ "schema": { "type": "integer" }
25
+ },
26
+ {
27
+ "in": "query",
28
+ "name": "keyword",
29
+ "schema": { "type": "string" }
30
+ }
31
+ ],
32
+ "responses": {
33
+ "200": {
34
+ "description": "ok",
35
+ "content": {
36
+ "application/json": {
37
+ "schema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "code": { "type": "integer" },
41
+ "message": { "type": "string" },
42
+ "data": {
43
+ "$ref": "#/components/schemas/WidgetList"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "post": {
53
+ "operationId": "widgets_create",
54
+ "tags": ["Widgets"],
55
+ "summary": "创建组件",
56
+ "requestBody": {
57
+ "content": {
58
+ "application/json": {
59
+ "schema": {
60
+ "$ref": "#/components/schemas/WidgetBody"
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "responses": {
66
+ "201": {
67
+ "description": "created",
68
+ "content": {
69
+ "application/json": {
70
+ "schema": {
71
+ "type": "object",
72
+ "properties": {
73
+ "code": { "type": "integer" },
74
+ "message": { "type": "string" },
75
+ "data": {
76
+ "$ref": "#/components/schemas/Widget"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "/api/v1/widgets/{id}/": {
87
+ "patch": {
88
+ "operationId": "widgets_partial_update",
89
+ "tags": ["Widgets"],
90
+ "summary": "更新组件",
91
+ "parameters": [
92
+ {
93
+ "in": "path",
94
+ "name": "id",
95
+ "required": true,
96
+ "schema": { "type": "string" }
97
+ }
98
+ ],
99
+ "requestBody": {
100
+ "content": {
101
+ "application/json": {
102
+ "schema": {
103
+ "$ref": "#/components/schemas/WidgetBody"
104
+ }
105
+ }
106
+ }
107
+ },
108
+ "responses": {
109
+ "200": {
110
+ "description": "ok",
111
+ "content": {
112
+ "application/json": {
113
+ "schema": {
114
+ "type": "object",
115
+ "properties": {
116
+ "code": { "type": "integer" },
117
+ "message": { "type": "string" },
118
+ "data": {
119
+ "$ref": "#/components/schemas/Widget"
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ },
128
+ "delete": {
129
+ "operationId": "widgets_destroy",
130
+ "tags": ["Widgets"],
131
+ "summary": "删除组件",
132
+ "parameters": [
133
+ {
134
+ "in": "path",
135
+ "name": "id",
136
+ "required": true,
137
+ "schema": { "type": "string" }
138
+ }
139
+ ],
140
+ "responses": {
141
+ "200": {
142
+ "description": "ok",
143
+ "content": {
144
+ "application/json": {
145
+ "schema": {
146
+ "type": "object",
147
+ "properties": {
148
+ "code": { "type": "integer" },
149
+ "message": { "type": "string" },
150
+ "data": { "type": "object" }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ },
160
+ "components": {
161
+ "schemas": {
162
+ "Widget": {
163
+ "type": "object",
164
+ "required": ["id", "name"],
165
+ "properties": {
166
+ "id": { "type": "string" },
167
+ "name": { "type": "string" },
168
+ "status": { "type": "string", "nullable": true },
169
+ "count": { "type": "integer" }
170
+ }
171
+ },
172
+ "WidgetBody": {
173
+ "type": "object",
174
+ "required": ["name"],
175
+ "properties": {
176
+ "name": { "type": "string" },
177
+ "status": { "type": "string" }
178
+ }
179
+ },
180
+ "WidgetList": {
181
+ "type": "object",
182
+ "properties": {
183
+ "results": {
184
+ "type": "array",
185
+ "items": {
186
+ "$ref": "#/components/schemas/Widget"
187
+ }
188
+ },
189
+ "count": { "type": "integer" }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,117 @@
1
+ function toPascal(value) {
2
+ return String(value || '')
3
+ .replace(/[^\w]+/g, ' ')
4
+ .split(/[\s_-]+/)
5
+ .filter(Boolean)
6
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
7
+ .join('');
8
+ }
9
+
10
+ function toCamel(value) {
11
+ const pascal = toPascal(value);
12
+ return pascal ? pascal.charAt(0).toLowerCase() + pascal.slice(1) : '';
13
+ }
14
+
15
+ function toNamespace(domain) {
16
+ return `${String(domain)
17
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
18
+ .replace(/[^\w]+/g, '_')
19
+ .replace(/_+/g, '_')
20
+ .toUpperCase()}_API`;
21
+ }
22
+
23
+ function toServiceFile(domain) {
24
+ return toCamel(domain);
25
+ }
26
+
27
+ function normalizePath(apiPath) {
28
+ if (!apiPath) return '/';
29
+ const withSlash = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
30
+ return withSlash.replace(/\/{2,}/g, '/');
31
+ }
32
+
33
+ function pathVariants(apiPath) {
34
+ const normalized = normalizePath(apiPath);
35
+ const trimmed = normalized.replace(/\/+$/, '') || '/';
36
+ const withSlash = trimmed === '/' ? '/' : `${trimmed}/`;
37
+ return [...new Set([normalized, trimmed, withSlash])];
38
+ }
39
+
40
+ function pathParamsFromPath(apiPath) {
41
+ return [...String(apiPath).matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
42
+ }
43
+
44
+ function pathToTemplate(apiPath) {
45
+ const replaced = normalizePath(apiPath).replace(/\{([^}]+)\}/g, '${$1}');
46
+ return replaced.includes('${') ? `\`${replaced}\`` : `'${replaced}'`;
47
+ }
48
+
49
+ function parseOperationSelector(raw) {
50
+ const value = String(raw || '').trim();
51
+ if (!value) throw new Error('Missing --operation. Example: GET /api/v1/devices/');
52
+ const operationId = value.match(/^operationId\s*:\s*(.+)$/i);
53
+ if (operationId) return { operationId: operationId[1].trim() };
54
+ const spaced = value.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/\S+)/i);
55
+ if (spaced) {
56
+ return { method: spaced[1].toLowerCase(), path: spaced[2] };
57
+ }
58
+ if (value.startsWith('/')) return { path: value };
59
+ return { operationId: value };
60
+ }
61
+
62
+ function functionName(operation) {
63
+ if (operation.operationId) return toCamel(operation.operationId);
64
+ const params = pathParamsFromPath(operation.path);
65
+ const segments = normalizePath(operation.path)
66
+ .replace(/\{[^}]+\}/g, '')
67
+ .split('/')
68
+ .filter(Boolean);
69
+ const resource = toPascal(segments[segments.length - 1] || 'Resource');
70
+ const method = operation.method.toLowerCase();
71
+ if (method === 'get' && params.length) return `get${resource}Detail`;
72
+ if (method === 'get') return `list${resource}`;
73
+ if (method === 'post') return `create${resource}`;
74
+ if (method === 'patch' || method === 'put') return `update${resource}`;
75
+ if (method === 'delete') return `delete${resource}`;
76
+ return toCamel(`${method}_${resource}`);
77
+ }
78
+
79
+ function requestHelper(operation) {
80
+ const method = operation.method.toLowerCase();
81
+ const contentType = operation.contentType || '';
82
+ if (method === 'get' && /export|download/.test(operation.path)) return 'export';
83
+ if (contentType.includes('multipart')) return 'import';
84
+ if (method === 'get') return 'get';
85
+ if (method === 'post') return 'post';
86
+ if (method === 'patch') return 'patch';
87
+ if (method === 'put') return 'put';
88
+ if (method === 'delete') return 'delete';
89
+ return method;
90
+ }
91
+
92
+ function domainFromPath(apiPath) {
93
+ const segments = normalizePath(apiPath).split('/').filter(Boolean);
94
+ const afterApi = segments[0] === 'api' ? segments.slice(2) : segments;
95
+ return toPascal(afterApi[0] || 'Common');
96
+ }
97
+
98
+ function typeName(prefix, suffix, useIPrefix) {
99
+ const name = `${toPascal(prefix)}${toPascal(suffix)}`;
100
+ return useIPrefix ? `I${name}` : name;
101
+ }
102
+
103
+ module.exports = {
104
+ toPascal,
105
+ toCamel,
106
+ toNamespace,
107
+ toServiceFile,
108
+ normalizePath,
109
+ pathVariants,
110
+ pathParamsFromPath,
111
+ pathToTemplate,
112
+ parseOperationSelector,
113
+ functionName,
114
+ requestHelper,
115
+ domainFromPath,
116
+ typeName,
117
+ };