akari-video 0.1.69 → 0.1.71

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 (32) hide show
  1. package/bin/akari.mjs +1 -1
  2. package/package.json +1 -1
  3. package/src/internal-command.mjs +4 -6
  4. package/src/kits.mjs +95 -3
  5. package/src/repo-assets.mjs +44 -13
  6. package/src/store-command.mjs +2 -0
  7. package/src/vendor-sources.mjs +47 -0
  8. package/src/world-command.mjs +2 -2
  9. package/vendor/.akari-capability-sources.json +3 -0
  10. package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +2 -1
  11. package/vendor/docs/contract-2026-09-13-world-map-v0.md +24 -4
  12. package/vendor/packages/akari-launcher/package.json +1 -1
  13. package/vendor/packages/asset-resolver/src/library.mjs +13 -6
  14. package/vendor/packages/asset-resolver/src/resolve.mjs +4 -3
  15. package/vendor/packages/asset-resolver/test/resolve-kit-symlink.test.mjs +69 -0
  16. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/LICENSE.md +3 -0
  17. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/README.md +3 -0
  18. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/assets/overlay/sample-kit-frame/fragment.html +10 -0
  19. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/assets/overlay/sample-kit-frame/meta.json +24 -0
  20. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/assets/overlay/sample-kit-frame/preview.png +0 -0
  21. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/manifest.json +25 -0
  22. package/vendor/packages/schemas/examples/kit-manifest-v1-with-asset/skills/sample-kit-skill/SKILL.md +8 -0
  23. package/vendor/skills/design-world/SKILL.md +94 -0
  24. package/vendor/skills/design-world/bin/expand-template.mjs +198 -0
  25. package/vendor/skills/design-world/bin/test/expand-template.test.mjs +152 -0
  26. package/vendor/skills/design-world/bin/test/package.json +4 -0
  27. package/vendor/skills/design-world/guide.md +17 -0
  28. package/vendor/skills/design-world/templates/browser-to-chat.json +39 -0
  29. package/vendor/skills/design-world/templates/paper-to-browser.json +38 -0
  30. package/vendor/skills/design-world/templates/street-to-room.json +39 -0
  31. package/vendor/skills/design-world/world.md +62 -0
  32. package/vendor/skills/overlay-authoring/world.md +39 -0
package/bin/akari.mjs CHANGED
@@ -38,7 +38,7 @@ async function printVersion() {
38
38
  // `--help` は claude/opencode へそのまま転送されてしまっていた — AKARI Video 自身の
39
39
  // コマンド一覧が一度も出ない行き止まりだったため新設した)。
40
40
  async function printCliHelp() {
41
- for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー']) {
41
+ for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー・停留所移動']) {
42
42
  console.log(line);
43
43
  }
44
44
  return { exitCode: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
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": {
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
 
5
- import { resolveLauncherAssets } from './repo-assets.mjs';
5
+ import { FINGER_FRAME_SCRIPT_RELATIVE, resolveLauncherAssets } from './repo-assets.mjs';
6
6
 
7
7
  const commands = [
8
8
  'beat-sync-beatmap',
@@ -31,12 +31,10 @@ export async function runInternalCommand(args, options = {}) {
31
31
  return { exitCode: 0 };
32
32
  }
33
33
 
34
- // vision-finger-frame task/2026-08-11-finger-frame-generator の境界規約により
35
- // repo-assets.mjs(別タスク task/2026-08-11-eye-bar-generator と衝突しやすい共有ファイル)を
36
- // 編集せず、他コマンドと違い assets.repoRoot から自己解決する(resolveRepoAssets() 側に
37
- // フィールドを追加していない唯一の例外 -- 経緯は非公開の内部記録を参照)。
34
+ // vision-finger-frame は他コマンドと違い assets.repoRoot から自己解決するが、
35
+ // 相対パス自体は配布検査と共有する repo-assets.mjs の正本を使う。
38
36
  const fingerFrameScript = assets.repoRoot
39
- ? path.join(assets.repoRoot, 'packages', 'akari-tools', 'bin', 'finger-frame.mjs')
37
+ ? path.join(assets.repoRoot, FINGER_FRAME_SCRIPT_RELATIVE)
40
38
  : null;
41
39
 
42
40
  const definitions = {
package/src/kits.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import {
3
4
  existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync,
4
- renameSync, rmSync, symlinkSync, writeFileSync
5
+ readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync
5
6
  } from 'node:fs';
6
7
  import path from 'node:path';
7
8
 
8
9
  import { resolveLauncherAssets } from './repo-assets.mjs';
9
10
 
10
11
  const KITS_SCHEMA = 'akari-installed-kits/v0';
12
+ const INSTALLED_ASSETS_SCHEMA = 'akari-installed-assets/v0';
11
13
  const PLUGIN_DESCRIPTION = 'AKARI Video 拡張キットのスキルをまとめて提供するローカルプラグイン。';
12
14
 
13
15
  function parseVersion(value) {
@@ -93,9 +95,47 @@ function replaceSymlink(source, destination, {
93
95
  }
94
96
  }
95
97
 
98
+ function listAssetFiles(assetRoot, current = assetRoot) {
99
+ const files = [];
100
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
101
+ const filePath = path.join(current, entry.name);
102
+ const stat = statSync(filePath);
103
+ if (stat.isDirectory()) {
104
+ files.push(...listAssetFiles(assetRoot, filePath));
105
+ } else if (stat.isFile()) {
106
+ const content = readFileSync(filePath);
107
+ files.push({
108
+ path: path.relative(assetRoot, filePath).split(path.sep).join('/'),
109
+ bytes: stat.size,
110
+ sha256: createHash('sha256').update(content).digest('hex')
111
+ });
112
+ }
113
+ }
114
+ return files.sort((left, right) => left.path.localeCompare(right.path));
115
+ }
116
+
117
+ function installedAssetItem(source, asset, manifest) {
118
+ const assetRoot = realpathSync(source);
119
+ let title = asset.id;
120
+ try {
121
+ const meta = JSON.parse(readFileSync(path.join(assetRoot, 'meta.json'), 'utf8'));
122
+ if (typeof meta.title === 'string' && meta.title) title = meta.title;
123
+ } catch {
124
+ // validate-asset が検査済み。配布環境で検査器が無い場合だけ id へフォールバックする。
125
+ }
126
+ return {
127
+ id: asset.id,
128
+ title,
129
+ path: ['assets', asset.category, asset.id].join('/'),
130
+ version: manifest.version,
131
+ files: listAssetFiles(assetRoot)
132
+ };
133
+ }
134
+
96
135
  export function linkKitAssets(kitDir, manifest, home, options = {}) {
97
136
  const warnings = [];
98
137
  const linked = [];
138
+ const items = [];
99
139
  const assets = options.assets?.schemasSourceDir !== undefined
100
140
  ? options.assets
101
141
  : resolveLauncherAssets(options.assets);
@@ -113,9 +153,14 @@ export function linkKitAssets(kitDir, manifest, home, options = {}) {
113
153
  warnings.push(`素材 ${asset.category}/${asset.id} の検査ツールが見つからないため検査をスキップしました。`);
114
154
  }
115
155
  const destination = path.join(home, 'assets', asset.category, asset.id);
156
+ const productRoot = path.join(home, 'assets', 'store', manifest.id);
157
+ const kitSubdir = path.relative(productRoot, kitDir);
116
158
  const result = replaceSymlink(source, destination, {
117
159
  ...options,
118
- relativeTarget: path.join('..', '..', 'assets', 'store', manifest.id, 'assets', asset.category, asset.id)
160
+ relativeTarget: path.join(
161
+ '..', '..', 'assets', 'store', manifest.id, kitSubdir,
162
+ 'assets', asset.category, asset.id
163
+ )
119
164
  });
120
165
  if (result.status === 'occupied') {
121
166
  warnings.push(`既存の実ディレクトリを保持しました: ${destination}`);
@@ -123,9 +168,55 @@ export function linkKitAssets(kitDir, manifest, home, options = {}) {
123
168
  warnings.push(`symlink を作成できませんでした(Windows の権限を確認してください): ${destination}`);
124
169
  } else {
125
170
  linked.push({ category: asset.category, id: asset.id });
171
+ items.push(installedAssetItem(source, asset, manifest));
126
172
  }
127
173
  }
128
- return { linked, warnings };
174
+ return { linked, items, warnings };
175
+ }
176
+
177
+ function readInstalledAssetsIndex(home) {
178
+ const indexPath = path.join(home, 'assets', 'installed.json');
179
+ if (!existsSync(indexPath)) return { schema: INSTALLED_ASSETS_SCHEMA, packs: {} };
180
+ const index = JSON.parse(readFileSync(indexPath, 'utf8'));
181
+ if (index?.schema !== INSTALLED_ASSETS_SCHEMA
182
+ || !index.packs || typeof index.packs !== 'object' || Array.isArray(index.packs)) {
183
+ throw new Error(`導入済み素材索引の形式が想定と違います: ${indexPath}`);
184
+ }
185
+ return index;
186
+ }
187
+
188
+ function writeInstalledAssetsIndex(home, index) {
189
+ const indexPath = path.join(home, 'assets', 'installed.json');
190
+ mkdirSync(path.dirname(indexPath), { recursive: true });
191
+ const temporary = `${indexPath}.tmp-${process.pid}`;
192
+ writeFileSync(temporary, `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
193
+ renameSync(temporary, indexPath);
194
+ }
195
+
196
+ export function registerKitAssets(home, manifest, kitDir, items) {
197
+ const root = path.resolve(kitDir);
198
+ const storeRoot = path.join(path.resolve(home), 'assets', 'store', manifest.id);
199
+ if (root !== storeRoot && !root.startsWith(`${storeRoot}${path.sep}`)) {
200
+ throw new Error(`キット素材の root が展開先の外を指しています: ${root}`);
201
+ }
202
+ const index = readInstalledAssetsIndex(home);
203
+ index.packs[manifest.id] = {
204
+ version: manifest.version,
205
+ installedAt: new Date().toISOString(),
206
+ root,
207
+ items
208
+ };
209
+ writeInstalledAssetsIndex(home, index);
210
+ return items;
211
+ }
212
+
213
+ function unregisterKitAssets(home, productId) {
214
+ const indexPath = path.join(home, 'assets', 'installed.json');
215
+ if (!existsSync(indexPath)) return;
216
+ const index = readInstalledAssetsIndex(home);
217
+ if (!Object.hasOwn(index.packs, productId)) return;
218
+ delete index.packs[productId];
219
+ writeInstalledAssetsIndex(home, index);
129
220
  }
130
221
 
131
222
  export function linkKitSkills(kitDir, manifest, home, options = {}) {
@@ -198,6 +289,7 @@ export function removeKit(home, productId) {
198
289
  const temporary = `${ledgerPath}.tmp-${process.pid}`;
199
290
  writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
200
291
  renameSync(temporary, ledgerPath);
292
+ unregisterKitAssets(home, productId);
201
293
  return true;
202
294
  }
203
295
 
@@ -19,28 +19,59 @@ const SKILLS_MARKER = path.join('skills', 'analyze-footage', 'SKILL.md');
19
19
  // 雛形側の .gitignore 実体は project-scaffold の writeFallbackTemplate が補完する。
20
20
  const TEMPLATE_MARKER = path.join('templates', 'project-default', 'CLAUDE.md');
21
21
  const SCHEMAS_MARKER = path.join('packages', 'schemas', 'analysis.schema.json');
22
- const DOCTOR_SCRIPT_RELATIVE = path.join('skills', 'manage-connections', 'bin', 'doctor.mjs');
23
- const SCAFFOLD_MODULE_RELATIVE = path.join('packages', 'project-scaffold', 'src', 'index.mjs');
22
+ export const DOCTOR_SCRIPT_RELATIVE = path.join('skills', 'manage-connections', 'bin', 'doctor.mjs');
23
+ export const SCAFFOLD_MODULE_RELATIVE = path.join('packages', 'project-scaffold', 'src', 'index.mjs');
24
24
  // 作業場(creator-root)モジュール。① Wave(packages/creator-root)の成果物で、本パッケージ
25
25
  // からは読み取り専用(動的 import のみ)。scaffoldModulePath と同型の解決方式。
26
- const CREATOR_ROOT_MODULE_RELATIVE = path.join('packages', 'creator-root', 'src', 'index.mjs');
26
+ export const CREATOR_ROOT_MODULE_RELATIVE = path.join('packages', 'creator-root', 'src', 'index.mjs');
27
27
  // 公式音源ライブラリ(AKARI Sounds)の一括取得スクリプト。初回動線(sounds-setup.mjs)と
28
28
  // `akari sounds` が子プロセスとして起動する。未同梱なら null(機能スキップ)。
29
- const AUDIO_FETCH_SCRIPT_RELATIVE = path.join('packages', 'audio-library-setup', 'bin', 'fetch-akari-sounds.mjs');
29
+ export const AUDIO_FETCH_SCRIPT_RELATIVE = path.join('packages', 'audio-library-setup', 'bin', 'fetch-akari-sounds.mjs');
30
30
  // 素材 resolver(アカウントの素材 = 無料 + 購入済みの一覧・取得)の CLI 実体。
31
31
  // `akari assets <list|fetch|sync|...>`(assets-command.mjs)が子プロセスとして起動する。
32
32
  // 未同梱なら null(`akari assets` はその旨のエラーを返す。他コマンドは無影響)。
33
- const ASSET_RESOLVER_CLI_RELATIVE = path.join('packages', 'asset-resolver', 'bin', 'akari-assets.mjs');
34
- const BEATMAP_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'beatmap.mjs');
35
- const PROBE_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'probe-frame.mjs');
36
- const DECISION_LOG_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'decision-log.mjs');
37
- const CAPTIONS_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'captions.mjs');
33
+ export const ASSET_RESOLVER_CLI_RELATIVE = path.join('packages', 'asset-resolver', 'bin', 'akari-assets.mjs');
34
+ export const BEATMAP_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'beatmap.mjs');
35
+ export const PROBE_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'probe-frame.mjs');
36
+ export const DECISION_LOG_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'decision-log.mjs');
37
+ export const CAPTIONS_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'captions.mjs');
38
38
  // capture-command.mjs のエラー文と apps/shell の同梱テストが同じ相対パスを名指しできるよう export する。
39
39
  export const CAPTURE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'capture.mjs');
40
- const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
41
- const EYE_BAR_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'eye-bar.mjs');
40
+ export const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
41
+ export const EYE_BAR_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'eye-bar.mjs');
42
+ export const FINGER_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'finger-frame.mjs');
43
+ export const MEDIA_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'media.mjs');
44
+ export const WORD_BOOK_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'word-book.mjs');
42
45
  export const GENERATE_CLI_RELATIVE = path.join('packages', 'generate', 'src', 'cli', 'index.mjs');
43
46
  export const STORYBOARD_CLI_RELATIVE = path.join('packages', 'decision-cards', 'render-storyboard-print.mjs');
47
+ export const WORLD_CLI_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'world.mjs');
48
+ export const WORLD_VALIDATOR_RELATIVE = path.join('packages', 'schemas', 'bin', 'validate-world-map.mjs');
49
+
50
+ // launcher が直接または起動した CLI の子プロセスとして解決する実行体の正本。
51
+ // launcher-assets は resolveLauncherAssets() が資産フィールド単位で vendor を補完できる経路、
52
+ // resources は assets.repoRoot または実行中 CLI の位置から自己解決し、vendor を見ない経路。
53
+ // relative は上の解決定数だけから組み立て、配布検査と実行時解決の文字列を乖離させない。
54
+ export const LAUNCHER_SUBCOMMAND_EXECUTABLES = [
55
+ { command: '接続 doctor(manage-connections)', relative: DOCTOR_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
56
+ { command: 'akari new', relative: SCAFFOLD_MODULE_RELATIVE, resolution: 'launcher-assets' },
57
+ { command: '初回動線(first-run)の作業場モジュール', relative: CREATOR_ROOT_MODULE_RELATIVE, resolution: 'launcher-assets' },
58
+ { command: 'akari sounds', relative: AUDIO_FETCH_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
59
+ { command: 'akari assets', relative: ASSET_RESOLVER_CLI_RELATIVE, resolution: 'launcher-assets' },
60
+ { command: 'akari internal beat-sync-beatmap', relative: BEATMAP_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
61
+ { command: 'akari internal beat-sync-probe-frame', relative: PROBE_FRAME_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
62
+ { command: 'akari decision-log', relative: DECISION_LOG_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
63
+ { command: 'akari captions', relative: CAPTIONS_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
64
+ { command: 'akari capture', relative: CAPTURE_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
65
+ { command: 'akari internal beat-sync-render-when-idle', relative: RENDER_WHEN_IDLE_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
66
+ { command: 'akari internal eye-bar', relative: EYE_BAR_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
67
+ { command: 'akari internal vision-finger-frame', relative: FINGER_FRAME_SCRIPT_RELATIVE, resolution: 'resources' },
68
+ { command: 'akari media', relative: MEDIA_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
69
+ { command: 'akari word-book', relative: WORD_BOOK_SCRIPT_RELATIVE, resolution: 'launcher-assets' },
70
+ { command: 'akari generate', relative: GENERATE_CLI_RELATIVE, resolution: 'launcher-assets' },
71
+ { command: 'akari storyboard', relative: STORYBOARD_CLI_RELATIVE, resolution: 'launcher-assets' },
72
+ { command: 'akari world', relative: WORLD_CLI_RELATIVE, resolution: 'resources' },
73
+ { command: 'akari world check', relative: WORLD_VALIDATOR_RELATIVE, resolution: 'resources' }
74
+ ];
44
75
 
45
76
  /**
46
77
  * 指定ルート配下に同梱されているスキル正本・雛形・schemas・scaffold 実装・creator-root
@@ -59,9 +90,9 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
59
90
  const probeFrameScript = path.join(repoRoot, PROBE_FRAME_SCRIPT_RELATIVE);
60
91
  const renderWhenIdleScript = path.join(repoRoot, RENDER_WHEN_IDLE_SCRIPT_RELATIVE);
61
92
  const eyeBarScript = path.join(repoRoot, EYE_BAR_SCRIPT_RELATIVE);
62
- const mediaScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'media.mjs');
93
+ const mediaScript = path.join(repoRoot, MEDIA_SCRIPT_RELATIVE);
63
94
  const decisionLogScript = path.join(repoRoot, DECISION_LOG_SCRIPT_RELATIVE);
64
- const wordBookScript = path.join(repoRoot, 'packages', 'akari-tools', 'bin', 'word-book.mjs');
95
+ const wordBookScript = path.join(repoRoot, WORD_BOOK_SCRIPT_RELATIVE);
65
96
  const generateScript = path.join(repoRoot, GENERATE_CLI_RELATIVE);
66
97
  const storyboardScript = path.join(repoRoot, STORYBOARD_CLI_RELATIVE);
67
98
 
@@ -28,6 +28,7 @@ import {
28
28
  linkKitSkills,
29
29
  readKitManifest,
30
30
  readKitsLedger,
31
+ registerKitAssets,
31
32
  removeKit,
32
33
  writeKitsLedger
33
34
  } from './kits.mjs';
@@ -540,6 +541,7 @@ export async function runStoreCommand(args, options = {}) {
540
541
  for (const blocker of skillLinks.blockers) log(`導入できません: ${blocker}`);
541
542
  return { exitCode: 1 };
542
543
  }
544
+ registerKitAssets(home, manifest, kitDir, assetLinks.items);
543
545
  writeKitsLedger(home, {
544
546
  id: manifest.id,
545
547
  version: manifest.version,
@@ -0,0 +1,47 @@
1
+ // prepack の vendor コピー、配布同梱ゲート、GPL 再配布ゲートが、vendor/ へ入る
2
+ // リポジトリ相対パスを同じ prefix 規則で判定するための共有正本。prepack.mjs は
3
+ // import 時にコピーを始める副作用モジュールなので、安全に共有できるデータだけを分離した。
4
+ // GPL 再配布ゲートは import せずテキストとして配列リテラルを読む。この宣言を
5
+ // `export const` + `VENDOR_SOURCES = [` の形とベタ書き文字列のまま保つこと。変数展開・spread・
6
+ // 他ファイルからの合成を入れると、安全に検査できないためゲートが fail-closed で落ちる。
7
+ export const VENDOR_SOURCES = [
8
+ 'skills',
9
+ 'templates/project-default',
10
+ 'presets/luts',
11
+ // edit-lint は外部 npm 依存ゼロだが、src から edit-store のビルド済み実装と
12
+ // textanim preset を参照する。既に同梱済みの schemas / audio-library-setup /
13
+ // media-bin と合わせ、CLI の実行時閉包を明示的に揃える。
14
+ 'presets/textanim',
15
+ 'assets/font/noto-sans-jp/NotoSansJP-Variable.ttf',
16
+ 'packages/schemas',
17
+ 'packages/project-scaffold',
18
+ // analysis-report は package.json / README.md を capability source として既に収集する。
19
+ // 実行に必要な CLI と同居必須テンプレートだけを追加し、test/ は配布しない。
20
+ 'packages/analysis-report/render-analysis-report.mjs',
21
+ 'packages/analysis-report/template.html',
22
+ // decision-log-report も package.json / README.md は capability source として収集する。
23
+ // 実行に必要な CLI と同居必須テンプレートだけを追加し、test/ は配布しない。
24
+ 'packages/decision-log-report/render-decision-log-report.mjs',
25
+ 'packages/decision-log-report/template.html',
26
+ 'packages/edit-lint/bin',
27
+ 'packages/edit-lint/src',
28
+ 'packages/edit-store/lib',
29
+ // 作業場(creator-root)モジュール。npm 配布時も初回動線(first-run.mjs 経由の
30
+ // 動的 import)が機能するよう同梱する。未同梱の場合は repo-assets.mjs 側で
31
+ // creatorRootModulePath が null になり、現行動作へフォールバックする。
32
+ 'packages/creator-root',
33
+ // 公式音源ライブラリ(AKARI Sounds)の一括取得(sounds-setup.mjs / `akari sounds`)。
34
+ // media-bin は fetch スクリプトの preview.png 生成(waveform-preview.mjs)が ffmpeg 解決に
35
+ // 使う。未同梱なら audioFetchScriptPath が null になり、音源セットアップだけスキップされる。
36
+ 'packages/audio-library-setup',
37
+ 'packages/media-bin',
38
+ // 素材 resolver(`akari assets` — アカウントの素材 = 無料 + 購入済みの一覧・取得)。
39
+ // 未同梱なら assetResolverCliPath が null になり、`akari assets` だけスキップされる
40
+ // (タスク契約 2026-08-09-agent-assets-discovery)。
41
+ 'packages/asset-resolver',
42
+ // 履歴に何を入れるかの宣言(history-policy.mjs)。project-scaffold が相対パスで参照するため、
43
+ // vendor ミラーにもモノレポと同じ深さで置く(`vendor/packages/akari-launcher/src/` →
44
+ // `vendor/packages/project-scaffold/src/` から `../../akari-launcher/src/` で解決できる)。
45
+ // 本体の src/ にも同じファイルが入るが、配布物の中で相対パスを 1 本に保つ方を採る。
46
+ 'packages/akari-launcher/src/history-policy.mjs'
47
+ ];
@@ -2,12 +2,12 @@ import { existsSync } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
 
5
- import { resolveLauncherAssets } from './repo-assets.mjs';
5
+ import { resolveLauncherAssets, WORLD_CLI_RELATIVE } from './repo-assets.mjs';
6
6
 
7
7
  export async function runWorldCommand(args, options = {}) {
8
8
  const logError = options.logError ?? ((line) => console.error(line));
9
9
  const assets = options.assets ?? resolveLauncherAssets();
10
- const script = assets.repoRoot ? path.join(assets.repoRoot, 'packages', 'akari-tools', 'bin', 'world.mjs') : null;
10
+ const script = assets.repoRoot ? path.join(assets.repoRoot, WORLD_CLI_RELATIVE) : null;
11
11
  if (!script || !existsSync(script)) {
12
12
  logError('内部コマンド world の実行スクリプトが見つかりません。AKARI Video の完全な checkout または配布物を確認してください。');
13
13
  return { exitCode: 1 };
@@ -152,6 +152,9 @@
152
152
  "skills/declare-audio/launch.md",
153
153
  "skills/declare-audio/SKILL.md",
154
154
  "skills/declare-audio/what-to-declare.md",
155
+ "skills/design-world/guide.md",
156
+ "skills/design-world/SKILL.md",
157
+ "skills/design-world/world.md",
155
158
  "skills/edit-lint/preview.md",
156
159
  "skills/edit-lint/SKILL.md",
157
160
  "skills/edit-plan/approvals-and-generation.md",
@@ -51,6 +51,7 @@
51
51
 
52
52
  1. manifest と `requires` を検査する。CLI または runtime の不足は fail-closed、依存商品の不足は警告と導入案内にする。
53
53
  2. `assets[]` を `~/.akari/assets/<category>/<id>` へ相対 symlink で公開する。各素材はリンク前に `validate-asset.mjs` で検査する。
54
+ 素材の実体ファイルと checksum は `~/.akari/assets/installed.json` にも登録し、素材 id から解決できるようにする。
54
55
  3. `skills[]` を `~/.akari/kits/plugin/skills/<name>` へ相対 symlink で公開する。
55
56
  4. `~/.akari/kits/installed.json` に id、version、導入日時、展開先、スキル、素材を記録する。
56
57
  5. `templates[]` は移動せず、CLI が各展開先の manifest を列挙して読む。
@@ -72,7 +73,7 @@ Codex、Cursor、opencode では、プロジェクトの `.agents/.codex/.cursor
72
73
 
73
74
  ## 5. アプリ(ホームの拡張キットカード)
74
75
 
75
- ホームの AKARI Store カードの隣に拡張キットカードを 1 枚出し、未接続では出さず、導入済みは id・version・スキル名・素材数の一覧と未有効化時の有効化案内、購入済み・未導入は `akari store install <id>` の案内、未購入は教材の引換ページの案内、という 3 状態とする(アプリはコマンドを実行せず、コピーと外部ブラウザ起動だけを行う)。
76
+ ホームの AKARI Store カードの隣に拡張キットカードを 1 枚出し、未接続では出さず、導入済みは id・version・スキル名・素材数の一覧と未有効化時の有効化案内、購入済み・未導入は `akari store install <id>` の案内、未購入は Lab の商品ページの案内(Lifetime パス対象)、という 3 状態とする(アプリはコマンドを実行せず、コピーと外部ブラウザ起動だけを行う)。
76
77
 
77
78
  ## 6. 更新と版
78
79
 
@@ -26,27 +26,47 @@
26
26
 
27
27
  flat world は 1 個の overlay 断片で構成する。Canvas 層は背景、格子、遠景、portal、cut の覆いを描き、DOM sheet 層は素材と文字を持つ。各 world は直下の `.akari-world-sheet[data-world]`、zone はその子の `.akari-world-zone[data-zone]` とし、sheet 自身は left / top 0、zone の px は bounds 原点を引かない world 座標そのままとする。sheet の transform は authoring 時に固定せず、ランタイムが `camera(t)` から設定する。DOM と Canvas の混在出力は rasterize 経路を使う。
28
28
 
29
- spatial world は three 断片で構成し、座標・床・背景・霧を宣言する。画面座標の 3D 小物は別 overlay item とする。
29
+ spatial world は `akari world build` `assets/world/world.glb` `overlays/world.html` の
30
+ three 断片へ決定論的に焼く。GLB は `worlds[].spatial.floor`、`background`、`haze`、
31
+ `zones[].c` の目印と、`camera(t)` を 60 Hz でサンプルした `TourCamera` / `Tour` clip を持つ。
32
+ three 宣言は `model`、`camera.fromModel: "TourCamera"`、`animationClip: "Tour"` に加え、
33
+ 先頭 world の `palette.haze` / `palette.background` がある場合だけ `fog` / `background` を持つ。
34
+ 画面座標の 3D 小物とテロップは別 overlay item とする。
30
35
 
31
36
  ## 4. CLI
32
37
 
33
38
  - `akari world check [--strict] [--migrate] [--json]`: スキーマと不変条件を検査し、必要なら v2 を v3 へ正規化する。
34
- - `akari world build`: flat world の宣言、sheet、zone、解決済み素材断片を `overlays/world.html` に生成し、edit.json の `world` item id 安定で upsert する。
35
- - `akari world preview [--measure]`: stop edge の代表時点を PNG`camera-proof.json` にする。measure 時は非 move edge の完全被覆区間を 30 Hz で測り、該当する `transition.cover` だけを書き戻す。
39
+ - `check --migrate` はラベル文字列または `null` の `cover` v3 語彙外の `pattern` を落として有限の暫定値へ正規化し、元の値と実測が必要な旨を注記に残す。
40
+ - `akari world build`: flat は宣言、sheet、zone、解決済み素材断片を `overlays/world.html` に生成する。spatial は世界 GLB three 断片を生成する。どちらも edit.json`world` item id 安定で upsert する。edit.json version 2 でなければ変更せず停止するため、先に `akari migrate <project-root>` を実行する。
41
+ - `akari world preview [--measure]`: flat / spatial とも rasterize 経路で stop と edge の代表時点を PNG と `camera-proof.json` にする。measure 時は非 move edge の全画素 RGB 標準偏差が 2 以下になる完全被覆区間を 30 Hz で測り、該当する `transition.cover` だけを書き戻す。
42
+ - `preview --measure` は入口では C7 を問わず、実測値を書き戻した後に C7 を含む全項目を検査する。
36
43
  - `akari world overview`: 外部通信を行わず `file://` で開ける自己完結の俯瞰 HTML を生成する。
44
+ - `akari world move-stop <project-root> --stop <id> --c x,y[,scale] [--json]`: flat の停留所座標だけを更新する。元テキストの整形と他の欄を変えず、bounds 外・spatial・不変条件違反では一切書き込まない。
37
45
 
38
46
  同じ入力から得る HTML と画像は決定論的でなければならない。素材 id は asset resolver で解決し、未解決時は失敗として扱う。
39
47
 
48
+ 実行順は flat / spatial 共通で、プロジェクトルートに対して次のようにする。
49
+
50
+ ```sh
51
+ akari world check . --migrate
52
+ akari world build .
53
+ akari world preview .
54
+ akari world preview . --measure
55
+ akari world overview .
56
+ ```
57
+
40
58
  ## 5. 地図 UI
41
59
 
42
60
  - 実装のマーカー判定は `akari-shell-strip` の ContextKey `akari.worldMap` に一元化する。
43
61
  - main の「地図」タブは `akari-world-view` が担う。
44
62
  - タイムラインのワールド帯と地図インスペクターは `akari-annotations` が担う。
45
63
 
46
- 地図 UI は world-map を読み取り専用で表示する。2D 俯瞰、ワールド帯、再生時刻に追従する撮影枠、選択中の stop / edge 詳細を提供し、データの編集機能は持たない。
64
+ 地図 UI は 2D 俯瞰、ワールド帯、再生時刻に追従する撮影枠、選択中の stop / edge 詳細を提供する。v1 では flat の停留所の座標だけを ⌥ ドラッグで `world-map.json` へ書き戻せる。書き手は `akari world move-stop` の 1 本に限定し、bounds 外・spatial・不変条件違反では書き込まない。`world-map.json` は edit.json の履歴の外にあるため、undo / redo は未対応とする。
47
65
 
48
66
  ## 6. 制作フロー
49
67
 
68
+ 作り方(ブリーフ → テンプレート → 台本 → `world-map.json` → `akari world`)は無料の純正スキル `akari:design-world`(`skills/design-world/SKILL.md`)が持つ。
69
+
50
70
  企画と絵コンテで章を world として宣言し、モーション区間は `world-map.json` → `akari world build` → overlay → 書き出しの順に処理する。実写区間との接点は portal とカットアウェイ章に限定する。
51
71
 
52
72
  ## 7. 将来拡張
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
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 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,14 +24,21 @@ export function scanLocalLibrary(home) {
24
24
  if (!existsSync(assetsDir)) return installed;
25
25
 
26
26
  for (const categoryEntry of readdirSync(assetsDir, { withFileTypes: true })) {
27
- if (!categoryEntry.isDirectory()) continue;
28
27
  const categoryDir = path.join(assetsDir, categoryEntry.name);
29
- for (const idEntry of readdirSync(categoryDir, { withFileTypes: true })) {
30
- if (!idEntry.isDirectory()) continue;
31
- const dir = path.join(categoryDir, idEntry.name);
32
- if (readdirSync(dir).length > 0) {
33
- installed.add(`${categoryEntry.name}/${idEntry.name}`);
28
+ try {
29
+ if (!statSync(categoryDir).isDirectory()) continue;
30
+ for (const idEntry of readdirSync(categoryDir, { withFileTypes: true })) {
31
+ const dir = path.join(categoryDir, idEntry.name);
32
+ try {
33
+ if (statSync(dir).isDirectory() && readdirSync(dir).length > 0) {
34
+ installed.add(`${categoryEntry.name}/${idEntry.name}`);
35
+ }
36
+ } catch {
37
+ // 壊れた symlink や読めない素材は取得済みとして数えない。
38
+ }
34
39
  }
40
+ } catch {
41
+ // ファイルや壊れた category symlink は対象外。
35
42
  }
36
43
  }
37
44
  return installed;
@@ -11,7 +11,7 @@
11
11
  // checksums.txt 検証(paid-zip.mjs)→ 同じ validate-asset / 原子的 move の経路に合流する。
12
12
 
13
13
  import { spawnSync } from 'node:child_process';
14
- import { constants, existsSync } from 'node:fs';
14
+ import { constants, existsSync, realpathSync } from 'node:fs';
15
15
  import { cp, mkdir, mkdtemp, rename, rm } from 'node:fs/promises';
16
16
  import path from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
@@ -38,6 +38,7 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
38
38
  // 素材箱側が「meta.json を含むディレクトリ = 1 カード」でグルーピングする際、
39
39
  // 深さではなくディレクトリ形で判定するため、置き場の形をライブラリと合わせておく必要はないが、
40
40
  // カテゴリ別に整理された配置の方が人間が見ても分かりやすいのでライブラリ型に統一する。
41
+ const realSourceDir = realpathSync(sourceDir);
41
42
  const dest = path.join(path.resolve(projectDir), 'assets', category, id);
42
43
  await mkdir(path.dirname(dest), { recursive: true });
43
44
  await rm(dest, { recursive: true, force: true });
@@ -46,7 +47,7 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
46
47
  // このマシンの Node(libuv)は clonefileat 相当が ENOSYS を返し、fs.cp の
47
48
  // COPYFILE_FICLONE では節約が効かない(前段 2026-08-09-project-copy-cow-clone で実測確認済み)。
48
49
  // BSD cp -c は clonefile(2) を Node を介さず直接使うため、同じ OS/FS 上で実際にクローンできる。
49
- const clone = spawnSync('/bin/cp', ['-Rc', sourceDir, dest], { stdio: 'ignore' });
50
+ const clone = spawnSync('/bin/cp', ['-Rc', realSourceDir, dest], { stdio: 'ignore' });
50
51
  if (!clone.error && clone.status === 0) {
51
52
  return dest;
52
53
  }
@@ -57,7 +58,7 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
57
58
 
58
59
  // COPYFILE_FICLONE(_FORCE ではない): 対応 FS(APFS 等)では CoW クローンで実体化コピーを
59
60
  // 省略し、非対応環境では黙って通常コピーへフォールバックする(失敗しない)。
60
- await cp(sourceDir, dest, { recursive: true, mode: constants.COPYFILE_FICLONE });
61
+ await cp(realSourceDir, dest, { recursive: true, mode: constants.COPYFILE_FICLONE });
61
62
  return dest;
62
63
  }
63
64
 
@@ -0,0 +1,69 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import {
4
+ lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync
5
+ } from 'node:fs';
6
+ import path from 'node:path';
7
+ import test from 'node:test';
8
+ import { isAssetCached, scanLocalLibrary } from '../src/library.mjs';
9
+ import { resolve as resolveAsset } from '../src/resolve.mjs';
10
+ import { setupFixtureEnv } from './helpers.mjs';
11
+
12
+ function sha256(value) {
13
+ return createHash('sha256').update(value).digest('hex');
14
+ }
15
+
16
+ test('キット素材の symlink 越し fetch はプロジェクトへ実体をコピーする', async () => {
17
+ const { env, home, root } = setupFixtureEnv();
18
+ try {
19
+ const id = 'sample-kit-frame';
20
+ const category = 'overlay';
21
+ const packRoot = path.join(home, 'assets', 'store', 'sample-kit');
22
+ const assetRoot = path.join(packRoot, 'assets', category, id);
23
+ const fragment = '<div>sample kit frame</div>\n';
24
+ mkdirSync(assetRoot, { recursive: true });
25
+ writeFileSync(path.join(assetRoot, 'fragment.html'), fragment);
26
+ mkdirSync(path.join(home, 'assets', category), { recursive: true });
27
+ symlinkSync(path.relative(path.join(home, 'assets', category), assetRoot), path.join(home, 'assets', category, id), 'dir');
28
+ writeFileSync(path.join(home, 'assets', 'installed.json'), `${JSON.stringify({
29
+ schema: 'akari-installed-assets/v0',
30
+ packs: {
31
+ 'sample-kit': {
32
+ version: 1,
33
+ installedAt: '2026-09-14T00:00:00.000Z',
34
+ root: packRoot,
35
+ items: [{
36
+ id,
37
+ title: 'Sample Kit Frame',
38
+ path: `assets/${category}/${id}`,
39
+ version: 1,
40
+ files: [{ path: 'fragment.html', bytes: Buffer.byteLength(fragment), sha256: sha256(fragment) }]
41
+ }]
42
+ }
43
+ }
44
+ }, null, 2)}\n`);
45
+
46
+ assert.equal(isAssetCached(home, category, id), true);
47
+ assert.equal(scanLocalLibrary(home).has(`${category}/${id}`), true);
48
+ const project = path.join(root, 'project');
49
+ const result = await resolveAsset(id, { env, project });
50
+ assert.equal(result.cached, true);
51
+ assert.equal(lstatSync(result.projectDir).isSymbolicLink(), false);
52
+ assert.equal(readFileSync(path.join(result.projectDir, 'fragment.html'), 'utf8'), fragment);
53
+ } finally {
54
+ rmSync(root, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ test('壊れた素材 symlink は cached 扱いにもローカル一覧にも入れない', () => {
59
+ const { home, root } = setupFixtureEnv();
60
+ try {
61
+ const categoryDir = path.join(home, 'assets', 'overlay');
62
+ mkdirSync(categoryDir, { recursive: true });
63
+ symlinkSync('../../store/missing/assets/overlay/broken-frame', path.join(categoryDir, 'broken-frame'), 'dir');
64
+ assert.equal(isAssetCached(home, 'overlay', 'broken-frame'), false);
65
+ assert.equal(scanLocalLibrary(home).has('overlay/broken-frame'), false);
66
+ } finally {
67
+ rmSync(root, { recursive: true, force: true });
68
+ }
69
+ });
@@ -0,0 +1,3 @@
1
+ # Fixture license
2
+
3
+ This fictional fixture represents `LicenseRef-AKARI-Assets-v0` metadata.
@@ -0,0 +1,3 @@
1
+ # Fictional sample kit with an asset
2
+
3
+ This directory is a validation fixture and is not a distributed product.
@@ -0,0 +1,10 @@
1
+ <div class="sample-kit-frame" aria-hidden="true"></div>
2
+
3
+ <style>
4
+ .sample-kit-frame {
5
+ position: absolute;
6
+ inset: 8%;
7
+ border: 12px solid #ffb000;
8
+ border-radius: 24px;
9
+ }
10
+ </style>