@goqoo/trunks 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/.claude/settings.local.json +7 -0
- package/.prettierignore +1 -0
- package/.prettierrc.js +5 -0
- package/CLAUDE.md +71 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +16 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.js +204 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.js +7 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +2 -0
- package/dts/activity-fields.d.ts +25 -0
- package/dts/customer-fields.d.ts +28 -0
- package/dts/project-fields.d.ts +30 -0
- package/jest.config.js +19 -0
- package/package.json +43 -0
- package/src/cli.ts +28 -0
- package/src/config.ts +23 -0
- package/src/generate.ts +238 -0
- package/src/index.ts +14 -0
- package/src/oauth.ts +14 -0
- package/src/types.ts +63 -0
- package/test/config.test.ts +15 -0
- package/test/generate.test.ts +32 -0
- package/test/types.test.ts +116 -0
- package/trunks.config.ts +18 -0
- package/tsconfig.json +16 -0
package/.prettierignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.md
|
package/.prettierrc.js
ADDED
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## プロジェクト概要
|
|
6
|
+
|
|
7
|
+
@goqoo/trunks - `@kintone/dts-gen`のラッパーCLIツール。
|
|
8
|
+
|
|
9
|
+
### 背景・目的
|
|
10
|
+
|
|
11
|
+
`@kintone/dts-gen`は優れた型定義ファイルを生成するが、CLIとしての使い勝手に課題がある(特に複数アプリの一括生成ができない)。このツールは設定ファイルベースで複数アプリの型定義を一括生成する。
|
|
12
|
+
|
|
13
|
+
### 参考実装
|
|
14
|
+
|
|
15
|
+
[goqoo/src/generator/dts/index.ts](https://github.com/goqoo-on-kintone/goqoo/blob/main/src/generator/dts/index.ts) の機能を単独抽出したもの。
|
|
16
|
+
|
|
17
|
+
goqooでの処理フロー:
|
|
18
|
+
1. 設定ファイルから対象環境を特定
|
|
19
|
+
2. OAuth or ユーザー名/パスワード認証を設定
|
|
20
|
+
3. `appId`オブジェクトをループして各アプリに対し`npx kintone-dts-gen`を実行
|
|
21
|
+
4. 出力ファイル名はkebab-case、型名はPascalCaseで自動生成
|
|
22
|
+
|
|
23
|
+
### 設定ファイル構造
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
type Config = {
|
|
27
|
+
host: string // Kintone環境のホスト(例: "example.cybozu.com")
|
|
28
|
+
apps: Record<string, number> // { appName: appId }
|
|
29
|
+
auth:
|
|
30
|
+
| { type: 'password' } // 環境変数 KINTONE_USERNAME, KINTONE_PASSWORD
|
|
31
|
+
| { type: 'oauth'; scope?: string } // OAuth認証
|
|
32
|
+
| { type: 'api-token'; token: string } // APIトークン
|
|
33
|
+
proxy?: { host: string; port: number } // プロキシ設定(任意)
|
|
34
|
+
basicAuth?: { username: string; password: string } // Basic認証(任意)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 開発コマンド
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
yarn install # パッケージインストール
|
|
42
|
+
yarn build # TypeScriptビルド
|
|
43
|
+
yarn dev # ウォッチモードでビルド
|
|
44
|
+
yarn test # テスト実行(Jest + ESM)
|
|
45
|
+
yarn test:watch # ウォッチモードでテスト
|
|
46
|
+
yarn prettier --write . # コードフォーマット
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ソース構造
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/
|
|
53
|
+
├── cli.ts # CLIエントリーポイント(Commander.js)
|
|
54
|
+
├── config.ts # 設定ファイル読み込み(jiti使用)
|
|
55
|
+
├── generate.ts # dts-gen実行処理
|
|
56
|
+
├── index.ts # エクスポート
|
|
57
|
+
└── types.ts # 型定義・defineConfig
|
|
58
|
+
|
|
59
|
+
test/
|
|
60
|
+
├── config.test.ts # configのテスト
|
|
61
|
+
├── generate.test.ts # generateのテスト
|
|
62
|
+
└── types.test.ts # typesのテスト
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 技術スタック
|
|
66
|
+
|
|
67
|
+
- **ランタイム:** Node.js >= 20
|
|
68
|
+
- **パッケージマネージャー:** Yarn
|
|
69
|
+
- **主要依存:** @kintone/dts-gen, Commander.js, jiti, change-case, chalk
|
|
70
|
+
- **テスト:** Jest + ts-jest(ESMモード)
|
|
71
|
+
- **フォーマッタ:** Prettier(シングルクォート、120文字/行)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { generate } from './generate.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('trunks')
|
|
9
|
+
.description('Generate TypeScript type definitions for multiple Kintone apps')
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
program
|
|
12
|
+
.command('generate', { isDefault: true })
|
|
13
|
+
.description('Generate type definitions for all configured apps')
|
|
14
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
const config = await loadConfig(options.config ? undefined : process.cwd());
|
|
18
|
+
await generate(config);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
program.parse();
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { createJiti } from 'jiti';
|
|
4
|
+
const CONFIG_FILES = ['trunks.config.ts', 'trunks.config.js', 'trunks.config.mjs'];
|
|
5
|
+
// 設定ファイルを検索して読み込む
|
|
6
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
7
|
+
const jiti = createJiti(cwd, { interopDefault: true });
|
|
8
|
+
for (const filename of CONFIG_FILES) {
|
|
9
|
+
const configPath = resolve(cwd, filename);
|
|
10
|
+
if (existsSync(configPath)) {
|
|
11
|
+
const config = await jiti.import(configPath);
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Config file not found. Create one of: ${CONFIG_FILES.join(', ')}`);
|
|
16
|
+
}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { kebabCase, pascalCase } from 'change-case';
|
|
6
|
+
import { getOauthToken } from './oauth.js';
|
|
7
|
+
// 標準入力からテキストを取得
|
|
8
|
+
function prompt(question) {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// 標準入力からパスワードを取得(入力を隠す)
|
|
21
|
+
function promptPassword(question) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
process.stdout.write(question);
|
|
24
|
+
const stdin = process.stdin;
|
|
25
|
+
if (!stdin.isTTY) {
|
|
26
|
+
// TTYでない場合は通常の入力
|
|
27
|
+
const rl = readline.createInterface({ input: stdin, output: process.stdout });
|
|
28
|
+
rl.question('', (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer);
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// TTYの場合はraw modeで入力を隠す
|
|
35
|
+
stdin.setRawMode(true);
|
|
36
|
+
stdin.resume();
|
|
37
|
+
stdin.setEncoding('utf8');
|
|
38
|
+
let password = '';
|
|
39
|
+
const onData = (char) => {
|
|
40
|
+
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
41
|
+
// Enter or Ctrl+D
|
|
42
|
+
stdin.setRawMode(false);
|
|
43
|
+
stdin.removeListener('data', onData);
|
|
44
|
+
stdin.pause();
|
|
45
|
+
process.stdout.write('\n');
|
|
46
|
+
resolve(password);
|
|
47
|
+
}
|
|
48
|
+
else if (char === '\u0003') {
|
|
49
|
+
// Ctrl+C
|
|
50
|
+
stdin.setRawMode(false);
|
|
51
|
+
process.stdout.write('\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
else if (char === '\u007F' || char === '\b') {
|
|
55
|
+
// Backspace
|
|
56
|
+
password = password.slice(0, -1);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
password += char;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
stdin.on('data', onData);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// 認証引数を構築
|
|
66
|
+
async function buildAuthArgs(config) {
|
|
67
|
+
const args = {};
|
|
68
|
+
switch (config.auth.type) {
|
|
69
|
+
case 'password': {
|
|
70
|
+
let username = process.env.KINTONE_USERNAME;
|
|
71
|
+
let password = process.env.KINTONE_PASSWORD;
|
|
72
|
+
// 環境変数が未設定の場合は標準入力で取得
|
|
73
|
+
if (!username) {
|
|
74
|
+
username = await prompt('Kintone Username: ');
|
|
75
|
+
}
|
|
76
|
+
if (!password) {
|
|
77
|
+
password = await promptPassword('Kintone Password: ');
|
|
78
|
+
}
|
|
79
|
+
if (!username || !password) {
|
|
80
|
+
throw new Error('Username and password are required for password auth');
|
|
81
|
+
}
|
|
82
|
+
args['username'] = username;
|
|
83
|
+
args['password'] = password;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'oauth': {
|
|
87
|
+
// Gyumaを使ってOAuthトークンを取得
|
|
88
|
+
const agentOptions = {
|
|
89
|
+
proxy: config.proxy ? `http://${config.proxy.host}:${config.proxy.port}` : undefined,
|
|
90
|
+
pfx: config.pfx,
|
|
91
|
+
};
|
|
92
|
+
const oauthToken = await getOauthToken(config.host, config.auth.scope, agentOptions);
|
|
93
|
+
args['oauth-token'] = oauthToken;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'api-token':
|
|
97
|
+
args['api-token'] = config.auth.token;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
// Basic認証
|
|
101
|
+
if (config.basicAuth) {
|
|
102
|
+
args['basic-auth-username'] = config.basicAuth.username;
|
|
103
|
+
args['basic-auth-password'] = config.basicAuth.password;
|
|
104
|
+
}
|
|
105
|
+
// プロキシ
|
|
106
|
+
if (config.proxy) {
|
|
107
|
+
args['proxy'] = `http://${config.proxy.host}:${config.proxy.port}`;
|
|
108
|
+
}
|
|
109
|
+
return args;
|
|
110
|
+
}
|
|
111
|
+
// kintoneのエラーレスポンスを抽出
|
|
112
|
+
function extractKintoneError(output) {
|
|
113
|
+
// 形式: code: 'GAIA_AP15' または "code": "GAIA_AP15"
|
|
114
|
+
const codeMatch = output.match(/code:\s*'([^']+)'/) ?? output.match(/"code":\s*"([^"]+)"/);
|
|
115
|
+
const idMatch = output.match(/id:\s*'([^']+)'/) ?? output.match(/"id":\s*"([^"]+)"/);
|
|
116
|
+
const messageMatch = output.match(/message:\s*'([^']+)'/) ?? output.match(/"message":\s*"([^"]+)"/);
|
|
117
|
+
const code = codeMatch?.[1];
|
|
118
|
+
const id = idMatch?.[1];
|
|
119
|
+
const message = messageMatch?.[1];
|
|
120
|
+
if (code && message) {
|
|
121
|
+
return { code, id: id ?? '', message };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
// 単一アプリの型定義を生成
|
|
126
|
+
function generateForApp(appName, appId, config, authArgs, outDir) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const outputPath = `${outDir}/${kebabCase(appName)}-fields.d.ts`;
|
|
129
|
+
const args = {
|
|
130
|
+
'base-url': `https://${config.host}`,
|
|
131
|
+
...authArgs,
|
|
132
|
+
'type-name': `${pascalCase(appName)}Fields`,
|
|
133
|
+
'app-id': String(appId),
|
|
134
|
+
'output': outputPath,
|
|
135
|
+
'guest-space-id': config.guestSpaceId !== undefined ? String(config.guestSpaceId) : undefined,
|
|
136
|
+
'namespace': config.namespace,
|
|
137
|
+
};
|
|
138
|
+
// undefined値をフィルタリングして引数配列を作成
|
|
139
|
+
const cliArgs = Object.entries(args)
|
|
140
|
+
.filter(([, value]) => value !== undefined)
|
|
141
|
+
.map(([key, value]) => `--${key}=${value}`);
|
|
142
|
+
// プレビュー環境の場合は--previewフラグを追加
|
|
143
|
+
if (config.preview) {
|
|
144
|
+
cliArgs.push('--preview');
|
|
145
|
+
}
|
|
146
|
+
const proc = spawn('npx', ['kintone-dts-gen', ...cliArgs], {
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
149
|
+
});
|
|
150
|
+
let stdout = '';
|
|
151
|
+
let stderr = '';
|
|
152
|
+
proc.stdout?.on('data', (data) => {
|
|
153
|
+
stdout += data.toString();
|
|
154
|
+
});
|
|
155
|
+
proc.stderr?.on('data', (data) => {
|
|
156
|
+
stderr += data.toString();
|
|
157
|
+
});
|
|
158
|
+
proc.on('close', (code) => {
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
// stdoutとstderr両方からエラー情報を探す
|
|
161
|
+
const output = stdout + stderr;
|
|
162
|
+
const kintoneError = extractKintoneError(output);
|
|
163
|
+
if (kintoneError) {
|
|
164
|
+
console.error(chalk.red(`Error [${appName}]:`), kintoneError.message);
|
|
165
|
+
console.error(chalk.gray(` code: ${kintoneError.code}, id: ${kintoneError.id}`));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.error(chalk.red(`Error [${appName}]:`), `kintone-dts-gen exited with code ${code}`);
|
|
169
|
+
}
|
|
170
|
+
resolve({ success: false, output: outputPath });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.info(`${chalk.cyan('info')} ${chalk.magenta('Created')} ${chalk.green(outputPath)}`);
|
|
174
|
+
resolve({ success: true, output: outputPath });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
proc.on('error', (err) => {
|
|
178
|
+
console.error(chalk.red(`Error [${appName}]:`), err.message);
|
|
179
|
+
resolve({ success: false, output: outputPath });
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// 全アプリの型定義を生成
|
|
184
|
+
export async function generate(config) {
|
|
185
|
+
const outDir = config.outDir ?? 'dts';
|
|
186
|
+
mkdirSync(outDir, { recursive: true });
|
|
187
|
+
const authArgs = await buildAuthArgs(config);
|
|
188
|
+
const apps = Object.entries(config.apps);
|
|
189
|
+
console.info(chalk.cyan(`Generating type definitions for ${apps.length} app(s)...`));
|
|
190
|
+
// 順次実行(並列だとコンソール出力が混在する)
|
|
191
|
+
const results = [];
|
|
192
|
+
for (const [appName, appId] of apps) {
|
|
193
|
+
const result = await generateForApp(appName, appId, config, authArgs, outDir);
|
|
194
|
+
results.push(result);
|
|
195
|
+
}
|
|
196
|
+
const successCount = results.filter((r) => r.success).length;
|
|
197
|
+
const failCount = results.length - successCount;
|
|
198
|
+
if (failCount > 0) {
|
|
199
|
+
console.info(chalk.yellow(`\nCompleted with ${failCount} error(s). (${successCount}/${results.length} succeeded)`));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.info(chalk.green('Done!'));
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineConfig } from './types.js';
|
|
2
|
+
export type { Config, Auth, PasswordAuth, OAuthAuth, ApiTokenAuth, ProxyConfig, BasicAuthConfig, PfxConfig, AgentOptions, } from './types.js';
|
|
3
|
+
export { loadConfig } from './config.js';
|
|
4
|
+
export { generate } from './generate.js';
|
package/dist/index.js
ADDED
package/dist/oauth.d.ts
ADDED
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { gyuma } from 'gyuma';
|
|
2
|
+
// dts-genはフィールド情報を読み取るので k:app_settings:read が必要
|
|
3
|
+
const DEFAULT_SCOPE = 'k:app_settings:read';
|
|
4
|
+
export const getOauthToken = async (domain, scope, agentOptions) => {
|
|
5
|
+
const token = await gyuma({ domain, scope: scope ?? DEFAULT_SCOPE, ...agentOptions }, true);
|
|
6
|
+
return token;
|
|
7
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type PasswordAuth = {
|
|
2
|
+
type: 'password';
|
|
3
|
+
};
|
|
4
|
+
export type OAuthAuth = {
|
|
5
|
+
type: 'oauth';
|
|
6
|
+
scope?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ApiTokenAuth = {
|
|
9
|
+
type: 'api-token';
|
|
10
|
+
token: string;
|
|
11
|
+
};
|
|
12
|
+
export type Auth = PasswordAuth | OAuthAuth | ApiTokenAuth;
|
|
13
|
+
export type ProxyConfig = {
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
};
|
|
17
|
+
export type BasicAuthConfig = {
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
};
|
|
21
|
+
export type PfxConfig = {
|
|
22
|
+
filepath: string;
|
|
23
|
+
password: string;
|
|
24
|
+
};
|
|
25
|
+
export type AgentOptions = {
|
|
26
|
+
proxy?: string;
|
|
27
|
+
pfx?: PfxConfig;
|
|
28
|
+
};
|
|
29
|
+
export type Config = {
|
|
30
|
+
host: string;
|
|
31
|
+
apps: Record<string, number>;
|
|
32
|
+
auth: Auth;
|
|
33
|
+
proxy?: ProxyConfig;
|
|
34
|
+
basicAuth?: BasicAuthConfig;
|
|
35
|
+
pfx?: PfxConfig;
|
|
36
|
+
outDir?: string;
|
|
37
|
+
preview?: boolean;
|
|
38
|
+
guestSpaceId?: number;
|
|
39
|
+
namespace?: string;
|
|
40
|
+
};
|
|
41
|
+
export declare const defineConfig: (config: Config) => Config;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare namespace kintone.types {
|
|
2
|
+
interface ActivityFields {
|
|
3
|
+
案件No: kintone.fieldTypes.Number;
|
|
4
|
+
顧客No: kintone.fieldTypes.Number;
|
|
5
|
+
タイトル: kintone.fieldTypes.SingleLineText;
|
|
6
|
+
案件名: kintone.fieldTypes.SingleLineText;
|
|
7
|
+
対応日付: kintone.fieldTypes.Date;
|
|
8
|
+
対応種別: kintone.fieldTypes.DropDown;
|
|
9
|
+
内容: kintone.fieldTypes.MultiLineText;
|
|
10
|
+
会社名: kintone.fieldTypes.SingleLineText;
|
|
11
|
+
|
|
12
|
+
対応者: kintone.fieldTypes.UserSelect;
|
|
13
|
+
所属組織: kintone.fieldTypes.OrganizationSelect;
|
|
14
|
+
添付ファイル: kintone.fieldTypes.File;
|
|
15
|
+
}
|
|
16
|
+
interface SavedActivityFields extends ActivityFields {
|
|
17
|
+
$id: kintone.fieldTypes.Id;
|
|
18
|
+
$revision: kintone.fieldTypes.Revision;
|
|
19
|
+
更新者: kintone.fieldTypes.Modifier;
|
|
20
|
+
作成者: kintone.fieldTypes.Creator;
|
|
21
|
+
レコード番号: kintone.fieldTypes.RecordNumber;
|
|
22
|
+
更新日時: kintone.fieldTypes.UpdatedTime;
|
|
23
|
+
作成日時: kintone.fieldTypes.CreatedTime;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare namespace kintone.types {
|
|
2
|
+
interface CustomerFields {
|
|
3
|
+
顧客情報メモ欄: kintone.fieldTypes.MultiLineText;
|
|
4
|
+
文字列__1行_: kintone.fieldTypes.SingleLineText;
|
|
5
|
+
Webサイト: kintone.fieldTypes.Link;
|
|
6
|
+
建物名: kintone.fieldTypes.SingleLineText;
|
|
7
|
+
顧客ランク: kintone.fieldTypes.RadioButton;
|
|
8
|
+
支払日: kintone.fieldTypes.DropDown;
|
|
9
|
+
住所: kintone.fieldTypes.SingleLineText;
|
|
10
|
+
電話番号: kintone.fieldTypes.Link;
|
|
11
|
+
会社名: kintone.fieldTypes.SingleLineText;
|
|
12
|
+
郵便番号: kintone.fieldTypes.SingleLineText;
|
|
13
|
+
業種: kintone.fieldTypes.DropDown;
|
|
14
|
+
文字列__1行__0: kintone.fieldTypes.SingleLineText;
|
|
15
|
+
都道府県: kintone.fieldTypes.DropDown;
|
|
16
|
+
締め日: kintone.fieldTypes.DropDown;
|
|
17
|
+
FAX: kintone.fieldTypes.Link;
|
|
18
|
+
}
|
|
19
|
+
interface SavedCustomerFields extends CustomerFields {
|
|
20
|
+
$id: kintone.fieldTypes.Id;
|
|
21
|
+
$revision: kintone.fieldTypes.Revision;
|
|
22
|
+
更新者: kintone.fieldTypes.Modifier;
|
|
23
|
+
作成者: kintone.fieldTypes.Creator;
|
|
24
|
+
顧客No: kintone.fieldTypes.RecordNumber;
|
|
25
|
+
更新日時: kintone.fieldTypes.UpdatedTime;
|
|
26
|
+
作成日時: kintone.fieldTypes.CreatedTime;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
declare namespace kintone.types {
|
|
2
|
+
interface ProjectFields {
|
|
3
|
+
提案商品: kintone.fieldTypes.DropDown;
|
|
4
|
+
アクション内容: kintone.fieldTypes.SingleLineText;
|
|
5
|
+
詳細: kintone.fieldTypes.MultiLineText;
|
|
6
|
+
案件名: kintone.fieldTypes.SingleLineText;
|
|
7
|
+
売上: kintone.fieldTypes.Number;
|
|
8
|
+
会社名: kintone.fieldTypes.SingleLineText;
|
|
9
|
+
商談フェーズ: kintone.fieldTypes.DropDown;
|
|
10
|
+
次回アクション日: kintone.fieldTypes.Date;
|
|
11
|
+
確度: kintone.fieldTypes.DropDown;
|
|
12
|
+
受注予定日: kintone.fieldTypes.Date;
|
|
13
|
+
顧客No_: kintone.fieldTypes.Number;
|
|
14
|
+
初回商談日: kintone.fieldTypes.Date;
|
|
15
|
+
|
|
16
|
+
アクション担当者: kintone.fieldTypes.UserSelect;
|
|
17
|
+
主担当: kintone.fieldTypes.UserSelect;
|
|
18
|
+
主担当組織: kintone.fieldTypes.OrganizationSelect;
|
|
19
|
+
契約書_申込書: kintone.fieldTypes.File;
|
|
20
|
+
}
|
|
21
|
+
interface SavedProjectFields extends ProjectFields {
|
|
22
|
+
$id: kintone.fieldTypes.Id;
|
|
23
|
+
$revision: kintone.fieldTypes.Revision;
|
|
24
|
+
更新者: kintone.fieldTypes.Modifier;
|
|
25
|
+
作成者: kintone.fieldTypes.Creator;
|
|
26
|
+
案件No_: kintone.fieldTypes.RecordNumber;
|
|
27
|
+
更新日時: kintone.fieldTypes.UpdatedTime;
|
|
28
|
+
作成日時: kintone.fieldTypes.CreatedTime;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
2
|
+
export default {
|
|
3
|
+
preset: 'ts-jest/presets/default-esm',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
6
|
+
moduleNameMapper: {
|
|
7
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
8
|
+
},
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.ts$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
useESM: true,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
testMatch: ['<rootDir>/test/**/*.test.ts'],
|
|
18
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/cli.ts'],
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goqoo/trunks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI wrapper for @kintone/dts-gen that generates type definitions for multiple Kintone apps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"trunks": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
14
|
+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"kintone",
|
|
18
|
+
"typescript",
|
|
19
|
+
"dts-gen",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/jest": "^30.0.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"jest": "^30.2.0",
|
|
31
|
+
"prettier": "^3.8.1",
|
|
32
|
+
"ts-jest": "^29.4.6",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@kintone/dts-gen": "^9.0.2",
|
|
37
|
+
"chalk": "^5.4.0",
|
|
38
|
+
"change-case": "^5.4.0",
|
|
39
|
+
"commander": "^13.0.0",
|
|
40
|
+
"gyuma": "^0.6.1",
|
|
41
|
+
"jiti": "^2.4.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { generate } from './generate.js';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('trunks')
|
|
11
|
+
.description('Generate TypeScript type definitions for multiple Kintone apps')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('generate', { isDefault: true })
|
|
16
|
+
.description('Generate type definitions for all configured apps')
|
|
17
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const config = await loadConfig(options.config ? undefined : process.cwd());
|
|
21
|
+
await generate(config);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
program.parse();
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { createJiti } from 'jiti';
|
|
4
|
+
import type { Config } from './types.js';
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILES = ['trunks.config.ts', 'trunks.config.js', 'trunks.config.mjs'];
|
|
7
|
+
|
|
8
|
+
// 設定ファイルを検索して読み込む
|
|
9
|
+
export async function loadConfig(cwd: string = process.cwd()): Promise<Config> {
|
|
10
|
+
const jiti = createJiti(cwd, { interopDefault: true });
|
|
11
|
+
|
|
12
|
+
for (const filename of CONFIG_FILES) {
|
|
13
|
+
const configPath = resolve(cwd, filename);
|
|
14
|
+
if (existsSync(configPath)) {
|
|
15
|
+
const config = await jiti.import(configPath);
|
|
16
|
+
return config as Config;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Config file not found. Create one of: ${CONFIG_FILES.join(', ')}`
|
|
22
|
+
);
|
|
23
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { kebabCase, pascalCase } from 'change-case';
|
|
6
|
+
import type { AgentOptions, Config } from './types.js';
|
|
7
|
+
import { getOauthToken } from './oauth.js';
|
|
8
|
+
|
|
9
|
+
type DtsGenArgs = Record<string, string | undefined>;
|
|
10
|
+
|
|
11
|
+
// 標準入力からテキストを取得
|
|
12
|
+
function prompt(question: string): Promise<string> {
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
rl.question(question, (answer) => {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(answer);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 標準入力からパスワードを取得(入力を隠す)
|
|
27
|
+
function promptPassword(question: string): Promise<string> {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
process.stdout.write(question);
|
|
30
|
+
|
|
31
|
+
const stdin = process.stdin;
|
|
32
|
+
if (!stdin.isTTY) {
|
|
33
|
+
// TTYでない場合は通常の入力
|
|
34
|
+
const rl = readline.createInterface({ input: stdin, output: process.stdout });
|
|
35
|
+
rl.question('', (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
resolve(answer);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TTYの場合はraw modeで入力を隠す
|
|
43
|
+
stdin.setRawMode(true);
|
|
44
|
+
stdin.resume();
|
|
45
|
+
stdin.setEncoding('utf8');
|
|
46
|
+
|
|
47
|
+
let password = '';
|
|
48
|
+
const onData = (char: string) => {
|
|
49
|
+
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
50
|
+
// Enter or Ctrl+D
|
|
51
|
+
stdin.setRawMode(false);
|
|
52
|
+
stdin.removeListener('data', onData);
|
|
53
|
+
stdin.pause();
|
|
54
|
+
process.stdout.write('\n');
|
|
55
|
+
resolve(password);
|
|
56
|
+
} else if (char === '\u0003') {
|
|
57
|
+
// Ctrl+C
|
|
58
|
+
stdin.setRawMode(false);
|
|
59
|
+
process.stdout.write('\n');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
} else if (char === '\u007F' || char === '\b') {
|
|
62
|
+
// Backspace
|
|
63
|
+
password = password.slice(0, -1);
|
|
64
|
+
} else {
|
|
65
|
+
password += char;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
stdin.on('data', onData);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 認証引数を構築
|
|
74
|
+
async function buildAuthArgs(config: Config): Promise<DtsGenArgs> {
|
|
75
|
+
const args: DtsGenArgs = {};
|
|
76
|
+
|
|
77
|
+
switch (config.auth.type) {
|
|
78
|
+
case 'password': {
|
|
79
|
+
let username = process.env.KINTONE_USERNAME;
|
|
80
|
+
let password = process.env.KINTONE_PASSWORD;
|
|
81
|
+
|
|
82
|
+
// 環境変数が未設定の場合は標準入力で取得
|
|
83
|
+
if (!username) {
|
|
84
|
+
username = await prompt('Kintone Username: ');
|
|
85
|
+
}
|
|
86
|
+
if (!password) {
|
|
87
|
+
password = await promptPassword('Kintone Password: ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!username || !password) {
|
|
91
|
+
throw new Error('Username and password are required for password auth');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
args['username'] = username;
|
|
95
|
+
args['password'] = password;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'oauth': {
|
|
99
|
+
// Gyumaを使ってOAuthトークンを取得
|
|
100
|
+
const agentOptions: AgentOptions = {
|
|
101
|
+
proxy: config.proxy ? `http://${config.proxy.host}:${config.proxy.port}` : undefined,
|
|
102
|
+
pfx: config.pfx,
|
|
103
|
+
};
|
|
104
|
+
const oauthToken = await getOauthToken(config.host, config.auth.scope, agentOptions);
|
|
105
|
+
args['oauth-token'] = oauthToken;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case 'api-token':
|
|
109
|
+
args['api-token'] = config.auth.token;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Basic認証
|
|
114
|
+
if (config.basicAuth) {
|
|
115
|
+
args['basic-auth-username'] = config.basicAuth.username;
|
|
116
|
+
args['basic-auth-password'] = config.basicAuth.password;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// プロキシ
|
|
120
|
+
if (config.proxy) {
|
|
121
|
+
args['proxy'] = `http://${config.proxy.host}:${config.proxy.port}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return args;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// kintoneのエラーレスポンスを抽出
|
|
128
|
+
function extractKintoneError(output: string): { code: string; id: string; message: string } | null {
|
|
129
|
+
// 形式: code: 'GAIA_AP15' または "code": "GAIA_AP15"
|
|
130
|
+
const codeMatch = output.match(/code:\s*'([^']+)'/) ?? output.match(/"code":\s*"([^"]+)"/);
|
|
131
|
+
const idMatch = output.match(/id:\s*'([^']+)'/) ?? output.match(/"id":\s*"([^"]+)"/);
|
|
132
|
+
const messageMatch = output.match(/message:\s*'([^']+)'/) ?? output.match(/"message":\s*"([^"]+)"/);
|
|
133
|
+
|
|
134
|
+
const code = codeMatch?.[1];
|
|
135
|
+
const id = idMatch?.[1];
|
|
136
|
+
const message = messageMatch?.[1];
|
|
137
|
+
|
|
138
|
+
if (code && message) {
|
|
139
|
+
return { code, id: id ?? '', message };
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 単一アプリの型定義を生成
|
|
145
|
+
function generateForApp(
|
|
146
|
+
appName: string,
|
|
147
|
+
appId: number,
|
|
148
|
+
config: Config,
|
|
149
|
+
authArgs: DtsGenArgs,
|
|
150
|
+
outDir: string
|
|
151
|
+
): Promise<{ success: boolean; output: string }> {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const outputPath = `${outDir}/${kebabCase(appName)}-fields.d.ts`;
|
|
154
|
+
const args: DtsGenArgs = {
|
|
155
|
+
'base-url': `https://${config.host}`,
|
|
156
|
+
...authArgs,
|
|
157
|
+
'type-name': `${pascalCase(appName)}Fields`,
|
|
158
|
+
'app-id': String(appId),
|
|
159
|
+
'output': outputPath,
|
|
160
|
+
'guest-space-id': config.guestSpaceId !== undefined ? String(config.guestSpaceId) : undefined,
|
|
161
|
+
'namespace': config.namespace,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// undefined値をフィルタリングして引数配列を作成
|
|
165
|
+
const cliArgs = Object.entries(args)
|
|
166
|
+
.filter(([, value]) => value !== undefined)
|
|
167
|
+
.map(([key, value]) => `--${key}=${value}`);
|
|
168
|
+
|
|
169
|
+
// プレビュー環境の場合は--previewフラグを追加
|
|
170
|
+
if (config.preview) {
|
|
171
|
+
cliArgs.push('--preview');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const proc = spawn('npx', ['kintone-dts-gen', ...cliArgs], {
|
|
175
|
+
cwd: process.cwd(),
|
|
176
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
let stdout = '';
|
|
180
|
+
let stderr = '';
|
|
181
|
+
proc.stdout?.on('data', (data) => {
|
|
182
|
+
stdout += data.toString();
|
|
183
|
+
});
|
|
184
|
+
proc.stderr?.on('data', (data) => {
|
|
185
|
+
stderr += data.toString();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
proc.on('close', (code) => {
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
// stdoutとstderr両方からエラー情報を探す
|
|
191
|
+
const output = stdout + stderr;
|
|
192
|
+
const kintoneError = extractKintoneError(output);
|
|
193
|
+
if (kintoneError) {
|
|
194
|
+
console.error(chalk.red(`Error [${appName}]:`), kintoneError.message);
|
|
195
|
+
console.error(chalk.gray(` code: ${kintoneError.code}, id: ${kintoneError.id}`));
|
|
196
|
+
} else {
|
|
197
|
+
console.error(chalk.red(`Error [${appName}]:`), `kintone-dts-gen exited with code ${code}`);
|
|
198
|
+
}
|
|
199
|
+
resolve({ success: false, output: outputPath });
|
|
200
|
+
} else {
|
|
201
|
+
console.info(`${chalk.cyan('info')} ${chalk.magenta('Created')} ${chalk.green(outputPath)}`);
|
|
202
|
+
resolve({ success: true, output: outputPath });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
proc.on('error', (err) => {
|
|
207
|
+
console.error(chalk.red(`Error [${appName}]:`), err.message);
|
|
208
|
+
resolve({ success: false, output: outputPath });
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 全アプリの型定義を生成
|
|
214
|
+
export async function generate(config: Config): Promise<void> {
|
|
215
|
+
const outDir = config.outDir ?? 'dts';
|
|
216
|
+
mkdirSync(outDir, { recursive: true });
|
|
217
|
+
|
|
218
|
+
const authArgs = await buildAuthArgs(config);
|
|
219
|
+
const apps = Object.entries(config.apps);
|
|
220
|
+
|
|
221
|
+
console.info(chalk.cyan(`Generating type definitions for ${apps.length} app(s)...`));
|
|
222
|
+
|
|
223
|
+
// 順次実行(並列だとコンソール出力が混在する)
|
|
224
|
+
const results: { success: boolean; output: string }[] = [];
|
|
225
|
+
for (const [appName, appId] of apps) {
|
|
226
|
+
const result = await generateForApp(appName, appId, config, authArgs, outDir);
|
|
227
|
+
results.push(result);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const successCount = results.filter((r) => r.success).length;
|
|
231
|
+
const failCount = results.length - successCount;
|
|
232
|
+
|
|
233
|
+
if (failCount > 0) {
|
|
234
|
+
console.info(chalk.yellow(`\nCompleted with ${failCount} error(s). (${successCount}/${results.length} succeeded)`));
|
|
235
|
+
} else {
|
|
236
|
+
console.info(chalk.green('Done!'));
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { defineConfig } from './types.js';
|
|
2
|
+
export type {
|
|
3
|
+
Config,
|
|
4
|
+
Auth,
|
|
5
|
+
PasswordAuth,
|
|
6
|
+
OAuthAuth,
|
|
7
|
+
ApiTokenAuth,
|
|
8
|
+
ProxyConfig,
|
|
9
|
+
BasicAuthConfig,
|
|
10
|
+
PfxConfig,
|
|
11
|
+
AgentOptions,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
export { loadConfig } from './config.js';
|
|
14
|
+
export { generate } from './generate.js';
|
package/src/oauth.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { gyuma } from 'gyuma';
|
|
2
|
+
import type { AgentOptions } from './types.js';
|
|
3
|
+
|
|
4
|
+
// dts-genはフィールド情報を読み取るので k:app_settings:read が必要
|
|
5
|
+
const DEFAULT_SCOPE = 'k:app_settings:read';
|
|
6
|
+
|
|
7
|
+
export const getOauthToken = async (
|
|
8
|
+
domain: string,
|
|
9
|
+
scope: string | undefined,
|
|
10
|
+
agentOptions: AgentOptions
|
|
11
|
+
): Promise<string> => {
|
|
12
|
+
const token = await gyuma({ domain, scope: scope ?? DEFAULT_SCOPE, ...agentOptions }, true);
|
|
13
|
+
return token;
|
|
14
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// 認証設定
|
|
2
|
+
export type PasswordAuth = {
|
|
3
|
+
type: 'password';
|
|
4
|
+
// 環境変数 KINTONE_USERNAME, KINTONE_PASSWORD から取得
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type OAuthAuth = {
|
|
8
|
+
type: 'oauth';
|
|
9
|
+
scope?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ApiTokenAuth = {
|
|
13
|
+
type: 'api-token';
|
|
14
|
+
token: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Auth = PasswordAuth | OAuthAuth | ApiTokenAuth;
|
|
18
|
+
|
|
19
|
+
// プロキシ設定
|
|
20
|
+
export type ProxyConfig = {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Basic認証設定
|
|
26
|
+
export type BasicAuthConfig = {
|
|
27
|
+
username: string;
|
|
28
|
+
password: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// クライアント証明書設定(PFX/PKCS#12形式)
|
|
32
|
+
export type PfxConfig = {
|
|
33
|
+
filepath: string;
|
|
34
|
+
password: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Gyuma用のエージェントオプション
|
|
38
|
+
export type AgentOptions = {
|
|
39
|
+
proxy?: string;
|
|
40
|
+
pfx?: PfxConfig;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// メイン設定
|
|
44
|
+
export type Config = {
|
|
45
|
+
host: string; // Kintone環境のホスト(例: "example.cybozu.com")
|
|
46
|
+
apps: Record<string, number>; // { appName: appId }
|
|
47
|
+
auth: Auth;
|
|
48
|
+
proxy?: ProxyConfig;
|
|
49
|
+
basicAuth?: BasicAuthConfig;
|
|
50
|
+
// クライアント証明書(OAuth使用時にGyumaへ渡す)
|
|
51
|
+
pfx?: PfxConfig;
|
|
52
|
+
// 出力ディレクトリ(デフォルト: "dts")
|
|
53
|
+
outDir?: string;
|
|
54
|
+
// プレビュー環境を参照する場合はtrue(デフォルト: false)
|
|
55
|
+
preview?: boolean;
|
|
56
|
+
// ゲストスペースID(ゲストスペース内のアプリの場合に指定)
|
|
57
|
+
guestSpaceId?: number;
|
|
58
|
+
// 生成する型のnamespace(デフォルト: "kintone.types")
|
|
59
|
+
namespace?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 設定ファイルの型(defineConfigのため)
|
|
63
|
+
export const defineConfig = (config: Config): Config => config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { loadConfig } from '../src/config';
|
|
5
|
+
|
|
6
|
+
describe('loadConfig', () => {
|
|
7
|
+
it('設定ファイルが見つからない場合はエラーをスローする', async () => {
|
|
8
|
+
// 存在しないディレクトリを指定
|
|
9
|
+
await expect(loadConfig('/non-existent-dir-12345')).rejects.toThrow('Config file not found');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('エラーメッセージに設定ファイル名の候補を含む', async () => {
|
|
13
|
+
await expect(loadConfig('/non-existent-dir-12345')).rejects.toThrow('trunks.config.ts');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { generate } from '../src/generate';
|
|
3
|
+
import type { Config } from '../src/types';
|
|
4
|
+
|
|
5
|
+
describe('generate', () => {
|
|
6
|
+
const originalEnv = process.env;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env = { ...originalEnv };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
process.env = originalEnv;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// NOTE: パスワード認証で環境変数が未設定の場合は標準入力を求めるため、
|
|
17
|
+
// 自動テストでは検証が困難。手動テストで確認する。
|
|
18
|
+
|
|
19
|
+
// NOTE: OAuth認証はGyumaがブラウザを開いて認証フローを実行するため、
|
|
20
|
+
// 自動テストでは検証が困難。手動テストで確認する。
|
|
21
|
+
|
|
22
|
+
it('appsが空の場合でもエラーにならない', async () => {
|
|
23
|
+
const config: Config = {
|
|
24
|
+
host: 'example.cybozu.com',
|
|
25
|
+
apps: {},
|
|
26
|
+
auth: { type: 'api-token', token: 'test' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// appsが空なのでspawnは呼ばれず、正常終了するはず
|
|
30
|
+
await expect(generate(config)).resolves.toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { defineConfig } from '../src/types';
|
|
3
|
+
import type { Config } from '../src/types';
|
|
4
|
+
|
|
5
|
+
describe('defineConfig', () => {
|
|
6
|
+
it('パスワード認証の設定を返す', () => {
|
|
7
|
+
const config = defineConfig({
|
|
8
|
+
host: 'example.cybozu.com',
|
|
9
|
+
apps: { customer: 1, order: 2 },
|
|
10
|
+
auth: { type: 'password' },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(config.host).toBe('example.cybozu.com');
|
|
14
|
+
expect(config.apps).toEqual({ customer: 1, order: 2 });
|
|
15
|
+
expect(config.auth.type).toBe('password');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('APIトークン認証の設定を返す', () => {
|
|
19
|
+
const config = defineConfig({
|
|
20
|
+
host: 'example.cybozu.com',
|
|
21
|
+
apps: { customer: 1 },
|
|
22
|
+
auth: { type: 'api-token', token: 'test-token' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(config.auth.type).toBe('api-token');
|
|
26
|
+
if (config.auth.type === 'api-token') {
|
|
27
|
+
expect(config.auth.token).toBe('test-token');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('OAuth認証の設定を返す', () => {
|
|
32
|
+
const config = defineConfig({
|
|
33
|
+
host: 'example.cybozu.com',
|
|
34
|
+
apps: { customer: 1 },
|
|
35
|
+
auth: { type: 'oauth', scope: 'k:app_record:read' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(config.auth.type).toBe('oauth');
|
|
39
|
+
if (config.auth.type === 'oauth') {
|
|
40
|
+
expect(config.auth.scope).toBe('k:app_record:read');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('オプション設定を含む設定を返す', () => {
|
|
45
|
+
const config = defineConfig({
|
|
46
|
+
host: 'example.cybozu.com',
|
|
47
|
+
apps: { customer: 1 },
|
|
48
|
+
auth: { type: 'password' },
|
|
49
|
+
proxy: { host: 'proxy.example.com', port: 8080 },
|
|
50
|
+
basicAuth: { username: 'user', password: 'pass' },
|
|
51
|
+
outDir: 'types',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(config.proxy).toEqual({ host: 'proxy.example.com', port: 8080 });
|
|
55
|
+
expect(config.basicAuth).toEqual({ username: 'user', password: 'pass' });
|
|
56
|
+
expect(config.outDir).toBe('types');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('プレビュー環境の設定を返す', () => {
|
|
60
|
+
const config = defineConfig({
|
|
61
|
+
host: 'example.cybozu.com',
|
|
62
|
+
apps: { customer: 1 },
|
|
63
|
+
auth: { type: 'api-token', token: 'test-token' },
|
|
64
|
+
preview: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(config.preview).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('ゲストスペースの設定を返す', () => {
|
|
71
|
+
const config = defineConfig({
|
|
72
|
+
host: 'example.cybozu.com',
|
|
73
|
+
apps: { customer: 1 },
|
|
74
|
+
auth: { type: 'api-token', token: 'test-token' },
|
|
75
|
+
guestSpaceId: 5,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(config.guestSpaceId).toBe(5);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('namespaceの設定を返す', () => {
|
|
82
|
+
const config = defineConfig({
|
|
83
|
+
host: 'example.cybozu.com',
|
|
84
|
+
apps: { customer: 1 },
|
|
85
|
+
auth: { type: 'api-token', token: 'test-token' },
|
|
86
|
+
namespace: 'myapp.types',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(config.namespace).toBe('myapp.types');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('型チェックが正しく機能する', () => {
|
|
93
|
+
// 型レベルでのテスト(コンパイルが通ればOK)
|
|
94
|
+
const passwordConfig: Config = {
|
|
95
|
+
host: 'example.cybozu.com',
|
|
96
|
+
apps: { app1: 1 },
|
|
97
|
+
auth: { type: 'password' },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const apiTokenConfig: Config = {
|
|
101
|
+
host: 'example.cybozu.com',
|
|
102
|
+
apps: { app1: 1 },
|
|
103
|
+
auth: { type: 'api-token', token: 'xxx' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const oauthConfig: Config = {
|
|
107
|
+
host: 'example.cybozu.com',
|
|
108
|
+
apps: { app1: 1 },
|
|
109
|
+
auth: { type: 'oauth' },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(passwordConfig).toBeDefined();
|
|
113
|
+
expect(apiTokenConfig).toBeDefined();
|
|
114
|
+
expect(oauthConfig).toBeDefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
package/trunks.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from './src/types'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
host: 'the-red.cybozu.com',
|
|
5
|
+
preview: false,
|
|
6
|
+
apps: {
|
|
7
|
+
activity: 265,
|
|
8
|
+
customer: 266,
|
|
9
|
+
// project: 267,
|
|
10
|
+
},
|
|
11
|
+
// 以下のいずれかの認証方式を選択
|
|
12
|
+
auth: { type: 'oauth' },
|
|
13
|
+
// または
|
|
14
|
+
// auth: {
|
|
15
|
+
// type: 'api-token',
|
|
16
|
+
// token: '6mrxShdoQdFdN943q9Wg50nr7LxdwMsy1TTFkttF,K6t3rpw8PGoKQm7UPtiG0lyZawUyK0SPyjy4x7wJ',
|
|
17
|
+
// }, // 環境変数 KINTONE_USERNAME, KINTONE_PASSWORD を使用
|
|
18
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|