fieldsmith 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +843 -0
  3. package/assets/fonts/DotGothic16.ttf +0 -0
  4. package/assets/fonts/OFL.txt +93 -0
  5. package/dist/cli/exit.js +37 -0
  6. package/dist/cli/exit.js.map +1 -0
  7. package/dist/cli/index.js +699 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/output.js +61 -0
  10. package/dist/cli/output.js.map +1 -0
  11. package/dist/cli/progress.js +83 -0
  12. package/dist/cli/progress.js.map +1 -0
  13. package/dist/cli/promptInput.js +57 -0
  14. package/dist/cli/promptInput.js.map +1 -0
  15. package/dist/cli/schema.js +140 -0
  16. package/dist/cli/schema.js.map +1 -0
  17. package/dist/config.js +149 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/icon/emojiFont.js +163 -0
  20. package/dist/icon/emojiFont.js.map +1 -0
  21. package/dist/icon/render.js +136 -0
  22. package/dist/icon/render.js.map +1 -0
  23. package/dist/kintone/client.js +157 -0
  24. package/dist/kintone/client.js.map +1 -0
  25. package/dist/kintone/deploy.js +519 -0
  26. package/dist/kintone/deploy.js.map +1 -0
  27. package/dist/kintone/oauth.js +156 -0
  28. package/dist/kintone/oauth.js.map +1 -0
  29. package/dist/kintone/tokenStore.js +87 -0
  30. package/dist/kintone/tokenStore.js.map +1 -0
  31. package/dist/llm/generate.js +317 -0
  32. package/dist/llm/generate.js.map +1 -0
  33. package/dist/llm/prompt.js +142 -0
  34. package/dist/llm/prompt.js.map +1 -0
  35. package/dist/spec/appSpec.js +283 -0
  36. package/dist/spec/appSpec.js.map +1 -0
  37. package/dist/spec/diff.js +218 -0
  38. package/dist/spec/diff.js.map +1 -0
  39. package/dist/spec/fieldSpec.js +353 -0
  40. package/dist/spec/fieldSpec.js.map +1 -0
  41. package/dist/spec/fromKintone.js +230 -0
  42. package/dist/spec/fromKintone.js.map +1 -0
  43. package/dist/spec/layout.js +215 -0
  44. package/dist/spec/layout.js.map +1 -0
  45. package/dist/spec/toKintone.js +170 -0
  46. package/dist/spec/toKintone.js.map +1 -0
  47. package/package.json +59 -0
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { createInterface } from "node:readline/promises";
4
+ import Anthropic from "@anthropic-ai/sdk";
5
+ import { KintoneRestAPIError } from "@kintone/rest-api-client";
6
+ import { Command } from "commander";
7
+ import { ConfigError, loadDotEnv, loadKintoneConfig, requireOAuth, } from "../config.js";
8
+ import { createAuthenticatedKintone, KintoneRequestError } from "../kintone/client.js";
9
+ import { DeployError, deployAppSpec, pullApp, UnsupportedUpdateError, updateApp, } from "../kintone/deploy.js";
10
+ import { buildAuthorizationRequest, exchangeAuthorizationCode, extractAuthorizationCode, OAuthError, REQUIRED_SCOPE, } from "../kintone/oauth.js";
11
+ import { clearToken, saveToken } from "../kintone/tokenStore.js";
12
+ import { AppSpecGenerationError, DEFAULT_TIMEOUT_MS, generateAppSpec, } from "../llm/generate.js";
13
+ import { AppSpecValidationError, fieldGroups, parseAppSpec, resolveFieldCode, resolveLayout, } from "../spec/appSpec.js";
14
+ import { describeDiff, diffAppSpec, isEmptyDiff } from "../spec/diff.js";
15
+ import { describeRows, groupIntoRows } from "../spec/layout.js";
16
+ import { toKintonePayloads } from "../spec/toKintone.js";
17
+ import { backgroundFor, renderIcon } from "../icon/render.js";
18
+ import { emitFailure, emitSuccess, isJsonMode, say, setJsonMode } from "./output.js";
19
+ import { appSpecExample, appSpecJsonSchema, appSpecReference } from "./schema.js";
20
+ import { startStatusLine, tailLine } from "./progress.js";
21
+ import { LARGE_PROMPT_CHARS, PromptInputError, resolvePrompt } from "./promptInput.js";
22
+ const program = new Command();
23
+ program
24
+ .name("fieldsmith")
25
+ .description("AppSpec (JSON) から kintone アプリを何度でも作る CLI。AppSpec は要件の文章からも作れる。")
26
+ .version("0.1.0")
27
+ .option("-v, --verbose", "何をしているかを詳しく表示する (思考の要約、API 呼び出し、revision の遷移)")
28
+ .option("--json", "結果を JSON で stdout に出す。人間向けの表示は stderr に回る")
29
+ .hook("preAction", () => setJsonMode(program.opts()["json"] === true));
30
+ /** --verbose はどのサブコマンドでも同じ意味なので、トップレベルの指定を見る。 */
31
+ function isVerbose() {
32
+ return program.opts()["verbose"] === true;
33
+ }
34
+ /** --verbose のときだけ出す補足行。進捗表示と混ざらないよう stderr に出す。 */
35
+ function trace(message) {
36
+ if (isVerbose()) {
37
+ process.stderr.write(` ${message}\n`);
38
+ }
39
+ }
40
+ /**
41
+ * 生成中の進捗を 1 行のステータスとして見せる。
42
+ * --verbose なら思考の要約もそこに流し、いま何を考えているかまで見えるようにする。
43
+ */
44
+ async function generateWithProgress(prompt, model, base) {
45
+ // まだサーバーから何も返っていないことが分かる文言にする。
46
+ // 「接続しています」のままカウントが進むのは応答待ちを意味する。
47
+ const status = startStatusLine("リクエストを送信しました (サーバーの応答を待っています)");
48
+ let thinking = "";
49
+ let connected = false;
50
+ let wroteFirstChunk = false;
51
+ // 応答が来ないまま長引いた場合に、待っているだけだと分かるようにする。
52
+ // TTY ではスピナーが経過秒を出し続けるので、ここは間隔を空けて十分。
53
+ const stallTimer = setInterval(() => {
54
+ if (!connected) {
55
+ status.update(`サーバーの応答を待っています (${status.elapsedSeconds()} 秒経過 / 上限 ${Math.round(DEFAULT_TIMEOUT_MS / 1000)} 秒)`);
56
+ }
57
+ }, 15_000);
58
+ stallTimer.unref?.();
59
+ const onEvent = (event) => {
60
+ switch (event.type) {
61
+ case "connected":
62
+ connected = true;
63
+ clearInterval(stallTimer);
64
+ trace(`モデル: ${event.model}`);
65
+ trace(`リクエスト ID: ${event.requestId ?? "(取得できず)"}`);
66
+ // 節目は履歴に残す (非 TTY では update が表示されないため)。
67
+ status.log(" 設計を考えています");
68
+ status.update("設計を考えています");
69
+ break;
70
+ case "schemaFallback":
71
+ status.log(" 構造化出力が使えないため、JSON を直接生成する方式に切り替えます");
72
+ trace(` 理由: ${event.reason}`);
73
+ break;
74
+ case "streamEvent":
75
+ // 生イベントは切り分け用。既定では出さない。
76
+ trace(`イベント: ${event.name} (${status.elapsedSeconds()} 秒)`);
77
+ break;
78
+ case "thinking":
79
+ thinking += event.delta;
80
+ status.update(isVerbose() ? `考え中: ${tailLine(thinking, 60)}` : "設計を考えています");
81
+ break;
82
+ case "writing":
83
+ if (wroteFirstChunk === false) {
84
+ wroteFirstChunk = true;
85
+ status.log(" 設計を書き出しています");
86
+ }
87
+ status.update(`設計を書き出しています (${event.totalChars} 文字)`);
88
+ break;
89
+ case "done":
90
+ status.done(`✓ 設計を生成しました (${status.elapsedSeconds()} 秒, 入力 ${event.inputTokens} / 出力 ${event.outputTokens} トークン)`);
91
+ break;
92
+ }
93
+ };
94
+ try {
95
+ return await generateAppSpec(prompt, {
96
+ onEvent,
97
+ ...(model === undefined ? {} : { model }),
98
+ ...(base === undefined ? {} : { base }),
99
+ });
100
+ }
101
+ finally {
102
+ clearInterval(stallTimer);
103
+ // 失敗した場合もステータス行を残さない。
104
+ status.done();
105
+ }
106
+ }
107
+ program
108
+ .command("schema")
109
+ .description("AppSpec の書き方を出力する (AI エージェントに読ませる用)")
110
+ .option("--json", "完全な JSON Schema を出力する")
111
+ .option("--example", "そのまま deploy できる実例を出力する")
112
+ .action((options) => {
113
+ if (options.example === true) {
114
+ process.stdout.write(`${JSON.stringify(appSpecExample(), null, 2)}\n`);
115
+ return;
116
+ }
117
+ if (options.json === true || isJsonMode()) {
118
+ process.stdout.write(`${JSON.stringify(appSpecJsonSchema(), null, 2)}\n`);
119
+ return;
120
+ }
121
+ process.stdout.write(`${appSpecReference()}\n`);
122
+ });
123
+ program
124
+ .command("deploy")
125
+ .description("AppSpec を kintone にデプロイする。同じ AppSpec から何個でも作れる")
126
+ .argument("<spec>", "AppSpec の JSON ファイル")
127
+ .option("--dry-run", "kintone に送信せず、送信予定の内容を表示する")
128
+ .option("--space <id>", "アプリを作成するスペース ID (AppSpec の space より優先)")
129
+ .option("--thread <id>", "スペース内のスレッド ID")
130
+ .option("--guest-space <id>", "ゲストスペース ID")
131
+ .option("--revert-on-failure", "途中で失敗した場合に動作テスト環境の変更を破棄する")
132
+ .action(async (specPath, options) => {
133
+ await run("deploy", async () => {
134
+ const spec = readSpecFile(specPath);
135
+ await deployWithOptions(spec, options);
136
+ });
137
+ });
138
+ program
139
+ .command("pull")
140
+ .description("既存アプリの設定を AppSpec として取り出す (kintone を変更しない)")
141
+ .argument("<appId>", "アプリ ID")
142
+ .option("-o, --out <path>", "保存先。省略時は標準出力")
143
+ .action(async (appId, options) => {
144
+ await run("pull", async () => {
145
+ const config = config_();
146
+ const kintone = connect(config);
147
+ const pulled = await pullApp(appId, kintone, {
148
+ onProgress: (progress) => {
149
+ say(` ${progress.message}`);
150
+ if (progress.detail !== undefined && isVerbose())
151
+ trace(` ${progress.detail}`);
152
+ },
153
+ });
154
+ // 取得した spec が本当にデプロイできる形かを、ここで確かめる。
155
+ // 通らないものを渡すと、使う側が原因を追うことになる。
156
+ const spec = parseAppSpec(pulled.spec);
157
+ const json = `${JSON.stringify(spec, null, 2)}\n`;
158
+ if (options.out === undefined) {
159
+ if (!isJsonMode())
160
+ process.stdout.write(json);
161
+ }
162
+ else {
163
+ writeFileSync(options.out, json, "utf-8");
164
+ say(`✓ ${options.out} に保存しました。`);
165
+ }
166
+ if (pulled.warnings.length > 0) {
167
+ say("");
168
+ say("次の設定は AppSpec で表現できないため含まれていません:");
169
+ for (const warning of pulled.warnings)
170
+ say(` - ${warning}`);
171
+ }
172
+ emitSuccess({
173
+ command: "pull",
174
+ app: { id: pulled.appId, name: pulled.appName },
175
+ spec,
176
+ warnings: pulled.warnings,
177
+ ...(options.out === undefined ? {} : { out: options.out }),
178
+ });
179
+ });
180
+ });
181
+ program
182
+ .command("diff")
183
+ .description("既存アプリと AppSpec の差分を表示する (kintone を変更しない)")
184
+ .argument("<appId>", "アプリ ID")
185
+ .argument("<spec>", "目標とする AppSpec の JSON ファイル")
186
+ .action(async (appId, specPath) => {
187
+ await run("diff", async () => {
188
+ const desired = readSpecFile(specPath);
189
+ const config = config_();
190
+ const kintone = connect(config);
191
+ const pulled = await pullApp(appId, kintone);
192
+ const current = parseAppSpec(pulled.spec);
193
+ const diff = diffAppSpec(current, desired);
194
+ if (isEmptyDiff(diff)) {
195
+ say(`アプリ ${appId}「${pulled.appName}」と ${specPath} に差分はありません。`);
196
+ }
197
+ else {
198
+ say(`アプリ ${appId}「${pulled.appName}」と ${specPath} の差分:`);
199
+ say("");
200
+ for (const line of describeDiff(diff))
201
+ say(line);
202
+ if (diff.retyped.length > 0) {
203
+ say("");
204
+ say("! kintone は作成後のフィールド型を変更できません。");
205
+ say(" 型を変えるには、別のフィールドコードで新しく作り、");
206
+ say(" 古いフィールドを削除候補に送ることになります (データは移りません)。");
207
+ }
208
+ if (diff.orphaned.length > 0) {
209
+ say("");
210
+ say("- 削除候補のフィールドは削除されず、畳んだグループに移されます (データは残ります)。");
211
+ }
212
+ }
213
+ emitSuccess({
214
+ command: "diff",
215
+ app: { id: appId, name: pulled.appName },
216
+ hasChanges: !isEmptyDiff(diff),
217
+ diff,
218
+ warnings: pulled.warnings,
219
+ });
220
+ });
221
+ });
222
+ program
223
+ .command("revise")
224
+ .description("既存アプリの設計を、指示に沿って書き換えた AppSpec を作る (kintone は読むだけ)")
225
+ .argument("<appId>", "アプリ ID")
226
+ .argument("[instruction]", "どう変えたいか。--prompt-file を使う場合は省略する")
227
+ .option("-f, --prompt-file <path>", "指示を書いたファイル。`-` で標準入力から読む")
228
+ .option("-o, --out <path>", "保存先。省略時は標準出力")
229
+ .option("--model <model>", "使用するモデル")
230
+ .action(async (appId, instruction, options) => {
231
+ await run("revise", async () => {
232
+ const input = readPrompt(instruction, options.promptFile);
233
+ const config = config_();
234
+ const kintone = connect(config);
235
+ const pulled = await pullApp(appId, kintone, {
236
+ onProgress: (progress) => say(` ${progress.message}`),
237
+ });
238
+ const current = parseAppSpec(pulled.spec);
239
+ const revised = await generateWithProgress(input.text, options.model, current);
240
+ const json = `${JSON.stringify(revised, null, 2)}\n`;
241
+ if (options.out === undefined) {
242
+ if (!isJsonMode())
243
+ process.stdout.write(json);
244
+ }
245
+ else {
246
+ writeFileSync(options.out, json, "utf-8");
247
+ say(`✓ ${options.out} に保存しました。`);
248
+ }
249
+ // 何が起きるかは、決定的な差分で示す。生成結果の自己申告には頼らない。
250
+ const diff = diffAppSpec(current, revised);
251
+ say("");
252
+ if (isEmptyDiff(diff)) {
253
+ say("この指示では変更は生じませんでした。");
254
+ }
255
+ else {
256
+ say("この AppSpec を適用すると、こうなります:");
257
+ say("");
258
+ for (const line of describeDiff(diff))
259
+ say(line);
260
+ }
261
+ if (diff.retyped.length > 0) {
262
+ say("");
263
+ say("! 型の変更が含まれています。kintone では実行できないため、update は失敗します。");
264
+ say(" 指示を変えるか、生成された AppSpec を手で直してください。");
265
+ }
266
+ say("");
267
+ say(`確認したら: fieldsmith update ${appId} ${options.out ?? "<保存した spec>"}`);
268
+ emitSuccess({
269
+ command: "revise",
270
+ app: { id: appId, name: pulled.appName },
271
+ spec: revised,
272
+ diff,
273
+ warnings: pulled.warnings,
274
+ ...(options.out === undefined ? {} : { out: options.out }),
275
+ });
276
+ });
277
+ });
278
+ program
279
+ .command("update")
280
+ .description("既存アプリを AppSpec の内容に近づける (既定では運用環境へ反映しない)")
281
+ .argument("<appId>", "アプリ ID")
282
+ .argument("<spec>", "目標とする AppSpec の JSON ファイル")
283
+ .option("--deploy", "運用環境まで反映する")
284
+ .action(async (appId, specPath, options) => {
285
+ await run("update", async () => {
286
+ const desired = readSpecFile(specPath);
287
+ const config = config_();
288
+ const kintone = connect(config);
289
+ const status = startStatusLine("差分を調べています");
290
+ let result;
291
+ try {
292
+ result = await updateApp(appId, desired, kintone, {
293
+ ...(options.deploy === true ? { deploy: true } : {}),
294
+ onProgress: (progress) => {
295
+ // 「動作テスト環境まで反映しました」は後段でまとめて案内するので、
296
+ // ここでは履歴に残さない (同じ文言が二度出てしまう)。
297
+ if (progress.step !== "deploy" || options.deploy === true) {
298
+ status.log(` ${progress.message}`);
299
+ if (progress.detail !== undefined && isVerbose())
300
+ status.log(` ${progress.detail}`);
301
+ }
302
+ status.update(progress.message);
303
+ },
304
+ });
305
+ }
306
+ finally {
307
+ status.done();
308
+ }
309
+ if (isEmptyDiff(result.diff) || result.revision === "-1") {
310
+ say(`アプリ ${appId}「${result.appName}」に適用する変更はありません。`);
311
+ if (result.diff.orphaned.length > 0) {
312
+ say(` ${result.diff.orphaned.length} 件のフィールドは、すでに削除候補グループに入っています。`);
313
+ }
314
+ }
315
+ else {
316
+ say("");
317
+ say("適用した内容:");
318
+ for (const line of describeDiff(result.diff))
319
+ say(line);
320
+ if (result.pendingOrphans.length > 0) {
321
+ say("");
322
+ say(`- ${result.pendingOrphans.length} 件のフィールドを「削除候補」グループに移しました。`);
323
+ say(" 削除はしていないので、データは残っています。");
324
+ }
325
+ if (!result.deployed) {
326
+ say("");
327
+ say("動作テスト環境まで反映しました。運用環境にはまだ反映していません。");
328
+ say(` ${config.baseUrl}/k/${appId}/ で「変更を確認」してください。`);
329
+ say(` 問題なければ --deploy を付けて再実行します。`);
330
+ }
331
+ else {
332
+ say("");
333
+ say(`✓ 運用環境へ反映しました。`);
334
+ say(` ${config.baseUrl}/k/${appId}/`);
335
+ }
336
+ }
337
+ emitSuccess({
338
+ command: "update",
339
+ app: { id: appId, name: result.appName, url: `${config.baseUrl}/k/${appId}/` },
340
+ hasChanges: result.revision !== "-1",
341
+ deployed: result.deployed,
342
+ movedToOrphanGroup: result.pendingOrphans,
343
+ revision: result.revision,
344
+ diff: result.diff,
345
+ warnings: result.warnings,
346
+ });
347
+ });
348
+ });
349
+ program
350
+ .command("status")
351
+ .description("アプリの運用環境への反映状況を確認する")
352
+ .argument("<appId>", "アプリ ID")
353
+ .action(async (appId) => {
354
+ await run("status", async () => {
355
+ const kintone = connect(config_());
356
+ const { apps } = await kintone.call((client) => client.app.getDeployStatus({ apps: [appId] }));
357
+ for (const app of apps) {
358
+ say(`アプリ ${app.app}: ${app.status}`);
359
+ }
360
+ emitSuccess({ command: "status", apps });
361
+ });
362
+ });
363
+ program
364
+ .command("login")
365
+ .description("kintone の OAuth 認可を行い、トークンを保存する (OAuth を使う場合のみ)")
366
+ .action(async () => {
367
+ await run("login", async () => {
368
+ const oauth = requireOAuth(config_());
369
+ const { url, state } = buildAuthorizationRequest(oauth);
370
+ say("次の URL をブラウザで開き、アクセスを許可してください:\n");
371
+ say(` ${url}\n`);
372
+ say(`要求するスコープ: ${REQUIRED_SCOPE}`);
373
+ say("許可すると、登録済みのリダイレクト先へ転送されます。");
374
+ say("転送先のアドレスバーの URL を、そのままここに貼り付けてください。\n");
375
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
376
+ const redirected = await rl.question("リダイレクト先の URL: ");
377
+ rl.close();
378
+ const code = extractAuthorizationCode(redirected, state);
379
+ const token = await exchangeAuthorizationCode(oauth, code);
380
+ saveToken(oauth.baseUrl, token, process.env);
381
+ say(`\n✓ ${oauth.baseUrl} の認証情報を保存しました。`);
382
+ say(" アクセストークンは 1 時間で失効しますが、以降は自動で更新されます。");
383
+ emitSuccess({ command: "login", baseUrl: oauth.baseUrl });
384
+ });
385
+ });
386
+ program
387
+ .command("logout")
388
+ .description("保存済みの OAuth トークンを破棄する")
389
+ .action(async () => {
390
+ await run("logout", async () => {
391
+ const oauth = requireOAuth(config_());
392
+ const config = oauth;
393
+ const cleared = clearToken(config.baseUrl, process.env);
394
+ say(cleared
395
+ ? `✓ ${config.baseUrl} の認証情報を破棄しました。`
396
+ : `${config.baseUrl} の認証情報は保存されていません。`);
397
+ emitSuccess({ command: "logout", baseUrl: config.baseUrl, cleared });
398
+ });
399
+ });
400
+ program
401
+ .command("plan")
402
+ .description("要件から AppSpec を生成する。kintone には接続しない (要 Claude API)")
403
+ .argument("[prompt]", "作りたいアプリの説明。--prompt-file を使う場合は省略する")
404
+ .option("-f, --prompt-file <path>", "説明を書いたファイル。`-` で標準入力から読む")
405
+ .option("-o, --out <path>", "生成した AppSpec の保存先。省略時は標準出力")
406
+ .option("--model <model>", "使用するモデル")
407
+ .action(async (prompt, options) => {
408
+ await run("plan", async () => {
409
+ const input = readPrompt(prompt, options.promptFile);
410
+ const spec = await generateWithProgress(input.text, options.model);
411
+ const json = `${JSON.stringify(spec, null, 2)}\n`;
412
+ if (options.out === undefined) {
413
+ // --json のときは emitSuccess が spec を含むので二重に出さない。
414
+ if (!isJsonMode())
415
+ process.stdout.write(json);
416
+ }
417
+ else {
418
+ writeFileSync(options.out, json, "utf-8");
419
+ say(`✓ ${options.out} に保存しました。`);
420
+ printSpecSummary(spec);
421
+ }
422
+ emitSuccess({
423
+ command: "plan",
424
+ spec,
425
+ ...(options.out === undefined ? {} : { out: options.out }),
426
+ });
427
+ });
428
+ });
429
+ program
430
+ .command("create")
431
+ .description("要件から AppSpec を生成し、確認のうえデプロイする (要 Claude API)")
432
+ .argument("[prompt]", "作りたいアプリの説明。--prompt-file を使う場合は省略する")
433
+ .option("-f, --prompt-file <path>", "説明を書いたファイル。`-` で標準入力から読む")
434
+ .option("-y, --yes", "確認せずにデプロイする")
435
+ .option("--dry-run", "kintone に送信せず、送信予定の内容を表示する")
436
+ .option("--space <id>", "アプリを作成するスペース ID (AppSpec の space より優先)")
437
+ .option("--thread <id>", "スペース内のスレッド ID")
438
+ .option("--guest-space <id>", "ゲストスペース ID")
439
+ .option("--revert-on-failure", "途中で失敗した場合に動作テスト環境の変更を破棄する")
440
+ .option("--model <model>", "使用するモデル")
441
+ .option("-o, --out <path>", "生成した AppSpec の保存先")
442
+ .action(async (prompt, options) => {
443
+ await run("create", async () => {
444
+ const input = readPrompt(prompt, options.promptFile);
445
+ const spec = await generateWithProgress(input.text, options.model);
446
+ printSpecSummary(spec);
447
+ if (options.out !== undefined) {
448
+ writeFileSync(options.out, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
449
+ say(`\nAppSpec を ${options.out} に保存しました。`);
450
+ }
451
+ if (options.dryRun !== true && options.yes !== true && !(await confirm("\nこの内容でデプロイしますか?"))) {
452
+ say("中止しました。");
453
+ emitSuccess({ command: "create", deployed: false, spec });
454
+ return;
455
+ }
456
+ await deployWithOptions(spec, options);
457
+ });
458
+ });
459
+ async function deployWithOptions(spec, options) {
460
+ if (options.dryRun === true) {
461
+ const payloads = toKintonePayloads(spec);
462
+ say("--dry-run のため送信しません。kintone に送る内容:\n");
463
+ if (spec.icon !== undefined) {
464
+ // アイコンは fileKey がデプロイ時にしか決まらないので、ここでは生成結果だけ示す。
465
+ const icon = renderIcon({
466
+ glyph: spec.icon,
467
+ background: spec.iconBackground ?? backgroundFor(spec.name),
468
+ });
469
+ say(`アイコン: ${spec.icon} を ${icon.mode === "emoji" ? "絵文字" : "文字"}として描画 ` +
470
+ `(${Math.round(icon.png.length / 1024)}KB) → アップロード後に icon.file.fileKey として設定\n`);
471
+ }
472
+ say(JSON.stringify(payloads, null, 2));
473
+ emitSuccess({ command: "deploy", dryRun: true, appName: spec.name, payloads });
474
+ return;
475
+ }
476
+ const config = config_();
477
+ // CLI の指定を AppSpec より優先する。
478
+ const spaceId = options.space ?? spec.space;
479
+ const threadId = options.thread ?? spec.thread;
480
+ const guestSpaceId = options.guestSpace ?? spec.guestSpaceId;
481
+ if (spaceId !== undefined) {
482
+ trace(`スペース: ${spaceId}${threadId === undefined ? "" : ` / スレッド ${threadId}`}`);
483
+ }
484
+ const kintone = connect(config, guestSpaceId);
485
+ say(`「${spec.name}」をデプロイします (フィールド ${spec.fields.length} 件)`);
486
+ // どのステップで待っているかと経過秒が常に動くようにする。
487
+ // 各リクエストには上限時間があるので、無限に止まったままにはならない。
488
+ const status = startStatusLine("開始しています");
489
+ let result;
490
+ try {
491
+ result = await deployAppSpec(spec, kintone, {
492
+ ...(spaceId === undefined ? {} : { spaceId }),
493
+ ...(threadId === undefined ? {} : { threadId }),
494
+ ...(guestSpaceId === undefined ? {} : { guestSpaceId }),
495
+ ...(options.revertOnFailure === true ? { revertOnFailure: true } : {}),
496
+ onProgress: (progress) => {
497
+ status.log(` ${progress.message}`);
498
+ if (progress.detail !== undefined && isVerbose()) {
499
+ status.log(` ${progress.detail}`);
500
+ }
501
+ status.update(progress.message);
502
+ },
503
+ });
504
+ }
505
+ finally {
506
+ status.done();
507
+ }
508
+ const url = `${config.baseUrl}/k/${result.appId}/`;
509
+ say(`\n✓ デプロイが完了しました。`);
510
+ say(` アプリ ID: ${result.appId}`);
511
+ say(` URL: ${url}`);
512
+ emitSuccess({
513
+ command: "deploy",
514
+ app: { id: result.appId, url, name: spec.name },
515
+ revision: result.revision,
516
+ fieldCount: spec.fields.length,
517
+ });
518
+ }
519
+ function readSpecFile(path) {
520
+ let content;
521
+ try {
522
+ content = readFileSync(path, "utf-8");
523
+ }
524
+ catch {
525
+ throw new CliError(`AppSpec のファイルを読み込めませんでした: ${path}`);
526
+ }
527
+ let parsed;
528
+ try {
529
+ parsed = JSON.parse(content);
530
+ }
531
+ catch (error) {
532
+ throw new CliError(`${path} を JSON として解釈できませんでした: ${error.message}`);
533
+ }
534
+ return parseAppSpec(parsed);
535
+ }
536
+ function printSpecSummary(spec) {
537
+ say(`\nアプリ名: ${spec.name}`);
538
+ if (spec.icon !== undefined) {
539
+ say(`アイコン: ${spec.icon} (背景 ${spec.iconBackground ?? backgroundFor(spec.name)})`);
540
+ }
541
+ if (spec.space !== undefined) {
542
+ say(`スペース: ${spec.space}`);
543
+ }
544
+ if (spec.description !== undefined) {
545
+ say(`説明: ${spec.description}`);
546
+ }
547
+ say(`フィールド (${spec.fields.length} 件):`);
548
+ for (const field of spec.fields) {
549
+ const marks = [field.type];
550
+ if (field.required === true)
551
+ marks.push("必須");
552
+ if ("options" in field)
553
+ marks.push(field.options.join(" / "));
554
+ say(` - ${field.label} [${marks.join(", ")}]`);
555
+ }
556
+ if (spec.views !== undefined) {
557
+ say(`一覧 (${spec.views.length} 件): ${spec.views.map((view) => view.name).join(", ")}`);
558
+ }
559
+ const layout = resolveLayout(spec);
560
+ if (layout.mode === "grouped") {
561
+ // 実際の並べ替えは kintone から取得したレイアウトに対して行うが、
562
+ // 結果は同じになるので、ここでは AppSpec から予想を見せる。
563
+ const rows = groupIntoRows(spec.fields.map((field) => ({ type: field.type, code: resolveFieldCode(field) })), { maxPerRow: layout.maxPerRow, groups: fieldGroups(spec) });
564
+ say(`フォームの並び (最大 ${layout.maxPerRow} 列 → ${rows.length} 行):`);
565
+ for (const line of describeRows(rows)) {
566
+ say(` ${line}`);
567
+ }
568
+ }
569
+ }
570
+ async function confirm(question) {
571
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
572
+ const answer = await rl.question(`${question} [y/N]: `);
573
+ rl.close();
574
+ return /^y(es)?$/i.test(answer.trim());
575
+ }
576
+ /**
577
+ * プロンプトを引数・ファイル・標準入力のいずれかから読む。
578
+ * どこから読んだかと分量は必ず知らせる (意図しない入力で生成が走るのを防ぐため)。
579
+ */
580
+ function readPrompt(argument, filePath) {
581
+ const input = resolvePrompt({ argument, filePath });
582
+ if (filePath !== undefined) {
583
+ say(`要件を ${input.source} から読み込みました (${input.text.length} 文字)`);
584
+ }
585
+ if (input.text.length > LARGE_PROMPT_CHARS) {
586
+ say(` 要件が長いため、生成に時間がかかるか、要点が薄まる可能性があります。` +
587
+ ` 必要な部分に絞ることを検討してください。`);
588
+ }
589
+ return input;
590
+ }
591
+ class CliError extends Error {
592
+ }
593
+ function config_() {
594
+ return loadKintoneConfig(process.env);
595
+ }
596
+ /** どちらの認証で繋いでいるかを表す。 */
597
+ function authLabel(config) {
598
+ return config.auth.kind === "password"
599
+ ? `パスワード認証: ${config.auth.username}`
600
+ : "OAuth";
601
+ }
602
+ /**
603
+ * kintone に繋ぐ。接続先と認証方式は必ず --verbose に残す。
604
+ * 「どこに繋いでいるつもりか」が分からないまま調べることになるのを防ぐ。
605
+ */
606
+ function connect(config, guestSpaceId) {
607
+ trace(`接続先: ${config.baseUrl} (${authLabel(config)})`);
608
+ return createAuthenticatedKintone({
609
+ config,
610
+ env: process.env,
611
+ ...(guestSpaceId === undefined ? {} : { guestSpaceId }),
612
+ });
613
+ }
614
+ /**
615
+ * コマンドの共通の入り口。
616
+ *
617
+ * 想定内のエラーはスタックトレースを出さず、**種類ごとに終了コードを分けて**返す。
618
+ * AI エージェントが「AppSpec を直す」「login する」「再試行する」を判断できるようにするため。
619
+ */
620
+ async function run(command, action) {
621
+ try {
622
+ await action();
623
+ }
624
+ catch (error) {
625
+ if (error instanceof AppSpecValidationError) {
626
+ emitFailure({
627
+ command,
628
+ kind: "validation",
629
+ message: error.message,
630
+ issues: error.issues,
631
+ });
632
+ return;
633
+ }
634
+ if (error instanceof ConfigError) {
635
+ emitFailure({ command, kind: "config", message: error.message });
636
+ return;
637
+ }
638
+ if (error instanceof OAuthError) {
639
+ // ReauthRequiredError も OAuthError を継承している。
640
+ emitFailure({ command, kind: "auth", message: error.message });
641
+ return;
642
+ }
643
+ if (error instanceof UnsupportedUpdateError) {
644
+ emitFailure({ command, kind: "validation", message: error.message });
645
+ return;
646
+ }
647
+ if (error instanceof DeployError) {
648
+ // スコープ不足は「login し直す」なので認証側に寄せる。
649
+ const kind = /スコープ/.test(error.message) ? "auth" : "kintone";
650
+ emitFailure({ command, kind, message: error.message, appId: error.appId });
651
+ return;
652
+ }
653
+ if (error instanceof KintoneRestAPIError || error instanceof KintoneRequestError) {
654
+ emitFailure({ command, kind: "kintone", message: error.message });
655
+ return;
656
+ }
657
+ if (error instanceof AppSpecGenerationError) {
658
+ const issues = error.cause instanceof AppSpecValidationError ? error.cause.issues : undefined;
659
+ emitFailure({
660
+ command,
661
+ kind: "generation",
662
+ message: error.message,
663
+ ...(issues === undefined ? {} : { issues }),
664
+ });
665
+ return;
666
+ }
667
+ if (error instanceof PromptInputError || error instanceof CliError) {
668
+ emitFailure({ command, kind: "input", message: error.message });
669
+ return;
670
+ }
671
+ if (error instanceof Anthropic.APIError) {
672
+ emitFailure({
673
+ command,
674
+ kind: "generation",
675
+ message: `Claude API がエラーを返しました (HTTP ${error.status ?? "?"}): ${apiErrorMessage(error)}` +
676
+ (error.requestID == null ? "" : `\n リクエスト ID: ${error.requestID}`),
677
+ });
678
+ return;
679
+ }
680
+ // 想定していないエラー。要点だけ出し、スタックトレースは --verbose のときだけ。
681
+ emitFailure({
682
+ command,
683
+ kind: "unknown",
684
+ message: error instanceof Error ? error.message : String(error),
685
+ });
686
+ if (isVerbose() && error instanceof Error && error.stack !== undefined) {
687
+ console.error(error.stack);
688
+ }
689
+ }
690
+ }
691
+ /** APIError の本文から、人が読むべきメッセージだけを取り出す。 */
692
+ function apiErrorMessage(error) {
693
+ const body = error.error;
694
+ const message = body?.error?.message;
695
+ return typeof message === "string" ? message : error.message;
696
+ }
697
+ loadDotEnv();
698
+ await program.parseAsync(process.argv);
699
+ //# sourceMappingURL=index.js.map