akari-video 0.1.66 → 0.1.68

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 (103) hide show
  1. package/README.md +11 -0
  2. package/bin/akari.mjs +7 -1
  3. package/package.json +1 -1
  4. package/src/doctor-command.mjs +5 -0
  5. package/src/generate-command.mjs +16 -0
  6. package/src/kits.mjs +238 -0
  7. package/src/messages.mjs +2 -0
  8. package/src/repo-assets.mjs +13 -1
  9. package/src/runtime-diagnostics.mjs +29 -0
  10. package/src/store-command.mjs +138 -1
  11. package/src/storyboard-command.mjs +24 -0
  12. package/src/world-command.mjs +17 -0
  13. package/vendor/.akari-capability-sources.json +9 -0
  14. package/vendor/docs/contract-2026-07-20-plan-json-v0.md +12 -0
  15. package/vendor/docs/contract-2026-07-25-plan-comments-v0.md +10 -11
  16. package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +75 -0
  17. package/vendor/docs/contract-2026-09-13-generation-v0.md +195 -0
  18. package/vendor/docs/contract-2026-09-13-world-map-v0.md +50 -0
  19. package/vendor/packages/akari-launcher/README.md +11 -0
  20. package/vendor/packages/akari-launcher/package.json +1 -1
  21. package/vendor/packages/akari-tools/package.json +3 -2
  22. package/vendor/packages/creator-root/src/index.mjs +6 -0
  23. package/vendor/packages/decision-cards/README.md +12 -0
  24. package/vendor/packages/edit-lint/src/edit-lint.mjs +2 -0
  25. package/vendor/packages/edit-lint/src/world-scene-declaration.mjs +64 -0
  26. package/vendor/packages/edit-store/lib/generation-meta-node.js +2 -11
  27. package/vendor/packages/edit-store/lib/generation-meta.d.ts +8 -1
  28. package/vendor/packages/edit-store/lib/generation-meta.js +21 -7
  29. package/vendor/packages/generate/README.md +6 -0
  30. package/vendor/packages/generate/package.json +5 -1
  31. package/vendor/packages/overlay-runtime/README.md +20 -0
  32. package/vendor/packages/overlay-runtime/package.json +2 -0
  33. package/vendor/packages/project-scaffold/src/index.mjs +33 -3
  34. package/vendor/packages/project-scaffold/test/kit-skill-adapter.test.mjs +38 -0
  35. package/vendor/packages/schemas/bin/validate-connections.mjs +21 -1
  36. package/vendor/packages/schemas/bin/validate-kit-manifest.mjs +374 -0
  37. package/vendor/packages/schemas/bin/validate-plan-comments.mjs +2 -2
  38. package/vendor/packages/schemas/bin/validate-world-map.mjs +170 -0
  39. package/vendor/packages/schemas/connections.schema.json +25 -0
  40. package/vendor/packages/schemas/examples/connections-v0-defaults-absent-valid/connections.json +8 -0
  41. package/vendor/packages/schemas/examples/connections-v0-defaults-unknown-key-invalid/connections.json +13 -0
  42. package/vendor/packages/schemas/examples/connections-v0-defaults-valid/connections.json +14 -0
  43. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-kind/LICENSE.md +1 -0
  44. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-kind/README.md +1 -0
  45. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-kind/manifest.json +9 -0
  46. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-missing-required/LICENSE.md +1 -0
  47. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-missing-required/README.md +1 -0
  48. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-missing-required/manifest.json +8 -0
  49. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-runtime/LICENSE.md +1 -0
  50. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-runtime/README.md +1 -0
  51. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-runtime/manifest.json +12 -0
  52. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-dir/LICENSE.md +1 -0
  53. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-dir/README.md +1 -0
  54. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-dir/manifest.json +12 -0
  55. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-name/LICENSE.md +1 -0
  56. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-name/README.md +1 -0
  57. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-name/manifest.json +12 -0
  58. package/vendor/packages/schemas/examples/kit-manifest-v1-invalid-skill-name/skills/manifest-name/SKILL.md +6 -0
  59. package/vendor/packages/schemas/examples/kit-manifest-v1-name-collision/LICENSE.md +1 -0
  60. package/vendor/packages/schemas/examples/kit-manifest-v1-name-collision/README.md +1 -0
  61. package/vendor/packages/schemas/examples/kit-manifest-v1-name-collision/manifest.json +12 -0
  62. package/vendor/packages/schemas/examples/kit-manifest-v1-name-collision/skills/edit-lint/SKILL.md +6 -0
  63. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/LICENSE.md +3 -0
  64. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/README.md +3 -0
  65. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/docs/guide.md +3 -0
  66. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/manifest.json +27 -0
  67. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/skills/sample-kit-skill/SKILL.md +8 -0
  68. package/vendor/packages/schemas/examples/kit-manifest-v1-valid/templates/sample.json +3 -0
  69. package/vendor/packages/schemas/examples/world-map-v1-no-worlds-legacy/planning/world-map.json +12 -0
  70. package/vendor/packages/schemas/examples/world-map-v2-flat-legacy/planning/world-map.json +10 -0
  71. package/vendor/packages/schemas/examples/world-map-v2-spatial-legacy/planning/world-map.json +27 -0
  72. package/vendor/packages/schemas/examples/world-map-v3-flat-valid/planning/world-map.json +34 -0
  73. package/vendor/packages/schemas/examples/world-map-v3-invalid-c10-same-world-portal/planning/world-map.json +34 -0
  74. package/vendor/packages/schemas/examples/world-map-v3-invalid-c3-edge-timing/planning/world-map.json +34 -0
  75. package/vendor/packages/schemas/examples/world-map-v3-invalid-c5-carry/planning/world-map.json +34 -0
  76. package/vendor/packages/schemas/examples/world-map-v3-invalid-c6-push-in-spatial/planning/world-map.json +26 -0
  77. package/vendor/packages/schemas/examples/world-map-v3-invalid-c7-cut-cover-null/planning/world-map.json +34 -0
  78. package/vendor/packages/schemas/examples/world-map-v3-spatial-valid/planning/world-map.json +26 -0
  79. package/vendor/packages/schemas/kit-manifest.schema.json +108 -0
  80. package/vendor/packages/schemas/package.json +3 -1
  81. package/vendor/packages/schemas/plan-comments.schema.json +3 -3
  82. package/vendor/packages/schemas/test/kit-manifest.test.mjs +83 -0
  83. package/vendor/packages/schemas/test/validate-connections.test.mjs +21 -0
  84. package/vendor/packages/schemas/test/validate-plan-comments.test.mjs +22 -0
  85. package/vendor/packages/schemas/test/world-map-schema.test.mjs +116 -0
  86. package/vendor/packages/schemas/world-map.schema.json +166 -0
  87. package/vendor/skills/edit-plan/approvals-and-generation.md +2 -2
  88. package/vendor/skills/edit-plan/plan-json.md +5 -5
  89. package/vendor/skills/edit-plan/workflow.md +6 -5
  90. package/vendor/skills/generate-media/SKILL.md +43 -0
  91. package/vendor/skills/generate-media/chain.md +32 -0
  92. package/vendor/skills/generate-media/engines.md +23 -0
  93. package/vendor/skills/generate-media/still.md +79 -0
  94. package/vendor/skills/generate-media/video.md +54 -0
  95. package/vendor/skills/manage-connections/SKILL.md +20 -5
  96. package/vendor/skills/manage-connections/bin/doctor.mjs +71 -29
  97. package/vendor/skills/manage-connections/bin/resolve-connections.mjs +35 -3
  98. package/vendor/skills/manage-connections/test/doctor-key-source.test.mjs +74 -0
  99. package/vendor/skills/manage-connections/test/resolve-connections-defaults.test.mjs +78 -0
  100. package/vendor/skills/overlay-authoring/3d.md +4 -1
  101. package/vendor/skills/overlay-authoring/SKILL.md +1 -0
  102. package/vendor/skills/overlay-authoring/world.md +42 -0
  103. package/vendor/skills/render-cut/SKILL.md +12 -0
package/README.md CHANGED
@@ -56,6 +56,17 @@ sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-com
56
56
  `akari clean [project-dir] [--dry-run] [--yes] [--json]`(使い捨ての中間ファイル、保持する
57
57
  正本、判断が必要なものを容量付きで一覧する。既定は一覧のみで、削除可能なものだけを承認後に削除)。
58
58
 
59
+ ## 拡張キット
60
+
61
+ `manifest.json` を持つ配布物を `akari store install <productId>` で導入すると、CLI / runtime の要件を検査し、素材とスキルを `~/.akari` 配下へ symlink で合成する。導入状況は `akari store status`、解除は `akari store uninstall <productId>` で確認・操作できる。Claude Code では初回だけ次を実行する。
62
+
63
+ ```sh
64
+ claude plugin marketplace add ~/.akari/kits
65
+ claude plugin install akari-kits@akari-kits
66
+ ```
67
+
68
+ `claude` が PATH に無い場合は、Claude Code のプラグイン設定で `~/.akari/kits` を marketplace として追加する。
69
+
59
70
  `akari` に渡した引数はそのまま `opencode` に転送する(例: `akari --continue` は
60
71
  `opencode --continue` を起動する)。
61
72
 
package/bin/akari.mjs CHANGED
@@ -13,6 +13,9 @@ import { runAssetsCommand } from '../src/assets-command.mjs';
13
13
  import { runMigrateCommand } from '../src/migrate-command.mjs';
14
14
  import { runCleanCommand } from '../src/clean-command.mjs';
15
15
  import { runDoctorCommand } from '../src/doctor-command.mjs';
16
+ import { runGenerateCommand } from '../src/generate-command.mjs';
17
+ import { runStoryboardCommand } from '../src/storyboard-command.mjs';
18
+ import { runWorldCommand } from '../src/world-command.mjs';
16
19
  import { resolveRuntimePaths } from '../src/runtime-diagnostics.mjs';
17
20
  import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
18
21
  import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
@@ -35,7 +38,7 @@ async function printVersion() {
35
38
  // `--help` は claude/opencode へそのまま転送されてしまっていた — AKARI Video 自身の
36
39
  // コマンド一覧が一度も出ない行き止まりだったため新設した)。
37
40
  async function printCliHelp() {
38
- for (const line of describeCliHelp()) {
41
+ for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー']) {
39
42
  console.log(line);
40
43
  }
41
44
  return { exitCode: 0 };
@@ -80,6 +83,9 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
80
83
  : argv[0] === 'assets' ? runAssetsCommand(argv.slice(1))
81
84
  : argv[0] === 'migrate' ? runMigrateCommand(argv.slice(1))
82
85
  : argv[0] === 'clean' ? runCleanCommand(argv.slice(1))
86
+ : argv[0] === 'generate' ? runGenerateCommand(argv.slice(1))
87
+ : argv[0] === 'storyboard' ? runStoryboardCommand(argv.slice(1))
88
+ : argv[0] === 'world' ? runWorldCommand(argv.slice(1))
83
89
  : run(argv);
84
90
 
85
91
  const result = await invoke.catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
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": {
@@ -36,6 +36,11 @@ export function formatDoctorReport(report) {
36
36
  ['ffmpeg', report.ffmpeg.origin, report.ffmpeg.path ?? '見つかりません'],
37
37
  ['ffprobe', report.ffprobe.origin, report.ffprobe.path ?? '見つかりません'],
38
38
  ['gpu_export', report.gpu_export?.available ? 'ok' : 'unavailable', report.gpu_export?.reason ?? '診断情報がありません'],
39
+ ['fal_key', report.fal_key?.source ?? 'missing', report.fal_key?.source === 'env'
40
+ ? '環境変数 FAL_KEY'
41
+ : report.fal_key?.source === 'credentials.env'
42
+ ? report.fal_key.credentials_path
43
+ : '環境変数にも credentials.env にもありません'],
39
44
  ['path', report.path.on_path ? 'ok' : 'missing', report.path.cli_shim_dir],
40
45
  ];
41
46
  const widths = [
@@ -0,0 +1,16 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { resolveLauncherAssets } from "./repo-assets.mjs";
4
+
5
+ export async function runGenerateCommand(args, options = {}) {
6
+ const logError = options.logError ?? ((line) => console.error(line));
7
+ const assets = options.assets ?? resolveLauncherAssets();
8
+ const spawn = options.spawn ?? spawnSync;
9
+ if (!assets.generateScript) {
10
+ logError("akari generate の実行スクリプトが見つかりません。完全な AKARI Video を再導入してください:");
11
+ logError(" npm install -g akari-video");
12
+ return { exitCode: 1 };
13
+ }
14
+ const result = spawn(process.execPath, [assets.generateScript, ...args], { stdio: "inherit" });
15
+ return { exitCode: typeof result?.status === "number" ? result.status : 1 };
16
+ }
package/src/kits.mjs ADDED
@@ -0,0 +1,238 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import {
3
+ existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync,
4
+ renameSync, rmSync, symlinkSync, writeFileSync
5
+ } from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ import { resolveLauncherAssets } from './repo-assets.mjs';
9
+
10
+ const KITS_SCHEMA = 'akari-installed-kits/v0';
11
+ const PLUGIN_DESCRIPTION = 'AKARI Video 拡張キットのスキルをまとめて提供するローカルプラグイン。';
12
+
13
+ function parseVersion(value) {
14
+ const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)$/u);
15
+ return match ? match.slice(1).map(Number) : null;
16
+ }
17
+
18
+ function compareVersions(left, right) {
19
+ for (let index = 0; index < 3; index += 1) {
20
+ if (left[index] !== right[index]) return left[index] - right[index];
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ function satisfies(version, range) {
26
+ const actual = parseVersion(version);
27
+ const match = String(range).match(/^(\^|~|>=)?(\d+\.\d+\.\d+)$/u);
28
+ if (!actual || !match) return false;
29
+ const required = parseVersion(match[2]);
30
+ if (compareVersions(actual, required) < 0) return false;
31
+ if (match[1] === '^') {
32
+ return actual[0] === required[0] && (required[0] !== 0 || actual[1] === required[1]);
33
+ }
34
+ if (match[1] === '~') return actual[0] === required[0] && actual[1] === required[1];
35
+ if (match[1] === '>=') return true;
36
+ return compareVersions(actual, required) === 0;
37
+ }
38
+
39
+ export function readKitManifest(kitDir) {
40
+ const manifestPath = path.join(kitDir, 'manifest.json');
41
+ return existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : null;
42
+ }
43
+
44
+ export function checkRequires(manifest, { cliVersion, runtimeIds = [], entitledProductIds = [] }) {
45
+ const blockers = [];
46
+ const warnings = [];
47
+ const requires = manifest?.requires ?? {};
48
+ if (!satisfies(cliVersion, requires.cli)) {
49
+ blockers.push(`CLI ${requires.cli} が必要です(現在 ${cliVersion})。AKARI Video を更新してください。`);
50
+ }
51
+ const availableRuntimes = new Set(runtimeIds);
52
+ for (const runtimeId of requires.runtimes ?? []) {
53
+ if (!availableRuntimes.has(runtimeId)) {
54
+ blockers.push(`runtime ${runtimeId} がありません。このアプリ版ではキットを利用できません。`);
55
+ }
56
+ }
57
+ const entitled = new Set(entitledProductIds);
58
+ for (const productId of requires.products ?? []) {
59
+ if (!entitled.has(productId)) {
60
+ warnings.push(`依存商品 ${productId} の購入が確認できません。\`akari store install ${productId}\` で導入してください。`);
61
+ }
62
+ }
63
+ return { ok: blockers.length === 0, blockers, warnings };
64
+ }
65
+
66
+ function safeKitPath(kitDir, ...parts) {
67
+ const root = path.resolve(kitDir);
68
+ const candidate = path.resolve(root, ...parts);
69
+ if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) {
70
+ throw new Error(`キット外のパスは参照できません: ${parts.join('/')}`);
71
+ }
72
+ return candidate;
73
+ }
74
+
75
+ function replaceSymlink(source, destination, {
76
+ platform = process.platform,
77
+ relativeTarget,
78
+ symlinkSyncImpl = symlinkSync
79
+ } = {}) {
80
+ if (existsSync(destination) || (() => { try { lstatSync(destination); return true; } catch { return false; } })()) {
81
+ const stat = lstatSync(destination);
82
+ if (!stat.isSymbolicLink()) return { status: 'occupied' };
83
+ rmSync(destination);
84
+ }
85
+ mkdirSync(path.dirname(destination), { recursive: true });
86
+ const target = relativeTarget ?? path.relative(path.dirname(destination), source);
87
+ try {
88
+ symlinkSyncImpl(target, destination, 'dir');
89
+ return { status: 'linked', target };
90
+ } catch (error) {
91
+ if (platform === 'win32' && error?.code === 'EPERM') return { status: 'permission-denied' };
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ export function linkKitAssets(kitDir, manifest, home, options = {}) {
97
+ const warnings = [];
98
+ const linked = [];
99
+ const assets = options.assets?.schemasSourceDir !== undefined
100
+ ? options.assets
101
+ : resolveLauncherAssets(options.assets);
102
+ const validator = options.validateAssetPath
103
+ ?? (assets.schemasSourceDir ? path.join(assets.schemasSourceDir, 'bin', 'validate-asset.mjs') : null);
104
+ for (const asset of manifest.assets ?? []) {
105
+ const source = safeKitPath(kitDir, 'assets', asset.category, asset.id);
106
+ if (validator && existsSync(validator)) {
107
+ const result = (options.spawnSyncImpl ?? spawnSync)(process.execPath, [validator, source], { stdio: 'pipe' });
108
+ if (result.status !== 0) {
109
+ warnings.push(`素材 ${asset.category}/${asset.id} は検査に失敗したためリンクしませんでした。`);
110
+ continue;
111
+ }
112
+ } else {
113
+ warnings.push(`素材 ${asset.category}/${asset.id} の検査ツールが見つからないため検査をスキップしました。`);
114
+ }
115
+ const destination = path.join(home, 'assets', asset.category, asset.id);
116
+ const result = replaceSymlink(source, destination, {
117
+ ...options,
118
+ relativeTarget: path.join('..', '..', 'assets', 'store', manifest.id, 'assets', asset.category, asset.id)
119
+ });
120
+ if (result.status === 'occupied') {
121
+ warnings.push(`既存の実ディレクトリを保持しました: ${destination}`);
122
+ } else if (result.status === 'permission-denied') {
123
+ warnings.push(`symlink を作成できませんでした(Windows の権限を確認してください): ${destination}`);
124
+ } else {
125
+ linked.push({ category: asset.category, id: asset.id });
126
+ }
127
+ }
128
+ return { linked, warnings };
129
+ }
130
+
131
+ export function linkKitSkills(kitDir, manifest, home, options = {}) {
132
+ const warnings = [];
133
+ const blockers = [];
134
+ const linked = [];
135
+ const planned = [];
136
+ for (const skill of manifest.skills ?? []) {
137
+ const source = safeKitPath(kitDir, skill.dir);
138
+ const destination = path.join(home, 'kits', 'plugin', 'skills', skill.name);
139
+ try {
140
+ const stat = lstatSync(destination);
141
+ if (!stat.isSymbolicLink()) {
142
+ blockers.push(`スキル名 ${skill.name} は既存の実ディレクトリと重複しています。`);
143
+ continue;
144
+ }
145
+ const current = path.resolve(path.dirname(destination), readlinkSync(destination));
146
+ if (current !== source) {
147
+ blockers.push(`スキル名 ${skill.name} は別のキットと重複しています。`);
148
+ continue;
149
+ }
150
+ } catch (error) {
151
+ if (error?.code !== 'ENOENT') throw error;
152
+ }
153
+ planned.push({ skill, source, destination });
154
+ }
155
+ if (blockers.length > 0) return { linked, blockers, warnings };
156
+ for (const { skill, source, destination } of planned) {
157
+ const result = replaceSymlink(source, destination, options);
158
+ if (result.status === 'permission-denied') {
159
+ warnings.push(`symlink を作成できませんでした(Windows の権限を確認してください): ${destination}`);
160
+ } else {
161
+ linked.push(skill.name);
162
+ }
163
+ }
164
+ return { linked, blockers, warnings };
165
+ }
166
+
167
+ export function readKitsLedger(home) {
168
+ const ledgerPath = path.join(home, 'kits', 'installed.json');
169
+ if (!existsSync(ledgerPath)) return { schema: KITS_SCHEMA, kits: [] };
170
+ const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'));
171
+ if (ledger?.schema !== KITS_SCHEMA || !Array.isArray(ledger.kits)) {
172
+ throw new Error(`拡張キット台帳の形式が想定と違います: ${ledgerPath}`);
173
+ }
174
+ return ledger;
175
+ }
176
+
177
+ export function writeKitsLedger(home, entry) {
178
+ const ledger = readKitsLedger(home);
179
+ ledger.kits = [...ledger.kits.filter((kit) => kit.id !== entry.id), entry];
180
+ const ledgerPath = path.join(home, 'kits', 'installed.json');
181
+ mkdirSync(path.dirname(ledgerPath), { recursive: true });
182
+ const temporary = `${ledgerPath}.tmp-${process.pid}`;
183
+ writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
184
+ renameSync(temporary, ledgerPath);
185
+ return ledger;
186
+ }
187
+
188
+ export function removeKit(home, productId) {
189
+ const ledger = readKitsLedger(home);
190
+ const entry = ledger.kits.find((kit) => kit.id === productId);
191
+ if (!entry) return false;
192
+ for (const name of entry.skills ?? []) removeOwnedSymlink(path.join(home, 'kits', 'plugin', 'skills', name), entry.kitDir);
193
+ for (const asset of entry.assets ?? []) {
194
+ removeOwnedSymlink(path.join(home, 'assets', asset.category, asset.id), entry.kitDir);
195
+ }
196
+ ledger.kits = ledger.kits.filter((kit) => kit.id !== productId);
197
+ const ledgerPath = path.join(home, 'kits', 'installed.json');
198
+ const temporary = `${ledgerPath}.tmp-${process.pid}`;
199
+ writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
200
+ renameSync(temporary, ledgerPath);
201
+ return true;
202
+ }
203
+
204
+ function removeOwnedSymlink(linkPath, kitDir) {
205
+ try {
206
+ if (!lstatSync(linkPath).isSymbolicLink()) return;
207
+ const target = path.resolve(path.dirname(linkPath), readlinkSync(linkPath));
208
+ if (target === path.resolve(kitDir) || target.startsWith(`${path.resolve(kitDir)}${path.sep}`)) rmSync(linkPath);
209
+ } catch (error) {
210
+ if (error?.code !== 'ENOENT') throw error;
211
+ }
212
+ }
213
+
214
+ export function ensureKitsMarketplace(home) {
215
+ const marketplacePath = path.join(home, 'kits', '.claude-plugin', 'marketplace.json');
216
+ const pluginPath = path.join(home, 'kits', 'plugin', '.claude-plugin', 'plugin.json');
217
+ const marketplace = {
218
+ name: 'akari-kits',
219
+ owner: { name: 'AKARI Video' },
220
+ plugins: [{ name: 'akari-kits', source: './plugin', description: PLUGIN_DESCRIPTION }]
221
+ };
222
+ const plugin = { name: 'akari-kits', description: PLUGIN_DESCRIPTION };
223
+ for (const [filePath, value] of [[marketplacePath, marketplace], [pluginPath, plugin]]) {
224
+ mkdirSync(path.dirname(filePath), { recursive: true });
225
+ const content = `${JSON.stringify(value, null, 2)}\n`;
226
+ if (!existsSync(filePath) || readFileSync(filePath, 'utf8') !== content) writeFileSync(filePath, content);
227
+ }
228
+ return { marketplacePath, pluginPath };
229
+ }
230
+
231
+ export function enableHint() {
232
+ return [
233
+ 'Claude Code で拡張キットを有効化してください:',
234
+ ' claude plugin marketplace add ~/.akari/kits',
235
+ ' claude plugin install akari-kits@akari-kits',
236
+ 'claude が PATH に無い場合は、Claude Code のプラグイン設定で ~/.akari/kits を marketplace として追加してください。'
237
+ ].join('\n');
238
+ }
package/src/messages.mjs CHANGED
@@ -330,6 +330,8 @@ export function describeCliHelp() {
330
330
  ' update [--force] 更新を確認する(--force で install.sh 経路の本体を入れ直す)',
331
331
  ' status 接続状態を確認する',
332
332
  ' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
333
+ ' generate 台本のビートから静止画クリップを生成',
334
+ ' storyboard タイムラインから印刷用の絵コンテを作成',
333
335
  ' akari clean [dir] 使い捨ての中間ファイルを一覧・削除(既定は一覧のみ)',
334
336
  '',
335
337
  '開発者向け:',
@@ -39,6 +39,8 @@ const CAPTIONS_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'ca
39
39
  export const CAPTURE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'capture.mjs');
40
40
  const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
41
41
  const EYE_BAR_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'eye-bar.mjs');
42
+ export const GENERATE_CLI_RELATIVE = path.join('packages', 'generate', 'src', 'cli', 'index.mjs');
43
+ export const STORYBOARD_CLI_RELATIVE = path.join('packages', 'decision-cards', 'render-storyboard-print.mjs');
42
44
 
43
45
  /**
44
46
  * 指定ルート配下に同梱されているスキル正本・雛形・schemas・scaffold 実装・creator-root
@@ -60,6 +62,8 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
60
62
  const mediaScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'media.mjs');
61
63
  const decisionLogScript = path.join(repoRoot, DECISION_LOG_SCRIPT_RELATIVE);
62
64
  const wordBookScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'word-book.mjs');
65
+ const generateScript = path.join(repoRoot, GENERATE_CLI_RELATIVE);
66
+ const storyboardScript = path.join(repoRoot, STORYBOARD_CLI_RELATIVE);
63
67
 
64
68
  return {
65
69
  repoRoot,
@@ -79,7 +83,9 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
79
83
  eyeBarScript: existsSync(eyeBarScript) ? eyeBarScript : null,
80
84
  mediaScript: existsSync(mediaScript) ? mediaScript : null,
81
85
  ...(existsSync(decisionLogScript) ? { decisionLogScript } : {}),
82
- ...(existsSync(wordBookScript) ? { wordBookScript } : {})
86
+ ...(existsSync(wordBookScript) ? { wordBookScript } : {}),
87
+ ...(existsSync(generateScript) ? { generateScript } : {}),
88
+ ...(existsSync(storyboardScript) ? { storyboardScript } : {})
83
89
  };
84
90
  }
85
91
 
@@ -115,6 +121,12 @@ export function resolveLauncherAssets({
115
121
  renderWhenIdleScript: candidate.renderWhenIdleScript ?? vendor.renderWhenIdleScript,
116
122
  eyeBarScript: candidate.eyeBarScript ?? vendor.eyeBarScript,
117
123
  mediaScript: candidate.mediaScript ?? vendor.mediaScript,
124
+ ...(candidate.generateScript ?? vendor.generateScript
125
+ ? { generateScript: candidate.generateScript ?? vendor.generateScript }
126
+ : {}),
127
+ ...(candidate.storyboardScript ?? vendor.storyboardScript
128
+ ? { storyboardScript: candidate.storyboardScript ?? vendor.storyboardScript }
129
+ : {}),
118
130
  ...(candidate.decisionLogScript ?? vendor.decisionLogScript ? { decisionLogScript: candidate.decisionLogScript ?? vendor.decisionLogScript } : {}),
119
131
  ...(candidate.wordBookScript ?? vendor.wordBookScript
120
132
  ? { wordBookScript: candidate.wordBookScript ?? vendor.wordBookScript }
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import path, { dirname, join, resolve } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
@@ -132,6 +133,7 @@ export async function resolveDoctorReport(options = {}) {
132
133
  const runtime = resolveRuntimePaths(options);
133
134
  const mediaBin = await resolveMediaTools({ ...options, env, platform, pathEnv, pathExt });
134
135
  const gpuExport = await resolveGpuExportAvailability({ ...options, env, platform });
136
+ const falKey = resolveFalKeySource({ env, homeDirectory: options.homeDirectory });
135
137
  const akariHome = resolveAkariHome(env);
136
138
  const cliShimDir = join(akariHome, 'cli', 'bin');
137
139
  const report = {
@@ -148,6 +150,7 @@ export async function resolveDoctorReport(options = {}) {
148
150
  ffmpeg: mediaBin.ffmpeg,
149
151
  ffprobe: mediaBin.ffprobe,
150
152
  gpu_export: gpuExport,
153
+ fal_key: falKey,
151
154
  path: {
152
155
  cli_shim_dir: cliShimDir,
153
156
  on_path: pathContains(pathEnv, cliShimDir, platform),
@@ -158,6 +161,29 @@ export async function resolveDoctorReport(options = {}) {
158
161
  return report;
159
162
  }
160
163
 
164
+ function resolveFalKeySource({ env, homeDirectory }) {
165
+ const credentialsPath = env.AKARI_CREDENTIALS_FILE
166
+ ?? join(homeDirectory ?? homedir(), '.config', 'akari-video', 'credentials.env');
167
+ if (typeof env.FAL_KEY === 'string' && env.FAL_KEY.trim().length > 0) {
168
+ return { source: 'env', credentials_path: credentialsPath };
169
+ }
170
+ try {
171
+ const source = readFileSync(credentialsPath, 'utf8');
172
+ for (const originalLine of source.split(/\r?\n/u)) {
173
+ const line = originalLine.trim();
174
+ if (!line || line.startsWith('#')) continue;
175
+ const separator = line.indexOf('=');
176
+ if (separator < 1 || line.slice(0, separator).trim() !== 'FAL_KEY') continue;
177
+ if (line.slice(separator + 1).trim().length > 0) {
178
+ return { source: 'credentials.env', credentials_path: credentialsPath };
179
+ }
180
+ }
181
+ } catch {
182
+ // credentials.env が無い・読めない場合も doctor 自体は継続する。
183
+ }
184
+ return { source: 'missing', credentials_path: credentialsPath };
185
+ }
186
+
161
187
  export async function resolveGpuExportAvailability(options = {}) {
162
188
  const platform = options.platform ?? process.platform;
163
189
  try {
@@ -265,6 +291,9 @@ function doctorNextSteps(report) {
265
291
  if (!report.path.on_path && report.render_cut.origin !== 'monorepo') {
266
292
  steps.push('`~/.akari/cli/bin` を PATH に追加してください。');
267
293
  }
294
+ if (report.fal_key?.source === 'missing') {
295
+ steps.push('FAL_KEY がありません。環境変数 FAL_KEY か credentials.env に置くと fal の生成が使えます(無くても書き出しは動きます)。');
296
+ }
268
297
  return steps;
269
298
  }
270
299
 
@@ -5,6 +5,7 @@ import {
5
5
  } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import path from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
8
9
  import {
9
10
  DEFAULT_STORE_BASE_URL,
10
11
  defaultOpenBrowser,
@@ -17,6 +18,19 @@ import {
17
18
  startDeviceConnection,
18
19
  validateAndSaveCredentials
19
20
  } from './store-device-connect.mjs';
21
+ import { readOwnVersion } from './update-check.mjs';
22
+ import { resolveLauncherAssets } from './repo-assets.mjs';
23
+ import {
24
+ checkRequires,
25
+ enableHint,
26
+ ensureKitsMarketplace,
27
+ linkKitAssets,
28
+ linkKitSkills,
29
+ readKitManifest,
30
+ readKitsLedger,
31
+ removeKit,
32
+ writeKitsLedger
33
+ } from './kits.mjs';
20
34
 
21
35
  export { readCredentials, resolveCredentialsPath } from './store-device-connect.mjs';
22
36
 
@@ -288,6 +302,27 @@ export async function runStoreCommand(args, options = {}) {
288
302
  }
289
303
  log(`接続中: ${data.email}(${creds.url})`);
290
304
  formatStoreEntitlements(data, log);
305
+ const kits = readKitsLedger(resolveAkariHome(env)).kits;
306
+ if (kits.length > 0) {
307
+ log('拡張キット:');
308
+ for (const kit of kits) {
309
+ log(` ${kit.id} v${kit.version} / スキル: ${(kit.skills ?? []).join(', ') || 'なし'} / 素材: ${(kit.assets ?? []).length} 件`);
310
+ }
311
+ }
312
+ return { exitCode: 0 };
313
+ }
314
+
315
+ if (sub === 'uninstall') {
316
+ const productId = args[1];
317
+ if (!isSafePathSegment(productId) || productId.startsWith('--')) {
318
+ log('使い方: akari store uninstall <productId>');
319
+ return { exitCode: 1 };
320
+ }
321
+ if (!removeKit(resolveAkariHome(env), productId)) {
322
+ log(`導入済みの拡張キットが見つかりません: ${productId}`);
323
+ return { exitCode: 1 };
324
+ }
325
+ log(`拡張キットを無効化しました: ${productId}(展開済みファイルは残しています)`);
291
326
  return { exitCode: 0 };
292
327
  }
293
328
 
@@ -415,6 +450,107 @@ export async function runStoreCommand(args, options = {}) {
415
450
  const readme = findFile(destDir, 'README.md');
416
451
  log(`展開しました: ${destDir}`);
417
452
  if (readme) log(`導入手順: ${readme}`);
453
+ // 深い階層の無関係な manifest.json をキットと誤認すると、従来成功していた素材商品の
454
+ // install を壊す。キットの規定位置は展開ルート、または zip が単一トップディレクトリを
455
+ // 持つ場合のその直下だけとし、JSON として読めても kind !== kit なら完全に素通りする。
456
+ let manifestPath = path.join(destDir, 'manifest.json');
457
+ if (!existsSync(manifestPath)) {
458
+ const rootEntries = readdirSync(destDir, { withFileTypes: true });
459
+ manifestPath = rootEntries.length === 1 && rootEntries[0].isDirectory()
460
+ ? path.join(destDir, rootEntries[0].name, 'manifest.json')
461
+ : null;
462
+ if (manifestPath && !existsSync(manifestPath)) manifestPath = null;
463
+ }
464
+ let manifest = null;
465
+ if (manifestPath) {
466
+ try {
467
+ manifest = readKitManifest(path.dirname(manifestPath));
468
+ } catch {
469
+ log('キットの検査に失敗しました。展開済みファイルを確認してください。');
470
+ return { exitCode: 1 };
471
+ }
472
+ }
473
+ if (manifest?.kind === 'kit') {
474
+ const kitDir = path.dirname(manifestPath);
475
+ const home = resolveAkariHome(env);
476
+ const launcherAssets = options.assets ?? resolveLauncherAssets();
477
+ const validator = launcherAssets.schemasSourceDir
478
+ ? path.join(launcherAssets.schemasSourceDir, 'bin', 'validate-kit-manifest.mjs')
479
+ : null;
480
+ if (validator && existsSync(validator)) {
481
+ const validateArgs = [validator, kitDir];
482
+ if (launcherAssets.skillsSourceDir) validateArgs.push('--public-skills', launcherAssets.skillsSourceDir);
483
+ const validation = (options.spawnSync ?? spawnSync)(process.execPath, validateArgs, { stdio: 'pipe' });
484
+ if (validation.status !== 0) {
485
+ log('キットの検査に失敗しました。展開済みファイルを確認してください。');
486
+ return { exitCode: 1 };
487
+ }
488
+ } else {
489
+ // npm 配布物には schemas の検査 bin が無い場合がある。runtime と同じく、
490
+ // 器の欠落で購入済みコンテンツを利用不能にしないため warning へ degrade する。
491
+ log('キットの検査ツールが見つからないため検査をスキップしました');
492
+ }
493
+ const requirementWarnings = [];
494
+ let runtimeIds;
495
+ const runtimePath = path.join(launcherAssets.repoRoot, 'packages', 'overlay-runtime', 'runtimes.mjs');
496
+ if (existsSync(runtimePath)) {
497
+ try {
498
+ const runtimeModule = await import(pathToFileURL(runtimePath).href);
499
+ runtimeIds = runtimeModule.runtimes.map((runtime) => runtime.id);
500
+ } catch {
501
+ runtimeIds = null;
502
+ }
503
+ }
504
+ if (!runtimeIds) {
505
+ // overlay-runtime は launcher の npm tarball に同梱されない。照合不能は
506
+ // manifest 不備ではないため warning に落とし、要求 id を既知扱いして続行する。
507
+ requirementWarnings.push('runtime registry が見つからないため runtime id の照合をスキップしました。');
508
+ runtimeIds = manifest.requires?.runtimes ?? [];
509
+ }
510
+ let entitledProductIds = [];
511
+ const credentials = readCredentials(env);
512
+ if (credentials) {
513
+ const entitlementResult = await fetchStoreEntitlements(fetchImpl, credentials.url, credentials.token);
514
+ entitledProductIds = (entitlementResult.data?.entitlements ?? [])
515
+ .map((entry) => entry.product_id ?? entry.id)
516
+ .filter(Boolean);
517
+ }
518
+ const requires = checkRequires(manifest, {
519
+ cliVersion: options.cliVersion ?? readOwnVersion(),
520
+ runtimeIds,
521
+ entitledProductIds
522
+ });
523
+ if (manifest.id !== productId) requires.blockers.push(`manifest id が商品 id と一致しません: ${manifest.id} != ${productId}`);
524
+ requires.ok = requires.blockers.length === 0;
525
+ for (const warning of [...requirementWarnings, ...requires.warnings]) log(`警告: ${warning}`);
526
+ if (!requires.ok) {
527
+ for (const blocker of requires.blockers) log(`導入できません: ${blocker}`);
528
+ return { exitCode: 1 };
529
+ }
530
+
531
+ const hadInstalledKit = readKitsLedger(home).kits.length > 0;
532
+ const assetLinks = linkKitAssets(kitDir, manifest, home, {
533
+ assets: options.assets,
534
+ spawnSyncImpl: options.spawnSync,
535
+ platform: options.platform
536
+ });
537
+ const skillLinks = linkKitSkills(kitDir, manifest, home, { platform: options.platform });
538
+ for (const warning of [...assetLinks.warnings, ...skillLinks.warnings]) log(`警告: ${warning}`);
539
+ if (skillLinks.blockers.length > 0) {
540
+ for (const blocker of skillLinks.blockers) log(`導入できません: ${blocker}`);
541
+ return { exitCode: 1 };
542
+ }
543
+ writeKitsLedger(home, {
544
+ id: manifest.id,
545
+ version: manifest.version,
546
+ installedAt: new Date().toISOString(),
547
+ kitDir,
548
+ skills: skillLinks.linked,
549
+ assets: assetLinks.linked
550
+ });
551
+ ensureKitsMarketplace(home);
552
+ if (!hadInstalledKit) log(enableHint());
553
+ }
418
554
  const packPath = findFile(destDir, 'PACK.json');
419
555
  if (packPath) {
420
556
  const items = registerInstalledPack(env, productId, packPath);
@@ -436,10 +572,11 @@ export async function runStoreCommand(args, options = {}) {
436
572
  return { exitCode: 0 };
437
573
  }
438
574
 
439
- log('使い方: akari store <connect|status|install|download|disconnect>');
575
+ log('使い方: akari store <connect|status|install|uninstall|download|disconnect>');
440
576
  log(' connect ブラウザで承認して接続(既定。--token akst_... で手動 / --no-open でブラウザを開かない / --url <base>)');
441
577
  log(' status 接続状態と購入済み一覧');
442
578
  log(' install <productId> [--from <zip>] 購入済み商品の導入(--from は手元 zip / PACK.json 素材を installed 索引へ登録)');
579
+ log(' uninstall <productId> 拡張キットの symlink と台帳登録を解除(展開済みファイルは保持)');
443
580
  log(' download <productId> [--dest <dir>] 購入済み配布物の取得のみ');
444
581
  log(' disconnect 接続解除');
445
582
  return { exitCode: sub ? 1 : 0 };
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { resolveLauncherAssets } from "./repo-assets.mjs";
4
+
5
+ const USAGE = "使い方: akari storyboard <projectDir> [--no-capture] [--captures <dir>] [--out <dir>]";
6
+
7
+ export async function runStoryboardCommand(args, options = {}) {
8
+ const log = options.log ?? ((line) => console.log(line));
9
+ if (args.includes("--help")) {
10
+ log(USAGE);
11
+ return { exitCode: 0 };
12
+ }
13
+
14
+ const logError = options.logError ?? ((line) => console.error(line));
15
+ const assets = options.assets ?? resolveLauncherAssets();
16
+ const spawn = options.spawn ?? spawnSync;
17
+ if (!assets.storyboardScript) {
18
+ logError("akari storyboard の実行スクリプトが見つかりません。完全な AKARI Video を再導入してください:");
19
+ logError(" npm install -g akari-video");
20
+ return { exitCode: 2 };
21
+ }
22
+ const result = spawn(process.execPath, [assets.storyboardScript, ...args], { stdio: "inherit" });
23
+ return { exitCode: typeof result?.status === "number" ? result.status : 1 };
24
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ import { resolveLauncherAssets } from './repo-assets.mjs';
6
+
7
+ export async function runWorldCommand(args, options = {}) {
8
+ const logError = options.logError ?? ((line) => console.error(line));
9
+ const assets = options.assets ?? resolveLauncherAssets();
10
+ const script = assets.repoRoot ? path.join(assets.repoRoot, 'packages', 'akari-tools', 'bin', 'world.mjs') : null;
11
+ if (!script || !existsSync(script)) {
12
+ logError('内部コマンド world の実行スクリプトが見つかりません。AKARI Video の完全な checkout または配布物を確認してください。');
13
+ return { exitCode: 1 };
14
+ }
15
+ const result = (options.spawn ?? spawnSync)(process.execPath, [script, ...args], { stdio: 'inherit' });
16
+ return { exitCode: typeof result?.status === 'number' ? result.status : 1 };
17
+ }