@wato787/microcms-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) 2026 wato787
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,96 @@
1
+ # microCMS CLI
2
+
3
+ microCMS 用の CLI ツールです。実行コマンドは `microcms-cli` です。
4
+ npm 配布物は事前ビルド済みのため、実行時に Bun は不要です(Node.js 18+)。
5
+
6
+ - `gen-types`: Management API のスキーマから TypeScript 型を生成
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @wato787/microcms-cli
12
+ # または
13
+ bun add -g @wato787/microcms-cli
14
+ ```
15
+
16
+ ```bash
17
+ # インストールせずに実行
18
+ npx @wato787/microcms-cli gen-types blog
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `gen-types [endpointId] [options]`
24
+
25
+ microCMS Management API(`/api/v1/apis`)から API スキーマを取得し、TypeScript 型を生成します。
26
+ 生成結果は `microcms.d.ts` に集約されます。
27
+ 共通型(`MicroCMSListResponse` など)は `microcms-js-sdk` の公式定義に合わせています。
28
+ このコマンドは Content API のコンテンツ取得エンドポイントは呼びません。
29
+
30
+ - `endpointId` 指定時: 対象 endpoint のスキーマのみ取得
31
+ - `--all` 指定時: API 一覧を取得して全 endpoint を生成
32
+ - 生成型: `XxxSchema`(スキーマ本体)と `XxxContent`(SDK の共通メタ情報付き)
33
+ - 利用する Management API: `/api/v1/apis`, `/api/v1/apis/{endpoint}`
34
+
35
+ ```bash
36
+ # 単一エンドポイント
37
+ microcms-cli gen-types blog
38
+
39
+ # 出力先ディレクトリ指定(デフォルトは ./types/microcms.d.ts)
40
+ microcms-cli gen-types blog -o ./src/types/microcms
41
+ # => ./src/types/microcms/microcms.d.ts
42
+
43
+ # 出力ファイルを直接指定
44
+ microcms-cli gen-types blog -o ./src/types/microcms.d.ts
45
+
46
+ # 全エンドポイントを一括生成
47
+ microcms-cli gen-types --all
48
+ # endpointIdを渡しても --all 指定時は無視されます
49
+ microcms-cli gen-types blog --all
50
+
51
+ # CIなどで環境変数をインライン指定
52
+ MICROCMS_SERVICE_DOMAIN=your-service-id \
53
+ MICROCMS_MANAGEMENT_API_KEY=your-management-api-key \
54
+ microcms-cli gen-types blog
55
+ ```
56
+
57
+ #### Options
58
+
59
+ - `-o, --output <path>`: 出力先(ディレクトリ指定時は `<path>/microcms.d.ts`、デフォルト `./types/microcms.d.ts`)
60
+ - `--all`: 全エンドポイントの型を生成
61
+ - `--service-domain <domain>`: `MICROCMS_SERVICE_DOMAIN` をCLI引数で上書き
62
+ - `--api-key <key>`: `MICROCMS_MANAGEMENT_API_KEY` をCLI引数で上書き
63
+
64
+ #### Required environment variables
65
+
66
+ `gen-types` は **実行した利用者の環境変数** から設定を読み取ります。
67
+ CLI引数で指定しない場合、以下の環境変数が必要です。
68
+
69
+ ```bash
70
+ MICROCMS_SERVICE_DOMAIN=your-service-id
71
+ MICROCMS_MANAGEMENT_API_KEY=your-management-api-key
72
+ ```
73
+
74
+ #### Required Management API permission
75
+
76
+ `MICROCMS_MANAGEMENT_API_KEY` には、マネジメントAPIのGET権限として **「API情報の取得」** が必要です。
77
+
78
+ > 補足: 単一 endpoint で `apiType` が取得できない場合、CLI は警告を出して LIST として型生成します。
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ bun install
84
+ npm run build
85
+ bun run typecheck
86
+ bun run test
87
+
88
+ # 事前ビルド済み CLI を実行
89
+ npm run start -- gen-types blog
90
+
91
+ # ソースから実行(Bun)
92
+ bun run dev gen-types blog
93
+
94
+ # 単一バイナリを生成
95
+ bun run compile
96
+ ```
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveConfig, resolveOutputFilePath, resolveSingleEndpoint } from './config.js';
4
+ import { fetchApiList, fetchApiSchema } from './management-api.js';
5
+ import { toErrorMessage } from './shared.js';
6
+ import { renderDefinitionsFile } from './type-generator.js';
7
+ function getTargetEndpoints(apiList) {
8
+ const deduped = new Map();
9
+ for (const api of apiList) {
10
+ if (!deduped.has(api.apiEndpoint)) {
11
+ deduped.set(api.apiEndpoint, api);
12
+ }
13
+ }
14
+ return Array.from(deduped.values()).sort((a, b) => a.apiEndpoint.localeCompare(b.apiEndpoint));
15
+ }
16
+ function createGenerationTarget(endpoint, schema, apiType) {
17
+ return {
18
+ endpoint,
19
+ apiType,
20
+ schema: {
21
+ ...schema,
22
+ apiEndpoint: schema.apiEndpoint ?? endpoint,
23
+ },
24
+ };
25
+ }
26
+ function resolveApiTypeByEndpoint(apiList, endpoint) {
27
+ return apiList.find((item) => item.apiEndpoint === endpoint)?.apiType;
28
+ }
29
+ async function resolveSingleEndpointApiType(config, endpoint, schema, fetchApiListFn, warn) {
30
+ if (schema.apiType) {
31
+ return schema.apiType;
32
+ }
33
+ try {
34
+ const apiList = await fetchApiListFn(config);
35
+ return resolveApiTypeByEndpoint(apiList, endpoint);
36
+ }
37
+ catch (error) {
38
+ warn(`[warn] Failed to resolve apiType for "${endpoint}" from /apis: ${toErrorMessage(error)}`);
39
+ return undefined;
40
+ }
41
+ }
42
+ const defaultGenTypesCommandDeps = {
43
+ resolveOutputFilePath,
44
+ resolveConfig,
45
+ resolveSingleEndpoint,
46
+ fetchApiList,
47
+ fetchApiSchema,
48
+ renderDefinitionsFile,
49
+ mkdirSync: fs.mkdirSync,
50
+ writeFileSync: fs.writeFileSync,
51
+ warn: console.warn,
52
+ log: console.log,
53
+ error: console.error,
54
+ exit: process.exit,
55
+ };
56
+ async function runGenTypesCommand(endpointId, options, deps) {
57
+ try {
58
+ const all = Boolean(options.all);
59
+ if (all && endpointId) {
60
+ deps.warn('[warn] endpointId is ignored because --all is specified.');
61
+ }
62
+ const outputFilePath = deps.resolveOutputFilePath(options.output);
63
+ const config = deps.resolveConfig(options);
64
+ deps.mkdirSync(path.dirname(outputFilePath), { recursive: true });
65
+ const targets = [];
66
+ if (all) {
67
+ const apiList = getTargetEndpoints(await deps.fetchApiList(config));
68
+ if (apiList.length === 0) {
69
+ throw new Error('No endpoints found from Management API.');
70
+ }
71
+ for (const api of apiList) {
72
+ const schema = await deps.fetchApiSchema(config, api.apiEndpoint);
73
+ targets.push(createGenerationTarget(api.apiEndpoint, schema, api.apiType));
74
+ }
75
+ }
76
+ else {
77
+ const endpoint = deps.resolveSingleEndpoint(endpointId);
78
+ const schema = await deps.fetchApiSchema(config, endpoint);
79
+ const apiType = await resolveSingleEndpointApiType(config, endpoint, schema, deps.fetchApiList, deps.warn);
80
+ if (!apiType && !schema.apiType) {
81
+ deps.warn(`[warn] apiType for "${endpoint}" could not be resolved. Falling back to LIST.`);
82
+ }
83
+ targets.push(createGenerationTarget(endpoint, schema, apiType));
84
+ }
85
+ const source = deps.renderDefinitionsFile(targets);
86
+ deps.writeFileSync(outputFilePath, source, 'utf-8');
87
+ deps.log(`Generated ${targets.length} endpoint type(s): ${outputFilePath}`);
88
+ }
89
+ catch (error) {
90
+ deps.error('Error:', toErrorMessage(error));
91
+ deps.exit(1);
92
+ }
93
+ }
94
+ export function createGenTypesCommand(overrides = {}) {
95
+ const deps = {
96
+ ...defaultGenTypesCommandDeps,
97
+ ...overrides,
98
+ };
99
+ return async (endpointId, options) => {
100
+ await runGenTypesCommand(endpointId, options, deps);
101
+ };
102
+ }
103
+ export async function genTypesCommand(endpointId, options) {
104
+ await runGenTypesCommand(endpointId, options, defaultGenTypesCommandDeps);
105
+ }
@@ -0,0 +1,34 @@
1
+ import path from 'node:path';
2
+ import { DEFAULT_OUTPUT_FILE_NAME, DEFAULT_OUTPUT_PATH } from './constants.js';
3
+ import { toStringValue } from './shared.js';
4
+ function isDeclarationFilePath(filePath) {
5
+ return filePath.toLowerCase().endsWith('.d.ts');
6
+ }
7
+ export function resolveOutputFilePath(outputOption) {
8
+ const outputPath = outputOption ?? DEFAULT_OUTPUT_PATH;
9
+ const resolvedPath = path.resolve(process.cwd(), outputPath);
10
+ if (isDeclarationFilePath(resolvedPath)) {
11
+ return resolvedPath;
12
+ }
13
+ return path.join(resolvedPath, DEFAULT_OUTPUT_FILE_NAME);
14
+ }
15
+ export function resolveConfig(options) {
16
+ const serviceDomain = toStringValue(options.serviceDomain) ??
17
+ toStringValue(process.env.MICROCMS_SERVICE_DOMAIN);
18
+ if (!serviceDomain) {
19
+ throw new Error('MICROCMS_SERVICE_DOMAIN is required. You can also pass --service-domain.');
20
+ }
21
+ const apiKey = toStringValue(options.apiKey) ??
22
+ toStringValue(process.env.MICROCMS_MANAGEMENT_API_KEY);
23
+ if (!apiKey) {
24
+ throw new Error('MICROCMS_MANAGEMENT_API_KEY is required. You can also pass --api-key.');
25
+ }
26
+ return { serviceDomain, apiKey };
27
+ }
28
+ export function resolveSingleEndpoint(endpointId) {
29
+ const resolved = toStringValue(endpointId);
30
+ if (!resolved) {
31
+ throw new Error('endpointId is required unless --all is specified.');
32
+ }
33
+ return resolved;
34
+ }
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_OUTPUT_PATH = './types/microcms.d.ts';
2
+ export const DEFAULT_OUTPUT_FILE_NAME = 'microcms.d.ts';
3
+ export const MANAGEMENT_API_BASE_DOMAIN = 'microcms-management.io';
@@ -0,0 +1 @@
1
+ export { genTypesCommand } from './command.js';
@@ -0,0 +1,131 @@
1
+ import { MANAGEMENT_API_BASE_DOMAIN } from './constants.js';
2
+ import { isRecord, toBooleanValue, toStringArray, toStringValue, } from './shared.js';
3
+ function buildManagementApiUrl(serviceDomain, resourcePath) {
4
+ return `https://${serviceDomain}.${MANAGEMENT_API_BASE_DOMAIN}/api/v1/${resourcePath}`;
5
+ }
6
+ async function fetchFromManagementApi(url, apiKey) {
7
+ const response = await fetch(url, {
8
+ method: 'GET',
9
+ headers: {
10
+ 'X-MICROCMS-API-KEY': apiKey,
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ const responseBody = await response.text();
15
+ if (response.status === 401) {
16
+ throw new Error(`Management API authentication failed (401). Check MICROCMS_MANAGEMENT_API_KEY. ${responseBody}`);
17
+ }
18
+ if (response.status === 403) {
19
+ throw new Error(`Management API authorization failed (403). Ensure "API情報の取得" is enabled for the key. ${responseBody}`);
20
+ }
21
+ throw new Error(`Management API request failed (${response.status} ${response.statusText}): ${responseBody}`);
22
+ }
23
+ return response.json();
24
+ }
25
+ function parseApiField(rawField) {
26
+ if (!isRecord(rawField)) {
27
+ return null;
28
+ }
29
+ const fieldId = toStringValue(rawField.fieldId);
30
+ const kind = toStringValue(rawField.kind);
31
+ if (!fieldId || !kind) {
32
+ return null;
33
+ }
34
+ return {
35
+ fieldId,
36
+ kind,
37
+ required: toBooleanValue(rawField.required) ?? false,
38
+ multipleSelect: toBooleanValue(rawField.multipleSelect) ?? false,
39
+ customFieldCreatedAt: toStringValue(rawField.customFieldCreatedAt),
40
+ customFieldCreatedAtList: toStringArray(rawField.customFieldCreatedAtList),
41
+ };
42
+ }
43
+ function parseCustomField(rawCustomField) {
44
+ if (!isRecord(rawCustomField)) {
45
+ return null;
46
+ }
47
+ const createdAt = toStringValue(rawCustomField.createdAt);
48
+ const fieldId = toStringValue(rawCustomField.fieldId);
49
+ const fields = Array.isArray(rawCustomField.fields)
50
+ ? rawCustomField.fields
51
+ .map((field) => parseApiField(field))
52
+ .filter((field) => field !== null)
53
+ : [];
54
+ if (!createdAt || !fieldId) {
55
+ return null;
56
+ }
57
+ return {
58
+ createdAt,
59
+ fieldId,
60
+ fields,
61
+ };
62
+ }
63
+ function parseApiSchema(rawSchema) {
64
+ if (!isRecord(rawSchema)) {
65
+ throw new Error('Management API schema response is invalid.');
66
+ }
67
+ const apiFields = Array.isArray(rawSchema.apiFields)
68
+ ? rawSchema.apiFields
69
+ .map((field) => parseApiField(field))
70
+ .filter((field) => field !== null)
71
+ : [];
72
+ const customFields = Array.isArray(rawSchema.customFields)
73
+ ? rawSchema.customFields
74
+ .map((field) => parseCustomField(field))
75
+ .filter((field) => field !== null)
76
+ : [];
77
+ return {
78
+ apiFields,
79
+ customFields,
80
+ apiType: toStringValue(rawSchema.apiType),
81
+ apiEndpoint: toStringValue(rawSchema.apiEndpoint),
82
+ apiName: toStringValue(rawSchema.apiName),
83
+ };
84
+ }
85
+ function parseApiListItem(rawItem) {
86
+ if (!isRecord(rawItem)) {
87
+ return null;
88
+ }
89
+ const apiEndpoint = toStringValue(rawItem.apiEndpoint) ?? toStringValue(rawItem.endpoint);
90
+ if (!apiEndpoint) {
91
+ return null;
92
+ }
93
+ return {
94
+ apiEndpoint,
95
+ apiName: toStringValue(rawItem.apiName) ?? toStringValue(rawItem.name),
96
+ apiType: toStringValue(rawItem.apiType) ?? toStringValue(rawItem.type),
97
+ };
98
+ }
99
+ function parseApiList(rawResponse) {
100
+ const candidateArrays = [];
101
+ if (Array.isArray(rawResponse)) {
102
+ candidateArrays.push(rawResponse);
103
+ }
104
+ else if (isRecord(rawResponse)) {
105
+ for (const key of ['apis', 'contents', 'items', 'data']) {
106
+ const value = rawResponse[key];
107
+ if (Array.isArray(value)) {
108
+ candidateArrays.push(value);
109
+ }
110
+ }
111
+ }
112
+ for (const items of candidateArrays) {
113
+ const parsedItems = items
114
+ .map((item) => parseApiListItem(item))
115
+ .filter((item) => item !== null);
116
+ if (parsedItems.length > 0) {
117
+ return parsedItems;
118
+ }
119
+ }
120
+ throw new Error('Failed to parse API list from Management API response.');
121
+ }
122
+ export async function fetchApiList(config) {
123
+ const url = buildManagementApiUrl(config.serviceDomain, 'apis');
124
+ const rawResponse = await fetchFromManagementApi(url, config.apiKey);
125
+ return parseApiList(rawResponse);
126
+ }
127
+ export async function fetchApiSchema(config, endpointId) {
128
+ const url = buildManagementApiUrl(config.serviceDomain, `apis/${encodeURIComponent(endpointId)}`);
129
+ const rawResponse = await fetchFromManagementApi(url, config.apiKey);
130
+ return parseApiSchema(rawResponse);
131
+ }
@@ -0,0 +1,36 @@
1
+ export function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ export function toStringValue(value) {
5
+ if (typeof value !== 'string') {
6
+ return undefined;
7
+ }
8
+ const trimmed = value.trim();
9
+ return trimmed.length > 0 ? trimmed : undefined;
10
+ }
11
+ export function toBooleanValue(value) {
12
+ return typeof value === 'boolean' ? value : undefined;
13
+ }
14
+ export function toStringArray(value) {
15
+ if (!Array.isArray(value)) {
16
+ return undefined;
17
+ }
18
+ const values = value
19
+ .map((item) => toStringValue(item))
20
+ .filter((item) => item !== undefined);
21
+ return values.length > 0 ? values : undefined;
22
+ }
23
+ export function toPascalCase(value) {
24
+ const chunks = value.split(/[^A-Za-z0-9]+/).filter((chunk) => chunk.length > 0);
25
+ const raw = chunks
26
+ .map((chunk) => `${chunk.charAt(0).toUpperCase()}${chunk.slice(1)}`)
27
+ .join('');
28
+ const safe = raw.length > 0 ? raw : 'Generated';
29
+ return /^[A-Za-z_$]/.test(safe) ? safe : `T${safe}`;
30
+ }
31
+ export function toTypePropertyName(value) {
32
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : JSON.stringify(value);
33
+ }
34
+ export function toErrorMessage(error) {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
@@ -0,0 +1,182 @@
1
+ import { toPascalCase, toTypePropertyName } from './shared.js';
2
+ export function normalizeApiType(apiType) {
3
+ return apiType?.toUpperCase() === 'OBJECT' ? 'OBJECT' : 'LIST';
4
+ }
5
+ function getUniqueTypeName(baseName, context) {
6
+ let candidate = baseName;
7
+ let suffix = 2;
8
+ while (context.usedCustomTypeNames.has(candidate)) {
9
+ candidate = `${baseName}${suffix}`;
10
+ suffix += 1;
11
+ }
12
+ context.usedCustomTypeNames.add(candidate);
13
+ return candidate;
14
+ }
15
+ function resolveCustomFieldTypeName(createdAt, context) {
16
+ const existing = context.customTypeNameByCreatedAt.get(createdAt);
17
+ if (existing) {
18
+ return existing;
19
+ }
20
+ const customField = context.customFieldByCreatedAt.get(createdAt);
21
+ if (!customField) {
22
+ return undefined;
23
+ }
24
+ const baseName = `${context.endpointBaseTypeName}${toPascalCase(customField.fieldId)}CustomField`;
25
+ const uniqueName = getUniqueTypeName(baseName, context);
26
+ context.customTypeNameByCreatedAt.set(createdAt, uniqueName);
27
+ return uniqueName;
28
+ }
29
+ function emitCustomFieldType(createdAt, context) {
30
+ const typeName = resolveCustomFieldTypeName(createdAt, context);
31
+ if (!typeName) {
32
+ return 'Record<string, unknown>';
33
+ }
34
+ if (context.emittedCustomTypeNames.has(typeName)) {
35
+ return typeName;
36
+ }
37
+ if (context.processingCustomFieldIds.has(createdAt)) {
38
+ return 'Record<string, unknown>';
39
+ }
40
+ const customField = context.customFieldByCreatedAt.get(createdAt);
41
+ if (!customField) {
42
+ return 'Record<string, unknown>';
43
+ }
44
+ context.processingCustomFieldIds.add(createdAt);
45
+ const fieldLines = customField.fields.map((field) => renderTypePropertyLine(field, context));
46
+ const typeBlock = fieldLines.length === 0
47
+ ? `export type ${typeName} = Record<string, never>;`
48
+ : [`export type ${typeName} = {`, ...fieldLines, '};'].join('\n');
49
+ context.emittedCustomTypeBlocks.push(typeBlock);
50
+ context.emittedCustomTypeNames.add(typeName);
51
+ context.processingCustomFieldIds.delete(createdAt);
52
+ return typeName;
53
+ }
54
+ function resolveFieldType(field, context) {
55
+ switch (field.kind) {
56
+ case 'text':
57
+ case 'textArea':
58
+ case 'richEditor':
59
+ case 'richEditorV2':
60
+ case 'date':
61
+ return 'string';
62
+ case 'number':
63
+ return 'number';
64
+ case 'boolean':
65
+ return 'boolean';
66
+ case 'media':
67
+ return 'MicroCMSImage';
68
+ case 'mediaList':
69
+ return 'MicroCMSImage[]';
70
+ case 'relation':
71
+ return 'MicroCMSContentId';
72
+ case 'relationList':
73
+ return 'MicroCMSContentId[]';
74
+ case 'select':
75
+ return field.multipleSelect ? 'string[]' : 'string';
76
+ case 'custom':
77
+ if (!field.customFieldCreatedAt) {
78
+ return 'Record<string, unknown>';
79
+ }
80
+ return emitCustomFieldType(field.customFieldCreatedAt, context);
81
+ case 'repeater': {
82
+ const customFieldCreatedAtList = field.customFieldCreatedAtList ?? [];
83
+ if (customFieldCreatedAtList.length === 0) {
84
+ return 'Array<Record<string, unknown>>';
85
+ }
86
+ const itemTypes = customFieldCreatedAtList.map((createdAt) => {
87
+ const customField = context.customFieldByCreatedAt.get(createdAt);
88
+ if (!customField) {
89
+ return 'Record<string, unknown>';
90
+ }
91
+ const customTypeName = emitCustomFieldType(createdAt, context);
92
+ return `({ fieldId: ${JSON.stringify(customField.fieldId)} } & ${customTypeName})`;
93
+ });
94
+ return `Array<${itemTypes.join(' | ')}>`;
95
+ }
96
+ default:
97
+ return 'unknown';
98
+ }
99
+ }
100
+ function renderTypePropertyLine(field, context) {
101
+ const propertyName = toTypePropertyName(field.fieldId);
102
+ const optionalMark = field.required ? '' : '?';
103
+ const typeExpression = resolveFieldType(field, context);
104
+ return ` ${propertyName}${optionalMark}: ${typeExpression};`;
105
+ }
106
+ function renderEndpointType(target) {
107
+ const endpoint = target.endpoint;
108
+ const apiType = normalizeApiType(target.schema.apiType ?? target.apiType);
109
+ const endpointBaseTypeName = toPascalCase(endpoint);
110
+ const context = {
111
+ endpointBaseTypeName,
112
+ customFieldByCreatedAt: new Map(target.schema.customFields.map((field) => [field.createdAt, field])),
113
+ customTypeNameByCreatedAt: new Map(),
114
+ usedCustomTypeNames: new Set(),
115
+ emittedCustomTypeNames: new Set(),
116
+ processingCustomFieldIds: new Set(),
117
+ emittedCustomTypeBlocks: [],
118
+ };
119
+ const schemaTypeName = `${endpointBaseTypeName}Schema`;
120
+ const contentTypeName = `${endpointBaseTypeName}Content`;
121
+ const rootFieldLines = target.schema.apiFields.map((field) => renderTypePropertyLine(field, context));
122
+ const responseTypeBlock = apiType === 'LIST'
123
+ ? `\nexport type ${endpointBaseTypeName}ListResponse = MicroCMSListResponse<${schemaTypeName}>;`
124
+ : '';
125
+ const customTypesBlock = context.emittedCustomTypeBlocks.length > 0
126
+ ? `${context.emittedCustomTypeBlocks.join('\n\n')}\n\n`
127
+ : '';
128
+ const schemaTypeBlock = rootFieldLines.length === 0
129
+ ? `export type ${schemaTypeName} = Record<string, never>;`
130
+ : [`export type ${schemaTypeName} = {`, ...rootFieldLines, '};'].join('\n');
131
+ const contentTypeBlock = apiType === 'OBJECT'
132
+ ? `export type ${contentTypeName} = ${schemaTypeName} & MicroCMSObjectContent;`
133
+ : `export type ${contentTypeName} = ${schemaTypeName} & MicroCMSListContent;`;
134
+ return [
135
+ `// Endpoint schema from Management API: ${endpoint} (${apiType})`,
136
+ '',
137
+ customTypesBlock + schemaTypeBlock,
138
+ '',
139
+ contentTypeBlock + responseTypeBlock,
140
+ '',
141
+ ].join('\n');
142
+ }
143
+ export function renderDefinitionsFile(targets) {
144
+ const sortedTargets = [...targets].sort((a, b) => a.endpoint.localeCompare(b.endpoint));
145
+ const commonTypesBlock = [
146
+ '// Generated by microcms-cli gen-types.',
147
+ '// Source: microCMS Management API (/api/v1/apis, /api/v1/apis/{endpoint}).',
148
+ '// Note: This command does not fetch content from microCMS Content API.',
149
+ '// DO NOT EDIT.',
150
+ '',
151
+ 'export interface MicroCMSContentId {',
152
+ ' id: string;',
153
+ '}',
154
+ '',
155
+ 'export interface MicroCMSDate {',
156
+ ' createdAt: string;',
157
+ ' updatedAt: string;',
158
+ ' publishedAt?: string;',
159
+ ' revisedAt?: string;',
160
+ '}',
161
+ '',
162
+ 'export interface MicroCMSImage {',
163
+ ' url: string;',
164
+ ' width?: number;',
165
+ ' height?: number;',
166
+ ' alt?: string;',
167
+ '}',
168
+ '',
169
+ 'export interface MicroCMSListResponse<T> {',
170
+ ' contents: (T & MicroCMSListContent)[];',
171
+ ' totalCount: number;',
172
+ ' limit: number;',
173
+ ' offset: number;',
174
+ '}',
175
+ '',
176
+ 'export type MicroCMSListContent = MicroCMSContentId & MicroCMSDate;',
177
+ 'export type MicroCMSObjectContent = MicroCMSDate;',
178
+ '',
179
+ ].join('\n');
180
+ const endpointBlocks = sortedTargets.map((target) => renderEndpointType(target)).join('\n');
181
+ return [commonTypesBlock, endpointBlocks].join('\n');
182
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { genTypesCommand } from './commands/gen-types/index.js';
4
+ const program = new Command();
5
+ program
6
+ .name('microcms-cli')
7
+ .description('CLI for microCMS')
8
+ .version('0.1.0');
9
+ program
10
+ .command('gen-types [endpointId]')
11
+ .description('Fetch API schema from microCMS Management API and generate TypeScript types')
12
+ .option('-o, --output <path>', 'Output path (default: ./types/microcms.d.ts)', './types/microcms.d.ts')
13
+ .option('--all', 'Generate types for all endpoints')
14
+ .option('--service-domain <domain>', 'Override MICROCMS_SERVICE_DOMAIN')
15
+ .option('--api-key <key>', 'Override MICROCMS_MANAGEMENT_API_KEY')
16
+ .action(async (endpointId, options) => {
17
+ await genTypesCommand(endpointId, options);
18
+ });
19
+ // Show help if no command is provided
20
+ if (process.argv.length === 2) {
21
+ program.outputHelp();
22
+ }
23
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@wato787/microcms-cli",
3
+ "version": "0.1.0",
4
+ "description": "AI-friendly CLI tool for microCMS, built with Bun",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "type": "module",
8
+ "bin": {
9
+ "microcms-cli": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/index.js', 0o755)\"",
18
+ "dev": "bun src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "test": "bun test",
21
+ "typecheck": "tsgo --noEmit",
22
+ "compile": "bun build ./src/index.ts --compile --outfile microcms-cli",
23
+ "prepack": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "commander": "^12.0.0",
27
+ "picocolors": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "latest",
31
+ "@types/node": "^24.10.13",
32
+ "@typescript/native-preview": "^7.0.0-dev.20260220.1",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "keywords": [
39
+ "microcms",
40
+ "cli",
41
+ "bun",
42
+ "typescript",
43
+ "mcp"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }