akari-video 0.1.24 → 0.1.26

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 (57) hide show
  1. package/bin/akari.mjs +7 -3
  2. package/package.json +1 -1
  3. package/src/chrome-command.mjs +6 -2
  4. package/src/cli.mjs +19 -5
  5. package/src/doctor-command.mjs +65 -0
  6. package/src/messages.mjs +34 -10
  7. package/src/path-lookup.mjs +10 -20
  8. package/src/runtime-diagnostics.mjs +323 -0
  9. package/src/status-command.mjs +5 -1
  10. package/src/update-check.mjs +1 -1
  11. package/vendor/.akari-capability-sources.json +5 -2
  12. package/vendor/docs/contract-2026-07-22-render-basics.md +4 -1
  13. package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +14 -0
  14. package/vendor/docs/contract-2026-08-02-preview-parity.md +252 -619
  15. package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +2 -2
  16. package/vendor/docs/contract-2026-08-12-still-image-cut-source-v0.md +4 -9
  17. package/vendor/docs/contract-2026-08-18-v1-render-parity.md +4 -0
  18. package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +134 -0
  19. package/vendor/docs/contract-2026-08-28-osr-export-v0.md +42 -3
  20. package/vendor/docs/contract-2026-08-28-v2-approximation-ledger.md +46 -0
  21. package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +8 -0
  22. package/vendor/packages/akari-launcher/package.json +3 -3
  23. package/vendor/packages/bake-layer/package.json +2 -2
  24. package/vendor/packages/chat-bridge/package.json +2 -2
  25. package/vendor/packages/decision-cards/package.json +2 -2
  26. package/vendor/packages/edit-lint/src/edit-lint.mjs +29 -1
  27. package/vendor/packages/edit-store/lib/edit-store.d.ts +1 -0
  28. package/vendor/packages/edit-store/lib/edit-v2.d.ts +5 -2
  29. package/vendor/packages/edit-store/lib/edit-v2.js +8 -1
  30. package/vendor/packages/edit-store/lib/internal-model.js +6 -1
  31. package/vendor/packages/edit-store/lib/migrate/index.js +20 -3
  32. package/vendor/packages/export-nle/package.json +2 -2
  33. package/vendor/packages/frame-engine/README.ja.md +4 -2
  34. package/vendor/packages/frame-engine/README.md +4 -2
  35. package/vendor/packages/frame-engine/package.json +2 -1
  36. package/vendor/packages/gpu-export/README.ja.md +30 -0
  37. package/vendor/packages/gpu-export/README.md +30 -0
  38. package/vendor/packages/gpu-export/package.json +30 -0
  39. package/vendor/packages/intake-form/package.json +2 -2
  40. package/vendor/packages/media-bin/src/alpha-intake.mjs +272 -0
  41. package/vendor/packages/media-bin/test/alpha-intake.test.mjs +114 -0
  42. package/vendor/packages/osr-export/README.md +10 -0
  43. package/vendor/packages/osr-export/package.json +5 -3
  44. package/vendor/packages/preview-server/package.json +2 -2
  45. package/vendor/packages/render-cut/package.json +2 -2
  46. package/vendor/packages/schemas/bin/validate-edit.mjs +2 -2
  47. package/vendor/packages/schemas/edit.schema.json +5 -3
  48. package/vendor/packages/schemas/test/validate-edit.test.mjs +9 -0
  49. package/vendor/packages/template-render/package.json +2 -2
  50. package/vendor/skills/analyze-footage/bin/person-matte/mask-from-alpha.mjs +8 -4
  51. package/vendor/skills/edit-lint/SKILL.md +26 -5
  52. package/vendor/skills/render-cut/SKILL.md +26 -4
  53. package/vendor/templates/project-default/.claude/skills/README.md +9 -7
  54. package/vendor/templates/project-default/AGENTS.md +23 -13
  55. package/vendor/templates/project-default/CLAUDE.md +27 -3
  56. package/vendor/packages/preview-engine/README.md +0 -271
  57. package/vendor/packages/preview-engine/package.json +0 -25
package/bin/akari.mjs CHANGED
@@ -11,6 +11,8 @@ import { runCapabilityCommand } from '../src/capability-command.mjs';
11
11
  import { runStoreCommand } from '../src/store-command.mjs';
12
12
  import { runAssetsCommand } from '../src/assets-command.mjs';
13
13
  import { runMigrateCommand } from '../src/migrate-command.mjs';
14
+ import { runDoctorCommand } from '../src/doctor-command.mjs';
15
+ import { resolveRuntimePaths } from '../src/runtime-diagnostics.mjs';
14
16
  import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
15
17
  import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
16
18
 
@@ -21,7 +23,7 @@ async function printVersion() {
21
23
  const versionInfo = resolveInstalledVersionInfo({ env: process.env });
22
24
  // 1 行目は update / rollback の既存機械観測契約として CLI 版だけを維持する。
23
25
  console.log(`v${versionInfo.cliVersion}`);
24
- for (const line of describeInstalledVersions(versionInfo)) {
26
+ for (const line of describeInstalledVersions(versionInfo, resolveRuntimePaths({ env: process.env }))) {
25
27
  console.log(line);
26
28
  }
27
29
  return { exitCode: 0 };
@@ -47,13 +49,14 @@ async function printCliHelp() {
47
49
  // 読み直せば新版の内容を観測できる — 同一 argv での re-exec はしない設計判断
48
50
  // (理由は report.md 参照))。失敗は他の起動時副作用と同じく握りつぶし、
49
51
  // claude/opencode 起動やサブコマンド実行を止めない。
52
+ const argv = process.argv.slice(2);
53
+ const doctorJson = argv[0] === 'doctor' && argv.includes('--json');
50
54
  try {
51
- maybeApplyPendingUpdateOnLaunch({ env: process.env, log: (line) => console.log(line) });
55
+ maybeApplyPendingUpdateOnLaunch({ env: process.env, log: doctorJson ? () => {} : (line) => console.log(line) });
52
56
  } catch (error) {
53
57
  console.error(`自動更新の適用確認でエラーが発生しました(続行します): ${error instanceof Error ? error.message : String(error)}`);
54
58
  }
55
59
 
56
- const argv = process.argv.slice(2);
57
60
  // `akari update` / `akari init` / `akari new` / `akari narration` / `akari internal` /
58
61
  // `akari sounds` / `akari status` / `akari accept` / `akari capability` / `akari store` /
59
62
  // `akari assets` は claude へ転送せず、専用のサブコマンドとして扱う(契約 §4-1 /
@@ -62,6 +65,7 @@ const argv = process.argv.slice(2);
62
65
  // それ以外の引数はすべて従来どおり claude へ転送する。
63
66
  const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
64
67
  : (argv[0] === '--help' || argv[0] === '-h') ? printCliHelp()
68
+ : argv[0] === 'doctor' ? runDoctorCommand(argv.slice(1))
65
69
  : argv[0] === 'update' ? runUpdateCommand(argv.slice(1))
66
70
  : argv[0] === 'init' ? runInitCommand(argv.slice(1))
67
71
  : argv[0] === 'new' ? runNewCommand(argv.slice(1))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,9 +17,13 @@ class PublicError extends Error {
17
17
  }
18
18
  }
19
19
 
20
+ export function resolveChromeCacheDir(homeDirectory = homedir(), cacheDir) {
21
+ return cacheDir ? resolve(cacheDir) : join(homeDirectory, '.cache', 'puppeteer');
22
+ }
23
+
20
24
  function parseInstallArguments(argv, homeDirectory) {
21
25
  const parsed = {
22
- cacheDir: join(homeDirectory, '.cache', 'puppeteer'),
26
+ cacheDir: resolveChromeCacheDir(homeDirectory),
23
27
  buildId: undefined,
24
28
  };
25
29
  for (let index = 1; index < argv.length; index += 1) {
@@ -32,7 +36,7 @@ function parseInstallArguments(argv, homeDirectory) {
32
36
  throw new PublicError(`${argument} の値がありません`, 2);
33
37
  }
34
38
  index += 1;
35
- if (argument === '--cache-dir') parsed.cacheDir = resolve(value);
39
+ if (argument === '--cache-dir') parsed.cacheDir = resolveChromeCacheDir(homeDirectory, value);
36
40
  else parsed.buildId = value;
37
41
  }
38
42
  return parsed;
package/src/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
 
5
5
  import { resolveLauncherAssets } from './repo-assets.mjs';
6
6
  import { detectProjectState } from './project-state.mjs';
7
- import { findClaudeExecutable, findOpencodeExecutable } from './path-lookup.mjs';
7
+ import { findClaudeExecutable, findExecutable, findOpencodeExecutable } from './path-lookup.mjs';
8
8
  import { loadTaskLabels } from './task-labels.mjs';
9
9
  import { describeForceReinstall, describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCacheFallback, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
10
10
  import { resolveEffectiveProjectRoot } from './first-run.mjs';
@@ -23,6 +23,7 @@ import {
23
23
  } from './update-check.mjs';
24
24
  import { applySelfUpdate, isRunningFromAppDir, rollbackSelfUpdate } from './self-update.mjs';
25
25
  import { runChromeCommand } from './chrome-command.mjs';
26
+ import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
26
27
 
27
28
  /**
28
29
  * `akari` ランチャーの本体。3 入口契約(ターミナル `akari` / セッション内 `/akari` /
@@ -98,7 +99,8 @@ export async function run(args, options = {}) {
98
99
  } catch (error) {
99
100
  log(`接続確認でエラーが発生しました(続行します): ${error instanceof Error ? error.message : String(error)}`);
100
101
  }
101
- log(describeVersionStatus(versionInfo, readCacheSync(resolveCachePath(env))));
102
+ const runtimeDiagnostics = options.runtimeDiagnostics ?? resolveRuntimePaths({ ...options, env, platform });
103
+ log(describeVersionStatus(versionInfo, readCacheSync(resolveCachePath(env)), runtimeDiagnostics));
102
104
  }
103
105
 
104
106
  // 新版通知(契約 §4-1): キャッシュの読み比較のみ・ネットワークには一切触れない
@@ -204,6 +206,11 @@ export async function runUpdateCommand(args, options = {}) {
204
206
  const dismissRequested = args.includes('--dismiss');
205
207
  const rollbackRequested = args.includes('--rollback');
206
208
  const forceRequested = args.includes('--force');
209
+ const runtimeDiagnostics = options.runtimeDiagnostics ?? resolveRuntimePaths({ ...options, env });
210
+ const pathEnv = Object.hasOwn(env, 'PATH') ? (env.PATH ?? '') : (process.env.PATH ?? '');
211
+ const npmAvailable = options.npmAvailable
212
+ ?? !!(options.findExecutable ?? findExecutable)('npm', pathEnv, options.platform ?? process.platform, env.PATHEXT);
213
+ const describeOptions = { runtimeDiagnostics, npmAvailable };
207
214
  let usingCachedFeed = false;
208
215
 
209
216
  if (rollbackRequested) {
@@ -217,7 +224,7 @@ export async function runUpdateCommand(args, options = {}) {
217
224
  dismissed = true;
218
225
  }
219
226
  const finalCache = dismissed ? readCacheSync(cachePath) : cache;
220
- for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache: finalCache, dismissed })) {
227
+ for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache: finalCache, dismissed, ...describeOptions })) {
221
228
  log(line);
222
229
  }
223
230
  return { exitCode: 0 };
@@ -241,16 +248,22 @@ export async function runUpdateCommand(args, options = {}) {
241
248
  && (hasManagedApp || (options.isRunningFromAppDir ?? isRunningFromAppDir)({ env, launcherRoot: options.launcherRoot }));
242
249
 
243
250
  if (!selfUpdateEligible) {
244
- for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed: false, usingCachedFeed })) {
251
+ for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed: false, usingCachedFeed, ...describeOptions })) {
245
252
  log(line);
246
253
  }
254
+ if (forceRequested
255
+ && versionInfo.installRefStatus === 'missing'
256
+ && runtimeDiagnostics.render_cut.origin !== 'monorepo') {
257
+ log('この CLI からは install.sh 経路の本体を入れ直せません。');
258
+ log('導入するには `curl -fsSL https://raw.githubusercontent.com/AkariLabs/akari-video/main/install.sh | bash` を実行してください(デスクトップ版だけで使う場合は不要です)。');
259
+ }
247
260
  return { exitCode: 0 };
248
261
  }
249
262
 
250
263
  if (usingCachedFeed) {
251
264
  log(describeUpdateCacheFallback(cache));
252
265
  }
253
- for (const line of describeInstalledVersions(versionInfo)) {
266
+ for (const line of describeInstalledVersions(versionInfo, runtimeDiagnostics)) {
254
267
  log(line);
255
268
  }
256
269
  log(`最新バージョン: v${feed.product}`);
@@ -268,6 +281,7 @@ export async function runUpdateCommand(args, options = {}) {
268
281
  }
269
282
 
270
283
  function resolveCommandVersionInfo(options, env) {
284
+ if (options.versionInfo) return options.versionInfo;
271
285
  // 既存テスト/埋め込み利用の currentVersion 注入は CLI 版注入としても扱う。
272
286
  const cliVersion = options.cliVersion ?? options.currentVersion ?? readOwnVersion();
273
287
  if (options.currentVersion !== undefined) {
@@ -0,0 +1,65 @@
1
+ import { doctorExitCode, resolveDoctorReport } from './runtime-diagnostics.mjs';
2
+
3
+ const usage = [
4
+ '使い方: akari doctor [--json]',
5
+ '',
6
+ '必須部品の実在、解決元、PATH を確認します。',
7
+ ' --json 機械可読な診断結果を 1 オブジェクトで出力',
8
+ ].join('\n');
9
+
10
+ export async function runDoctorCommand(argv, options = {}) {
11
+ const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
12
+ const error = options.error ?? ((line) => process.stderr.write(`${line}\n`));
13
+ if (argv.includes('--help') || argv.includes('-h')) {
14
+ log(usage);
15
+ return { exitCode: 0 };
16
+ }
17
+ const unknown = argv.find((argument) => argument !== '--json');
18
+ if (unknown) {
19
+ error(`不明な doctor オプションです: ${unknown}\n${usage}`);
20
+ return { exitCode: 2 };
21
+ }
22
+
23
+ const report = options.report ?? await (options.resolveReport ?? resolveDoctorReport)(options);
24
+ if (argv.includes('--json')) log(JSON.stringify(report));
25
+ else log(formatDoctorReport(report));
26
+ return { exitCode: doctorExitCode(report.verdict), report };
27
+ }
28
+
29
+ export function formatDoctorReport(report) {
30
+ const rows = [
31
+ ['cli', 'ok', `v${report.cli.version} — ${report.cli.entry_path}`],
32
+ ['app_managed', report.app_managed.status, detail(report.app_managed.path, report.app_managed.version)],
33
+ ['app_bundle', report.app_bundle.found ? 'found' : 'missing', detail(report.app_bundle.path, report.app_bundle.version)],
34
+ ['render_cut', report.render_cut.origin, report.render_cut.path ?? '見つかりません'],
35
+ ['edit_lint', report.edit_lint.origin, report.edit_lint.path ?? '見つかりません'],
36
+ ['ffmpeg', report.ffmpeg.origin, report.ffmpeg.path ?? '見つかりません'],
37
+ ['ffprobe', report.ffprobe.origin, report.ffprobe.path ?? '見つかりません'],
38
+ ['chrome', report.chrome.found ? 'found' : 'missing', report.chrome.path ?? report.chrome.cache_dir],
39
+ ['gpu_export', report.gpu_export?.available ? 'ok' : 'unavailable', report.gpu_export?.reason ?? '診断情報がありません'],
40
+ ['path', report.path.on_path ? 'ok' : 'missing', report.path.cli_shim_dir],
41
+ ];
42
+ const widths = [
43
+ Math.max('項目'.length, ...rows.map((row) => row[0].length)),
44
+ Math.max('状態'.length, ...rows.map((row) => row[1].length)),
45
+ ];
46
+ const lines = [
47
+ 'AKARI Video doctor',
48
+ '',
49
+ `${'項目'.padEnd(widths[0])} ${'状態'.padEnd(widths[1])} 詳細`,
50
+ `${'-'.repeat(widths[0])} ${'-'.repeat(widths[1])} ${'-'.repeat(20)}`,
51
+ ...rows.map((row) => `${row[0].padEnd(widths[0])} ${row[1].padEnd(widths[1])} ${row[2]}`),
52
+ '',
53
+ `判定: ${report.verdict}`,
54
+ ];
55
+ if (report.next_steps.length > 0) {
56
+ lines.push('次の手順:');
57
+ lines.push(...report.next_steps.map((step) => ` - ${step}`));
58
+ }
59
+ return lines.join('\n');
60
+ }
61
+
62
+ function detail(path, version) {
63
+ const location = path ?? '見つかりません';
64
+ return version ? `v${version} — ${location}` : location;
65
+ }
package/src/messages.mjs CHANGED
@@ -71,7 +71,7 @@ export function formatUpdateNotice(status) {
71
71
  }
72
72
 
73
73
  /** `akari doctor` 系出力に足す 1 行(現在版 + フィード取得状態)。 */
74
- export function describeVersionStatus(versionOrInfo, cache) {
74
+ export function describeVersionStatus(versionOrInfo, cache, runtimeDiagnostics) {
75
75
  if (typeof versionOrInfo === 'string') {
76
76
  if (!cache?.feed) {
77
77
  return `バージョン: v${versionOrInfo}(更新フィード: 未取得)`;
@@ -80,7 +80,7 @@ export function describeVersionStatus(versionOrInfo, cache) {
80
80
  return `バージョン: v${versionOrInfo}(更新フィード: 取得済み・${fetchedAt} 時点)`;
81
81
  }
82
82
  const info = normalizeVersionInfo(versionOrInfo);
83
- const installed = describeInstalledVersions(info).join(' / ');
83
+ const installed = describeInstalledVersions(info, runtimeDiagnostics).join(' / ');
84
84
  if (!cache?.feed) {
85
85
  return `${installed}(更新フィード: 未取得)`;
86
86
  }
@@ -88,7 +88,7 @@ export function describeVersionStatus(versionOrInfo, cache) {
88
88
  return `${installed}(更新フィード: 取得済み・${fetchedAt} 時点)`;
89
89
  }
90
90
 
91
- export function describeInstalledVersions(versionOrInfo) {
91
+ export function describeInstalledVersions(versionOrInfo, runtimeDiagnostics) {
92
92
  const info = normalizeVersionInfo(versionOrInfo);
93
93
  if (info.installRefNeedsRepair) {
94
94
  const installRefPath = info.installRefPath ?? '~/.akari/app/.akari-install-ref';
@@ -99,6 +99,21 @@ export function describeInstalledVersions(versionOrInfo) {
99
99
  ];
100
100
  }
101
101
  if (!info.appVersion) {
102
+ if (info.installRefStatus === 'missing' && runtimeDiagnostics?.render_cut) {
103
+ const renderCut = runtimeDiagnostics.render_cut;
104
+ const availability = renderCut.origin === 'none'
105
+ ? 'render-cut が見つからないため書き出しできません'
106
+ : `書き出しは ${humanRenderOrigin(renderCut.origin)} の render-cut を使います`;
107
+ const lines = [
108
+ `現在のバージョン: v${info.currentVersion}`,
109
+ `CLI バージョン: v${info.cliVersion}`,
110
+ `install.sh 経路の本体は未導入(${availability}。詳細: \`akari doctor\`)`,
111
+ ];
112
+ if (renderCut.origin === 'none') {
113
+ lines.push('復旧するにはデスクトップ版を導入するか、install.sh 経路の本体を導入してください。');
114
+ }
115
+ return lines;
116
+ }
102
117
  return [
103
118
  `現在のバージョン: v${info.currentVersion}`,
104
119
  `CLI バージョン: v${info.cliVersion}`,
@@ -112,11 +127,15 @@ export function describeInstalledVersions(versionOrInfo) {
112
127
  return lines;
113
128
  }
114
129
 
130
+ function humanRenderOrigin(origin) {
131
+ return origin === 'monorepo' ? '開発リポジトリ' : origin;
132
+ }
133
+
115
134
  export function describeForceReinstall(versionOrInfo, targetVersion) {
116
135
  const info = normalizeVersionInfo(versionOrInfo);
117
136
  return info.installRefNeedsRepair
118
- ? `--force: 版を判定できない本体 → v${targetVersion} を入れ直します。`
119
- : `--force: 本体 v${info.currentVersion} → v${targetVersion} を入れ直します。`;
137
+ ? `--force: 版を判定できない install.sh 経路の本体 → v${targetVersion} を入れ直します。`
138
+ : `--force: install.sh 経路の本体 v${info.currentVersion} → v${targetVersion} を入れ直します。`;
120
139
  }
121
140
 
122
141
  function normalizeVersionInfo(value) {
@@ -172,9 +191,9 @@ export function creatorRootPromptText(defaultPath) {
172
191
  * `akari update` の出力本文(複数行)。フィード未取得・最新・新版ありで案内が変わる。
173
192
  * `dismissed` は今回の実行で dismiss 記録を書いたかどうか(表示文言の切り替えのみに使う)。
174
193
  */
175
- export function describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed, usingCachedFeed = false }) {
194
+ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed, usingCachedFeed = false, runtimeDiagnostics, npmAvailable = true }) {
176
195
  const info = versionInfo ?? normalizeVersionInfo(currentVersion);
177
- const lines = versionInfo ? describeInstalledVersions(info) : [`現在のバージョン: v${currentVersion}`];
196
+ const lines = versionInfo ? describeInstalledVersions(info, runtimeDiagnostics) : [`現在のバージョン: v${currentVersion}`];
178
197
  const feed = cache?.feed;
179
198
  if (!feed) {
180
199
  lines.push('最新情報をまだ取得できていません(オフライン、または初回起動直後の可能性があります)。');
@@ -202,8 +221,12 @@ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dism
202
221
  }
203
222
 
204
223
  const tarballUrl = feed.components?.cli?.tarball?.url;
205
- lines.push('更新するには、次のコマンドを実行してください(自動実行はしません):');
206
- lines.push(tarballUrl ? ` npm i -g ${tarballUrl}` : ' npm i -g akari-video@latest');
224
+ if (npmAvailable) {
225
+ lines.push('CLI を更新するには、次のコマンドを実行してください(自動実行はしません):');
226
+ lines.push(tarballUrl ? ` npm i -g ${tarballUrl}` : ' npm i -g akari-video@latest');
227
+ } else {
228
+ lines.push('npm が PATH に無いため、CLI の npm 更新コマンドは表示しません。');
229
+ }
207
230
  lines.push(
208
231
  dismissed
209
232
  ? `この版(v${feed.product})の通知は今後表示しません。`
@@ -304,7 +327,8 @@ export function describeCliHelp() {
304
327
  ' store connect アカウント連携(無料の素材パックと購入済み素材が使えるようになる)',
305
328
  ' sounds 公式音源ライブラリを一括ダウンロード(無料)',
306
329
  ' chrome install 動画書き出しに使う Chrome をダウンロードして導入する',
307
- ' update [--force] 更新を確認する(--force で本体を入れ直す)',
330
+ ' doctor [--json] 必須部品の実在と解決元を診断する',
331
+ ' update [--force] 更新を確認する(--force で install.sh 経路の本体を入れ直す)',
308
332
  ' status 接続状態を確認する',
309
333
  ' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
310
334
  '',
@@ -27,33 +27,23 @@ function resolveWindowsExtensions(pathExt) {
27
27
  * を判定するため、拡張子なしファイル(npm の POSIX シムなど)は直接 spawn できない。
28
28
  */
29
29
  export function findClaudeExecutable(pathEnv = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT) {
30
- const directories = pathEnv.split(path.delimiter).filter(Boolean);
31
- const candidateNames = platform === 'win32'
32
- ? resolveWindowsExtensions(pathExt).map((extension) => `claude${extension.toLowerCase()}`)
33
- : ['claude'];
34
-
35
- for (const directory of directories) {
36
- for (const name of candidateNames) {
37
- const candidate = path.join(directory, name);
38
- try {
39
- accessSync(candidate, fsConstants.X_OK);
40
- return candidate;
41
- } catch {
42
- // このディレクトリには無い。次を探す。
43
- }
44
- }
45
- }
46
- return null;
30
+ return findExecutable('claude', pathEnv, platform, pathExt);
47
31
  }
48
32
 
49
33
  /**
50
34
  * PATH 上に `opencode` 実行ファイルがあるかを探す。`findClaudeExecutable` と同じ規約。
51
35
  */
52
36
  export function findOpencodeExecutable(pathEnv = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT) {
53
- const directories = pathEnv.split(path.delimiter).filter(Boolean);
37
+ return findExecutable('opencode', pathEnv, platform, pathExt);
38
+ }
39
+
40
+ /** PATH 上の任意のコマンドを探索する共有ヘルパー。doctor / update も同じ判定を使う。 */
41
+ export function findExecutable(command, pathEnv = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT) {
42
+ const delimiter = platform === 'win32' ? ';' : path.delimiter;
43
+ const directories = pathEnv.split(delimiter).filter(Boolean);
54
44
  const candidateNames = platform === 'win32'
55
- ? resolveWindowsExtensions(pathExt).map((extension) => `opencode${extension.toLowerCase()}`)
56
- : ['opencode'];
45
+ ? resolveWindowsExtensions(pathExt).map((extension) => `${command}${extension.toLowerCase()}`)
46
+ : [command];
57
47
 
58
48
  for (const directory of directories) {
59
49
  for (const name of candidateNames) {