akari-video 0.1.67 → 0.1.69

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 (30) hide show
  1. package/README.md +11 -0
  2. package/bin/akari.mjs +5 -1
  3. package/package.json +1 -1
  4. package/src/kits.mjs +238 -0
  5. package/src/messages.mjs +1 -0
  6. package/src/repo-assets.mjs +7 -1
  7. package/src/store-command.mjs +138 -1
  8. package/src/storyboard-command.mjs +24 -0
  9. package/src/world-command.mjs +17 -0
  10. package/vendor/.akari-capability-sources.json +3 -0
  11. package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +79 -0
  12. package/vendor/docs/contract-2026-09-13-world-map-v0.md +54 -0
  13. package/vendor/packages/akari-launcher/README.md +11 -0
  14. package/vendor/packages/akari-launcher/package.json +1 -1
  15. package/vendor/packages/akari-tools/package.json +3 -2
  16. package/vendor/packages/asset-resolver/src/state.mjs +32 -1
  17. package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +36 -0
  18. package/vendor/packages/edit-lint/src/edit-lint.mjs +2 -0
  19. package/vendor/packages/edit-lint/src/world-scene-declaration.mjs +64 -0
  20. package/vendor/packages/edit-store/lib/generation-meta-node.js +2 -11
  21. package/vendor/packages/edit-store/lib/generation-meta.d.ts +17 -0
  22. package/vendor/packages/edit-store/lib/generation-meta.js +44 -0
  23. package/vendor/packages/generate/README.md +1 -1
  24. package/vendor/packages/overlay-runtime/README.md +18 -0
  25. package/vendor/packages/overlay-runtime/package.json +2 -0
  26. package/vendor/packages/project-scaffold/src/index.mjs +33 -3
  27. package/vendor/packages/project-scaffold/test/kit-skill-adapter.test.mjs +38 -0
  28. package/vendor/skills/overlay-authoring/SKILL.md +1 -0
  29. package/vendor/skills/overlay-authoring/world.md +42 -0
  30. package/vendor/skills/render-cut/SKILL.md +1 -1
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
@@ -14,6 +14,8 @@ 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
16
  import { runGenerateCommand } from '../src/generate-command.mjs';
17
+ import { runStoryboardCommand } from '../src/storyboard-command.mjs';
18
+ import { runWorldCommand } from '../src/world-command.mjs';
17
19
  import { resolveRuntimePaths } from '../src/runtime-diagnostics.mjs';
18
20
  import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
19
21
  import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
@@ -36,7 +38,7 @@ async function printVersion() {
36
38
  // `--help` は claude/opencode へそのまま転送されてしまっていた — AKARI Video 自身の
37
39
  // コマンド一覧が一度も出ない行き止まりだったため新設した)。
38
40
  async function printCliHelp() {
39
- for (const line of describeCliHelp()) {
41
+ for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー']) {
40
42
  console.log(line);
41
43
  }
42
44
  return { exitCode: 0 };
@@ -82,6 +84,8 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
82
84
  : argv[0] === 'migrate' ? runMigrateCommand(argv.slice(1))
83
85
  : argv[0] === 'clean' ? runCleanCommand(argv.slice(1))
84
86
  : argv[0] === 'generate' ? runGenerateCommand(argv.slice(1))
87
+ : argv[0] === 'storyboard' ? runStoryboardCommand(argv.slice(1))
88
+ : argv[0] === 'world' ? runWorldCommand(argv.slice(1))
85
89
  : run(argv);
86
90
 
87
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.67",
3
+ "version": "0.1.69",
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": {
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
@@ -331,6 +331,7 @@ export function describeCliHelp() {
331
331
  ' status 接続状態を確認する',
332
332
  ' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
333
333
  ' generate 台本のビートから静止画クリップを生成',
334
+ ' storyboard タイムラインから印刷用の絵コンテを作成',
334
335
  ' akari clean [dir] 使い捨ての中間ファイルを一覧・削除(既定は一覧のみ)',
335
336
  '',
336
337
  '開発者向け:',
@@ -40,6 +40,7 @@ export const CAPTURE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin
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
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');
43
44
 
44
45
  /**
45
46
  * 指定ルート配下に同梱されているスキル正本・雛形・schemas・scaffold 実装・creator-root
@@ -62,6 +63,7 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
62
63
  const decisionLogScript = path.join(repoRoot, DECISION_LOG_SCRIPT_RELATIVE);
63
64
  const wordBookScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'word-book.mjs');
64
65
  const generateScript = path.join(repoRoot, GENERATE_CLI_RELATIVE);
66
+ const storyboardScript = path.join(repoRoot, STORYBOARD_CLI_RELATIVE);
65
67
 
66
68
  return {
67
69
  repoRoot,
@@ -82,7 +84,8 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
82
84
  mediaScript: existsSync(mediaScript) ? mediaScript : null,
83
85
  ...(existsSync(decisionLogScript) ? { decisionLogScript } : {}),
84
86
  ...(existsSync(wordBookScript) ? { wordBookScript } : {}),
85
- ...(existsSync(generateScript) ? { generateScript } : {})
87
+ ...(existsSync(generateScript) ? { generateScript } : {}),
88
+ ...(existsSync(storyboardScript) ? { storyboardScript } : {})
86
89
  };
87
90
  }
88
91
 
@@ -121,6 +124,9 @@ export function resolveLauncherAssets({
121
124
  ...(candidate.generateScript ?? vendor.generateScript
122
125
  ? { generateScript: candidate.generateScript ?? vendor.generateScript }
123
126
  : {}),
127
+ ...(candidate.storyboardScript ?? vendor.storyboardScript
128
+ ? { storyboardScript: candidate.storyboardScript ?? vendor.storyboardScript }
129
+ : {}),
124
130
  ...(candidate.decisionLogScript ?? vendor.decisionLogScript ? { decisionLogScript: candidate.decisionLogScript ?? vendor.decisionLogScript } : {}),
125
131
  ...(candidate.wordBookScript ?? vendor.wordBookScript
126
132
  ? { wordBookScript: candidate.wordBookScript ?? vendor.wordBookScript }
@@ -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
+ }
@@ -67,7 +67,9 @@
67
67
  "docs/contract-2026-09-06-cut-audio-split-v0.md",
68
68
  "docs/contract-2026-09-06-vgpu-layer-v0.md",
69
69
  "docs/contract-2026-09-12-review-session-viewer.md",
70
+ "docs/contract-2026-09-13-extension-kit-v0.md",
70
71
  "docs/contract-2026-09-13-generation-v0.md",
72
+ "docs/contract-2026-09-13-world-map-v0.md",
71
73
  "packages/akari-launcher/package.json",
72
74
  "packages/akari-launcher/README.md",
73
75
  "packages/akari-tools/package.json",
@@ -188,6 +190,7 @@
188
190
  "skills/overlay-authoring/telop.md",
189
191
  "skills/overlay-authoring/text-behind-person.md",
190
192
  "skills/overlay-authoring/thumbnail.md",
193
+ "skills/overlay-authoring/world.md",
191
194
  "skills/render-cut/SKILL.md",
192
195
  "skills/research-plan/competitor.md",
193
196
  "skills/research-plan/ideate.md",
@@ -0,0 +1,79 @@
1
+ # 設計契約 — 拡張キット v0(技術契約)
2
+
3
+ ## 1. 定義と構成
4
+
5
+ 拡張キットは、公開されている AKARI Video の器へスキル・テンプレート・素材・説明書を追加するコンテキスト束である。既存の `akari store install <productId>` が次へ展開する。
6
+
7
+ ```text
8
+ ~/.akari/assets/store/<productId>/
9
+ ├── manifest.json
10
+ ├── skills/<skill-name>/SKILL.md
11
+ ├── templates/<name>.json
12
+ ├── assets/<category>/<id>/{meta.json, …}
13
+ ├── docs/*.md
14
+ ├── README.md
15
+ ├── LICENSE.md
16
+ └── checksums.txt
17
+ ```
18
+
19
+ `productId` は Store の商品 id、`version` は商品の整数版である。キット全体のライセンスは `LicenseRef-AKARI-Assets-v0` とし、各素材の `meta.json` も個別の `license` を持つ。
20
+
21
+ ## 2. `manifest.json`
22
+
23
+ `schemaVersion: 1` の manifest は additive-only とする。例:
24
+
25
+ ```json
26
+ {
27
+ "schemaVersion": 1,
28
+ "id": "world-kit",
29
+ "kind": "kit",
30
+ "name": "ワールドキット",
31
+ "version": 1,
32
+ "requires": {
33
+ "cli": ">=0.1.70",
34
+ "runtimes": ["world", "three"],
35
+ "products": ["akari-pop-motion-set"]
36
+ },
37
+ "skills": [{ "dir": "skills/design-world", "name": "design-world" }],
38
+ "templates": [{ "path": "templates/paper-to-browser.json", "for": "world-map", "label": "紙の地図 → ブラウザの中" }],
39
+ "assets": [{ "category": "overlay", "id": "world-far-bands" }],
40
+ "docs": [{ "path": "docs/world-hen.md", "label": "ワールド編(抜粋)" }],
41
+ "license": "LicenseRef-AKARI-Assets-v0",
42
+ "provenance": { "author": "AKARI Labs", "source": "example:world-kit" }
43
+ }
44
+ ```
45
+
46
+ 必須フィールドは `schemaVersion`、`id`、`kind`、`name`、`version`、`requires.cli`、`license`。`kind` は v0 では `kit` のみ。`validate-kit-manifest.mjs` はスキル実体、素材メタデータ、テンプレート用途語彙を含めて検査する。
47
+
48
+ ## 3. 展開
49
+
50
+ `akari store install <productId>` は従来の entitlement 確認、zip 取得、checksum 照合、展開を終えたあと、`manifest.json` がある商品だけ次を行う。
51
+
52
+ 1. manifest と `requires` を検査する。CLI または runtime の不足は fail-closed、依存商品の不足は警告と導入案内にする。
53
+ 2. `assets[]` を `~/.akari/assets/<category>/<id>` へ相対 symlink で公開する。各素材はリンク前に `validate-asset.mjs` で検査する。
54
+ 3. `skills[]` を `~/.akari/kits/plugin/skills/<name>` へ相対 symlink で公開する。
55
+ 4. `~/.akari/kits/installed.json` に id、version、導入日時、展開先、スキル、素材を記録する。
56
+ 5. `templates[]` は移動せず、CLI が各展開先の manifest を列挙して読む。
57
+
58
+ manifest が無い商品は従来の素材商品として扱う。検査器や runtime registry が npm 配布物に同梱されておらず照合できない場合は、その検査だけを警告付きでスキップする。`akari store uninstall <productId>` は台帳に記録した symlink と台帳エントリを外し、再導入用の展開ディレクトリは残す。
59
+
60
+ ## 4. スキルの発見
61
+
62
+ Claude Code 向けには `~/.akari/kits` を directory marketplace として生成する。`plugin/skills/` は全キットの合成ディレクトリで、名前空間は純正の `akari:` と分離した `akari-kits:` になる。初回だけ次を実行する。
63
+
64
+ ```sh
65
+ claude plugin marketplace add ~/.akari/kits
66
+ claude plugin install akari-kits@akari-kits
67
+ ```
68
+
69
+ `claude` が PATH に無い場合は、Claude Code のプラグイン設定で `~/.akari/kits` を marketplace として追加する。SessionStart hook は導入済みキットがあり、`enabledPlugins` に `akari-kits@akari-kits` が無い場合だけこの案内を 1 行表示し、設定を変更しない。
70
+
71
+ Codex、Cursor、opencode では、プロジェクトの `.agents/.codex/.cursor/.opencode/skills` へキットスキルも合成する。同名があれば純正スキルを優先する。plugin が利用できない環境でも `~/.akari/kits/plugin/skills/<name>/SKILL.md` を直接読める。
72
+
73
+ ## 5. アプリ(ホームの拡張キットカード)
74
+
75
+ ホームの AKARI Store カードの隣に拡張キットカードを 1 枚出し、未接続では出さず、導入済みは id・version・スキル名・素材数の一覧と未有効化時の有効化案内、購入済み・未導入は `akari store install <id>` の案内、未購入は教材の引換ページの案内、という 3 状態とする(アプリはコマンドを実行せず、コピーと外部ブラウザ起動だけを行う)。
76
+
77
+ ## 6. 更新と版
78
+
79
+ キットの版は Store の整数版とする。`akari store status` が導入済みの id、version、スキル名、素材数を表示し、同じ `akari store install` で新版へ置換する。