akari-video 0.1.67 → 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.
- package/README.md +11 -0
- package/bin/akari.mjs +5 -1
- package/package.json +1 -1
- package/src/kits.mjs +238 -0
- package/src/messages.mjs +1 -0
- package/src/repo-assets.mjs +7 -1
- package/src/store-command.mjs +138 -1
- package/src/storyboard-command.mjs +24 -0
- package/src/world-command.mjs +17 -0
- package/vendor/.akari-capability-sources.json +3 -0
- package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +75 -0
- package/vendor/docs/contract-2026-09-13-world-map-v0.md +50 -0
- package/vendor/packages/akari-launcher/README.md +11 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/package.json +3 -2
- package/vendor/packages/edit-lint/src/edit-lint.mjs +2 -0
- package/vendor/packages/edit-lint/src/world-scene-declaration.mjs +64 -0
- package/vendor/packages/edit-store/lib/generation-meta-node.js +2 -11
- package/vendor/packages/edit-store/lib/generation-meta.d.ts +4 -0
- package/vendor/packages/edit-store/lib/generation-meta.js +11 -0
- package/vendor/packages/generate/README.md +1 -1
- package/vendor/packages/overlay-runtime/README.md +18 -0
- package/vendor/packages/overlay-runtime/package.json +2 -0
- package/vendor/packages/project-scaffold/src/index.mjs +33 -3
- package/vendor/packages/project-scaffold/test/kit-skill-adapter.test.mjs +38 -0
- package/vendor/skills/overlay-authoring/SKILL.md +1 -0
- package/vendor/skills/overlay-authoring/world.md +42 -0
- 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.
|
|
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": {
|
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
|
'開発者向け:',
|
package/src/repo-assets.mjs
CHANGED
|
@@ -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 }
|
package/src/store-command.mjs
CHANGED
|
@@ -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,75 @@
|
|
|
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
|
+
## 6. 更新と版
|
|
74
|
+
|
|
75
|
+
キットの版は Store の整数版とする。`akari store status` が導入済みの id、version、スキル名、素材数を表示し、同じ `akari store install` で新版へ置換する。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ワールドマップ v0 契約
|
|
2
|
+
|
|
3
|
+
## 1. ファイルとスキーマ
|
|
4
|
+
|
|
5
|
+
ワールドマップはプロジェクトの `planning/world-map.json` に置く。このファイルの存在を地図 UI の表示条件とする。公開 v0 は `schemaVersion: 3` で、以後の変更は additive-only とする。旧 `schemaVersion: 2` は CLI の読み口で v3 に正規化できる。
|
|
6
|
+
|
|
7
|
+
共通のルートは `kind`、`worlds[]`、`zones[]`、`cameraStops[]`、`edges[]`、`retainedNodes[]` からなる。`kind` は `flat` または `spatial`。flat world は `flat.bounds` と `flat.pattern`、spatial world は `spatial.c` を持つ。zone は世界内の位置、cameraStop は滞在窓とカメラ位置、edge は連続する停留所間の移動または切替を宣言する。
|
|
8
|
+
|
|
9
|
+
機械検査する不変条件は次のとおり。
|
|
10
|
+
|
|
11
|
+
1. world は 1 件以上で、各 world は zone を 2 件以上持つ。
|
|
12
|
+
2. zones と cameraStops の id 集合は一致し、stop は `at` 昇順、`at < leave`、窓は非重複とする。
|
|
13
|
+
3. edge 数は stop 数より 1 少なく、順序と `from` / `to` / `t0` / `t1` が stop 列に一致する。
|
|
14
|
+
4. edge type は `move` / `portal` / `cut`。非 move は `via`、`transition`、区間内の `switchTime` が必須。
|
|
15
|
+
5. 世界をまたぐ edge は空でない `carry` を持ち、その値は `retainedNodes` の部分集合とする。
|
|
16
|
+
6. transition kind は `none` / `dive` / `mist` / `occluder` / `fade` / `push`。spatial では `push` を禁止する。
|
|
17
|
+
7. cut の実測 `cover` は 0.4 秒以下。未測定の `null` は通常検査では警告、strict 検査ではエラーとする。portal の cover には上限を設けない。
|
|
18
|
+
8. palette は 6 桁 hex、flat bounds と spatial floor size は有限かつ正とする。
|
|
19
|
+
9. 同一 world 内の連続 stop 間は move とする。
|
|
20
|
+
|
|
21
|
+
## 2. カメラ関数
|
|
22
|
+
|
|
23
|
+
`camera(t)` は world-map だけを入力にする純関数である。stop 窓では宣言値を返し、move では両 stop 間を補間する。portal / cut では `switchTime` より前を接近、後を脱出として `via` を経由し、切替時点で world を切り替える。同一入力時刻には常に同じ値を返す。
|
|
24
|
+
|
|
25
|
+
## 3. 描画
|
|
26
|
+
|
|
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
|
+
|
|
29
|
+
spatial world は three 断片で構成し、座標・床・背景・霧を宣言する。画面座標の 3D 小物は別 overlay item とする。
|
|
30
|
+
|
|
31
|
+
## 4. CLI
|
|
32
|
+
|
|
33
|
+
- `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` だけを書き戻す。
|
|
36
|
+
- `akari world overview`: 外部通信を行わず `file://` で開ける自己完結の俯瞰 HTML を生成する。
|
|
37
|
+
|
|
38
|
+
同じ入力から得る HTML と画像は決定論的でなければならない。素材 id は asset resolver で解決し、未解決時は失敗として扱う。
|
|
39
|
+
|
|
40
|
+
## 5. 地図 UI
|
|
41
|
+
|
|
42
|
+
地図 UI は world-map を読み取り専用で表示する。2D 俯瞰、ワールド帯、再生時刻に追従する撮影枠、選択中の stop / edge 詳細を提供し、データの編集機能は持たない。
|
|
43
|
+
|
|
44
|
+
## 6. 制作フロー
|
|
45
|
+
|
|
46
|
+
企画と絵コンテで章を world として宣言し、モーション区間は `world-map.json` → `akari world build` → overlay → 書き出しの順に処理する。実写区間との接点は portal とカットアウェイ章に限定する。
|
|
47
|
+
|
|
48
|
+
## 7. 将来拡張
|
|
49
|
+
|
|
50
|
+
生成動画を world の zone や edge へ配置する機能、world camera と別 overlay の 3D を世界座標で同期する機能、より大規模な world の間引きは v0 の外とし、後方互換な追加として導入する。
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
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 組み込みモジュールのみ)。 [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": [
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"@akari-video/render-cut": "0.0.0",
|
|
14
14
|
"puppeteer-core": "25.2.1"
|
|
15
15
|
},
|
|
16
|
-
"description": "@akari-video/akari-tools [akari-video npm vendor: bin/capture.mjs is reference-only; bin/media.mjs is reference-only; bin/word-book.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.]",
|
|
16
|
+
"description": "@akari-video/akari-tools [akari-video npm vendor: bin/capture.mjs is reference-only; bin/media.mjs is reference-only; bin/word-book.mjs is reference-only; bin/world.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.]",
|
|
17
17
|
"akariVideoVendor": {
|
|
18
18
|
"execution": "reference-only",
|
|
19
19
|
"omittedBin": {
|
|
20
20
|
"akari-capture": "bin/capture.mjs",
|
|
21
21
|
"akari-media": "bin/media.mjs",
|
|
22
|
-
"akari-word-book": "bin/word-book.mjs"
|
|
22
|
+
"akari-word-book": "bin/word-book.mjs",
|
|
23
|
+
"akari-world": "bin/world.mjs"
|
|
23
24
|
},
|
|
24
25
|
"guidance": "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."
|
|
25
26
|
}
|
|
@@ -22,6 +22,7 @@ import { musicGrid } from "../../audio-library-setup/shared/beat-grid.mjs";
|
|
|
22
22
|
import { resolveFfmpeg, resolveFfprobe } from "../../media-bin/src/index.mjs";
|
|
23
23
|
import { buildMatcher, protectedTermsFrom } from "../../word-book/src/index.mjs";
|
|
24
24
|
import { resolveWordBookSync, scanRecord } from "../../word-book/src/index.mjs";
|
|
25
|
+
import { validateWorldSceneDeclaration } from "./world-scene-declaration.mjs";
|
|
25
26
|
import {
|
|
26
27
|
readProjectReferences,
|
|
27
28
|
resolveAkariAssetsDir,
|
|
@@ -2542,6 +2543,7 @@ async function validateOverlays(overlays, timeline, findings, paths) {
|
|
|
2542
2543
|
isHtmlFile ? relativePath(paths.projectRoot, htmlPath) : `${itemPath}.html`,
|
|
2543
2544
|
findings,
|
|
2544
2545
|
);
|
|
2546
|
+
for (const finding of validateWorldSceneDeclaration(html, await readFile(join(paths.projectRoot, "planning/world-map.json"), "utf8").catch(error => error?.code === "ENOENT" ? null : Promise.reject(error)), isHtmlFile ? relativePath(paths.projectRoot, htmlPath) : `${itemPath}.html`)) addFinding(findings, finding);
|
|
2545
2547
|
if (!isHtmlFile) continue;
|
|
2546
2548
|
|
|
2547
2549
|
validateOverlayFragmentAssets(html, overlay, paths, findings);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const CHECK = "overlays.world-scene-declaration";
|
|
2
|
+
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const finite = value => typeof value === "number" && Number.isFinite(value);
|
|
4
|
+
const nonEmptyString = value => typeof value === "string" && value.length > 0;
|
|
5
|
+
|
|
6
|
+
function finding(path, message) {
|
|
7
|
+
return { severity: "error", check: CHECK, message, path };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sameIds(left, right) {
|
|
11
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
12
|
+
const ids = values => values.map(value => value?.id).sort();
|
|
13
|
+
return JSON.stringify(ids(left)) === JSON.stringify(ids(right));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sameTimes(left, right, fields) {
|
|
17
|
+
if (!sameIds(left, right)) return false;
|
|
18
|
+
const byId = new Map(right.map(value => [value.id, value]));
|
|
19
|
+
return left.every(value => fields.every(field => {
|
|
20
|
+
const expected = byId.get(value.id)?.[field];
|
|
21
|
+
const actual = value[field];
|
|
22
|
+
return actual === expected || (actual === undefined && expected === undefined);
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateShape(value) {
|
|
27
|
+
if (!isRecord(value)) return "宣言は JSON object である必要があります";
|
|
28
|
+
const allowed = new Set(["schemaVersion", "kind", "frame", "worlds", "zones", "cameraStops", "edges", "retainedNodes", "render"]);
|
|
29
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key));
|
|
30
|
+
if (unknown) return `未知のキーです: ${unknown}`;
|
|
31
|
+
if (value.schemaVersion !== 1) return "schemaVersion は 1 である必要があります";
|
|
32
|
+
if (value.kind !== "flat") return "kind は flat である必要があります";
|
|
33
|
+
if (!isRecord(value.frame) || !finite(value.frame.width) || value.frame.width <= 0 || !finite(value.frame.height) || value.frame.height <= 0) return "frame は正の width / height を持つ必要があります";
|
|
34
|
+
for (const name of ["worlds", "zones", "cameraStops", "edges", "retainedNodes"]) if (!Array.isArray(value[name])) return `${name} は配列である必要があります`;
|
|
35
|
+
if (!value.worlds.every(world => isRecord(world) && nonEmptyString(world.id) && isRecord(world.palette) && isRecord(world.flat) && Array.isArray(world.flat.bounds) && world.flat.bounds.length === 4 && world.flat.bounds.every(finite) && ["dots", "grid", "none"].includes(world.flat.pattern))) return "worlds の形が不正です";
|
|
36
|
+
if (!value.zones.every(zone => isRecord(zone) && nonEmptyString(zone.id) && nonEmptyString(zone.world) && Array.isArray(zone.c) && zone.c.length === 2 && zone.c.every(finite))) return "zones の形が不正です";
|
|
37
|
+
if (!value.cameraStops.every(stop => isRecord(stop) && nonEmptyString(stop.id) && nonEmptyString(stop.world) && finite(stop.at) && finite(stop.leave) && Array.isArray(stop.c) && stop.c.length === 3 && stop.c.every(finite))) return "cameraStops の形が不正です";
|
|
38
|
+
if (!value.edges.every(edge => isRecord(edge) && nonEmptyString(edge.id) && nonEmptyString(edge.from) && nonEmptyString(edge.to) && nonEmptyString(edge.type) && finite(edge.t0) && finite(edge.t1) && isRecord(edge.transition) && nonEmptyString(edge.transition.kind) && finite(edge.transition.cover) && (edge.switchTime === undefined || finite(edge.switchTime)))) return "edges の形が不正です";
|
|
39
|
+
if (value.render !== undefined && (!isRecord(value.render) || ["dotStep", "margin", "hazeAlpha"].some(name => value.render[name] !== undefined && !finite(value.render[name])))) return "render の形が不正です";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateWorldSceneDeclaration(html, worldMapText, path) {
|
|
44
|
+
const pattern = /<script\b(?=[^>]*\btype\s*=\s*(?:"application\/json"|'application\/json'))(?=[^>]*\sdata-akari-world-scene(?=\s|=|\/?>))[^>]*>([\s\S]*?)<\/script\s*>/giu;
|
|
45
|
+
const declarations = [...html.matchAll(pattern)];
|
|
46
|
+
if (!declarations.length) return [];
|
|
47
|
+
if (declarations.length !== 1) return [finding(path, "data-akari-world-scene 宣言は 1 個である必要があります")];
|
|
48
|
+
let descriptor;
|
|
49
|
+
try { descriptor = JSON.parse(declarations[0][1]); }
|
|
50
|
+
catch (error) { return [finding(path, `宣言 JSON を読めません: ${error.message}`)]; }
|
|
51
|
+
const shapeError = validateShape(descriptor);
|
|
52
|
+
if (shapeError) return [finding(path, shapeError)];
|
|
53
|
+
if (worldMapText === null) return [];
|
|
54
|
+
let worldMap;
|
|
55
|
+
try { worldMap = JSON.parse(worldMapText); }
|
|
56
|
+
catch (error) { return [finding(path, `planning/world-map.json を読めません: ${error.message}`)]; }
|
|
57
|
+
for (const name of ["worlds", "zones", "cameraStops", "edges"]) {
|
|
58
|
+
if (!sameIds(descriptor[name], worldMap?.[name])) return [finding(path, `planning/world-map.json と ${name} の id 集合が一致しません`)];
|
|
59
|
+
}
|
|
60
|
+
if (!sameTimes(descriptor.cameraStops, worldMap.cameraStops, ["at", "leave"])) return [finding(path, "planning/world-map.json と cameraStops の時刻が一致しません")];
|
|
61
|
+
if (!sameTimes(descriptor.edges, worldMap.edges, ["t0", "t1", "switchTime"])) return [finding(path, "planning/world-map.json と edges の時刻が一致しません")];
|
|
62
|
+
const transitionTimesMatch = descriptor.edges.every(edge => worldMap.edges.find(item => item.id === edge.id)?.transition?.cover === edge.transition.cover);
|
|
63
|
+
return transitionTimesMatch ? [] : [finding(path, "planning/world-map.json と edges.transition.cover が一致しません")];
|
|
64
|
+
}
|
|
@@ -13,7 +13,7 @@ function readGenerationMeta(options) {
|
|
|
13
13
|
return { state: 'none', meta: null, sidecarPath, binding: null };
|
|
14
14
|
}
|
|
15
15
|
const meta = parseMeta(sidecarPath);
|
|
16
|
-
const expected =
|
|
16
|
+
const expected = (0, generation_meta_1.bindingShaFor)(meta);
|
|
17
17
|
let binding = null;
|
|
18
18
|
let state = (0, generation_meta_1.resolveGenerationState)(meta, options.now);
|
|
19
19
|
if (expected) {
|
|
@@ -37,21 +37,12 @@ function findGenerationMetaBySha(options) {
|
|
|
37
37
|
return null;
|
|
38
38
|
for (const sidecarPath of generationSidecars(generatedRoot)) {
|
|
39
39
|
const meta = parseMeta(sidecarPath);
|
|
40
|
-
const expected =
|
|
40
|
+
const expected = (0, generation_meta_1.bindingShaFor)(meta);
|
|
41
41
|
if (expected?.sha256 === options.sha256)
|
|
42
42
|
return meta;
|
|
43
43
|
}
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
|
-
function bindingSha(meta) {
|
|
47
|
-
if (meta.status === 'done' && typeof meta.result?.sha256 === 'string') {
|
|
48
|
-
return { sha256: meta.result.sha256, source: 'result' };
|
|
49
|
-
}
|
|
50
|
-
if ((meta.kind === 'still' || meta.status === 'planned') && typeof meta.inputs?.first_frame?.sha256 === 'string') {
|
|
51
|
-
return { sha256: meta.inputs.first_frame.sha256, source: 'first_frame' };
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
46
|
function generationSidecars(directory) {
|
|
56
47
|
const found = [];
|
|
57
48
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -38,6 +38,10 @@ export interface ReadGenerationMetaResult {
|
|
|
38
38
|
binding: GenerationBinding | null;
|
|
39
39
|
}
|
|
40
40
|
export declare function sidecarPathFor(sourcePath: string): string;
|
|
41
|
+
export declare function bindingShaFor(meta: GenerationMetaV1 | null | undefined): {
|
|
42
|
+
sha256: string;
|
|
43
|
+
source: 'result' | 'first_frame';
|
|
44
|
+
} | null;
|
|
41
45
|
/**
|
|
42
46
|
* fs に触れず、サイドカー自身が表す状態だけを解決する。
|
|
43
47
|
* `job.stale_after_s` が未指定・不正な場合は既定 900 秒を使う。
|
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.sidecarPathFor = sidecarPathFor;
|
|
9
|
+
exports.bindingShaFor = bindingShaFor;
|
|
9
10
|
exports.resolveGenerationState = resolveGenerationState;
|
|
10
11
|
function sidecarPathFor(sourcePath) {
|
|
11
12
|
return `${sourcePath}.meta.json`;
|
|
12
13
|
}
|
|
14
|
+
function bindingShaFor(meta) {
|
|
15
|
+
if (meta?.status === 'done' && typeof meta.result?.sha256 === 'string') {
|
|
16
|
+
return { sha256: meta.result.sha256, source: 'result' };
|
|
17
|
+
}
|
|
18
|
+
if ((meta?.kind === 'still' || meta?.status === 'planned')
|
|
19
|
+
&& typeof meta.inputs?.first_frame?.sha256 === 'string') {
|
|
20
|
+
return { sha256: meta.inputs.first_frame.sha256, source: 'first_frame' };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
13
24
|
/**
|
|
14
25
|
* fs に触れず、サイドカー自身が表す状態だけを解決する。
|
|
15
26
|
* `job.stale_after_s` が未指定・不正な場合は既定 900 秒を使う。
|
|
@@ -13,6 +13,6 @@ A model without recorded pricing remains valid and returns `cost.needs_explicit_
|
|
|
13
13
|
|
|
14
14
|
`akari generate still <projectDir> --spec beats.json` はビートごとの静止画を Codex で生成します。
|
|
15
15
|
`--parallel N` で並列数を指定でき、既定は 4 です。
|
|
16
|
-
`--placeholder` は Codex
|
|
16
|
+
`--placeholder` は Codex を呼ばず、Chrome → ffmpeg drawtext → 単色の順で無料の文字カード PNG を置きます。
|
|
17
17
|
`--dry-run` は edit.json や素材を書かず、配置予定だけを表示します。
|
|
18
18
|
生成物と meta は `assets/generated/`、クリップは edit.json v2 の visual トラック末尾に入ります。
|
|
@@ -623,6 +623,24 @@ npm グローバルインストール禁止の制約内で完結するよう、
|
|
|
623
623
|
`prepare` を指定したランタイムは seek ごとにそのメソッドを await し、render 直後に ready を確認する。
|
|
624
624
|
この場合、t=0 の事前描画と ready ポーリングは生成しない。
|
|
625
625
|
|
|
626
|
+
## Canvas 2D ワールドランタイム
|
|
627
|
+
|
|
628
|
+
`src/world-runtime.js` は `<script type="application/json" data-akari-world-scene>` を持つ
|
|
629
|
+
`kind: "flat"` 断片を描く。宣言は `schemaVersion: 1`、出力枠 `frame`、および
|
|
630
|
+
`worlds` / `zones` / `cameraStops` / `edges` / `retainedNodes` を持つ。任意の `render` は
|
|
631
|
+
`dotStep: 90`、`margin: 0.25`、`hazeAlpha: 0.92` を既定値とする。
|
|
632
|
+
|
|
633
|
+
registry は `world-camera.js`、`world-runtime.js` の順で読み込む。前者は
|
|
634
|
+
`packages/akari-tools/src/world/camera.mjs` から `npm run gen:world-camera` で生成した classic script
|
|
635
|
+
で、`npm run check:world-camera` が正本とのドリフトを検査する。
|
|
636
|
+
|
|
637
|
+
ランタイムは Canvas に背景・遠景・格子・portal 枠・遷移 cover を描き、同じ `camera(seconds)`
|
|
638
|
+
から断片直下の `.akari-world-sheet[data-world]` の transform と
|
|
639
|
+
`.akari-world-zone[data-zone]` の画面外カリングを同期する。描画は外部時刻だけに依存する。
|
|
640
|
+
俯瞰は公開 API `worldRuntime.drawOverview(ctx, descriptor, { scale, ox, oy }, seconds, options)` を使い、
|
|
641
|
+
`view` はカメラ位置に依存しない world → screen の affine(`screen = ox + p * scale`)として扱う。
|
|
642
|
+
`options.frame: true` で現在の撮影枠を重ねられる。
|
|
643
|
+
|
|
626
644
|
## vgpu vendor の固定と再生成
|
|
627
645
|
|
|
628
646
|
`src/vendor/vgpu-bundle.js` は `vgpu@0.4.0` の browser entry を `esbuild@0.24.2` で
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
+
"gen:world-camera": "node scripts/gen-world-camera.mjs",
|
|
15
|
+
"check:world-camera": "node scripts/gen-world-camera.mjs --check",
|
|
14
16
|
"check": "node --check src/parts.mjs && node --check src/vendor/three-bundle.js && node --check src/vendor/vendor-3d-text-bundle.js && node --check src/three-runtime.js && node --check src/slot-params.js && node --check src/video-fx.js && node --check src/viewport-units.js && node --check src/vendor/budoux-ja-bundle.js && node --check src/text-split.js && node --check src/overlay-runtime.js && node --check src/interaction.js && node --check src/minimap.js",
|
|
15
17
|
"test": "node --test test-harness/*.test.mjs"
|
|
16
18
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
|
|
@@ -341,15 +342,25 @@ export async function installProjectSkills(destinationDir, skillsSourceDir, sche
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
export async function installSkillAdapters(destinationDir, options = {}) {
|
|
344
|
-
const { fsImpl = fs, platform = process.platform } = options;
|
|
345
|
+
const { fsImpl = fs, platform = process.platform, env = process.env } = options;
|
|
345
346
|
const skillsDir = path.join(destinationDir, '.claude', 'skills');
|
|
346
347
|
const skillNames = (await fsImpl.readdir(skillsDir, { withFileTypes: true }))
|
|
347
348
|
.filter(entry => entry.isDirectory())
|
|
348
349
|
.map(entry => entry.name);
|
|
350
|
+
const kitSkillsDir = path.join(env.AKARI_HOME || path.join(homedir(), '.akari'), 'kits', 'plugin', 'skills');
|
|
351
|
+
let kitSkillNames = [];
|
|
352
|
+
try {
|
|
353
|
+
kitSkillNames = (await fsImpl.readdir(kitSkillsDir, { withFileTypes: true }))
|
|
354
|
+
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
|
355
|
+
.map(entry => entry.name);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (!error || typeof error !== 'object' || error.code !== 'ENOENT') throw error;
|
|
358
|
+
}
|
|
349
359
|
|
|
350
360
|
const created = [];
|
|
351
361
|
const skippedExisting = [];
|
|
352
362
|
const degraded = [];
|
|
363
|
+
const warnings = [];
|
|
353
364
|
for (const adapter of SKILL_ADAPTER_DIRECTORIES) {
|
|
354
365
|
const adapterDir = path.join(destinationDir, adapter, 'skills');
|
|
355
366
|
await fsImpl.mkdir(adapterDir, { recursive: true });
|
|
@@ -372,8 +383,27 @@ export async function installSkillAdapters(destinationDir, options = {}) {
|
|
|
372
383
|
skippedExisting.push(relativeName);
|
|
373
384
|
}
|
|
374
385
|
}
|
|
386
|
+
for (const name of kitSkillNames) {
|
|
387
|
+
const relativeName = `${adapter}/skills/${name}`;
|
|
388
|
+
if (skillNames.includes(name)) {
|
|
389
|
+
warnings.push(`${relativeName}: 純正スキルを優先し、同名の拡張キットスキルをスキップしました`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const { method } = await createSkillAdapterLink(
|
|
394
|
+
path.relative(adapterDir, path.join(kitSkillsDir, name)),
|
|
395
|
+
path.join(adapterDir, name),
|
|
396
|
+
{ fsImpl, platform }
|
|
397
|
+
);
|
|
398
|
+
created.push(relativeName);
|
|
399
|
+
if (method !== 'symlink') degraded.push({ name: relativeName, method });
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (!isAlreadyExists(error)) throw error;
|
|
402
|
+
skippedExisting.push(relativeName);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
375
405
|
}
|
|
376
|
-
return { created, skippedExisting, degraded };
|
|
406
|
+
return { created, skippedExisting, degraded, warnings };
|
|
377
407
|
}
|
|
378
408
|
|
|
379
409
|
export async function readSkillsVersion(destinationDir) {
|
|
@@ -517,7 +547,7 @@ export async function createProject(destinationDir, templateDir, options = {}) {
|
|
|
517
547
|
const fallback = await writeFallbackTemplate(destination);
|
|
518
548
|
if (options.skillsSourceDir) {
|
|
519
549
|
await installProjectSkills(destination, options.skillsSourceDir, options.schemasSourceDir);
|
|
520
|
-
await installSkillAdapters(destination);
|
|
550
|
+
await installSkillAdapters(destination, { env: options.env });
|
|
521
551
|
}
|
|
522
552
|
const skillsVersion = await readSkillsVersion(destination);
|
|
523
553
|
const boundary = await checkGitBoundary(destination);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readlink, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { installSkillAdapters } from '../src/index.mjs';
|
|
8
|
+
|
|
9
|
+
test('installSkillAdapters: kits の skill を全アダプタへ合成し、重複は純正を優先する', async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(tmpdir(), 'akari-project-kit-adapter-test-'));
|
|
11
|
+
const home = await mkdtemp(path.join(tmpdir(), 'akari-home-kit-adapter-test-'));
|
|
12
|
+
try {
|
|
13
|
+
await mkdir(path.join(root, '.claude', 'skills', 'official'), { recursive: true });
|
|
14
|
+
await writeFile(path.join(root, '.claude', 'skills', 'official', 'SKILL.md'), '# official\n');
|
|
15
|
+
const kitSkills = path.join(home, 'kits', 'plugin', 'skills');
|
|
16
|
+
await mkdir(path.join(kitSkills, 'kit-only'), { recursive: true });
|
|
17
|
+
await mkdir(path.join(kitSkills, 'official'), { recursive: true });
|
|
18
|
+
|
|
19
|
+
const report = await installSkillAdapters(root, { env: { AKARI_HOME: home } });
|
|
20
|
+
for (const adapter of ['.agents', '.codex', '.cursor', '.opencode']) {
|
|
21
|
+
const kitLink = path.join(root, adapter, 'skills', 'kit-only');
|
|
22
|
+
assert.equal(
|
|
23
|
+
path.resolve(path.dirname(kitLink), await readlink(kitLink)),
|
|
24
|
+
path.join(kitSkills, 'kit-only')
|
|
25
|
+
);
|
|
26
|
+
const officialLink = path.join(root, adapter, 'skills', 'official');
|
|
27
|
+
assert.equal(
|
|
28
|
+
path.resolve(path.dirname(officialLink), await readlink(officialLink)),
|
|
29
|
+
path.join(root, '.claude', 'skills', 'official')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
assert.equal(report.warnings.length, 4);
|
|
33
|
+
assert.ok(report.warnings.every((line) => line.includes('純正スキルを優先')));
|
|
34
|
+
} finally {
|
|
35
|
+
await rm(root, { recursive: true, force: true });
|
|
36
|
+
await rm(home, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -39,6 +39,7 @@ description: AKARI Video のオーバーレイ HTML、字幕、表・グラフ
|
|
|
39
39
|
- 表・グラフの HTML/CSS 構成とアニメーション: [table.md](table.md)
|
|
40
40
|
- Three.js + glTF、動画テクスチャ、3D 性能: [3d.md](3d.md)。端末の画面・キーの生成や反射調整は [device-materials.md](device-materials.md) も読む。
|
|
41
41
|
- ガラス屈折の宣言、入れ子、ツマミ、静止背景: [glass.md](glass.md)
|
|
42
|
+
- Canvas 2D 世界、DOM シート同期、俯瞰: [world.md](world.md)
|
|
42
43
|
- 新しい描画の種類は `packages/overlay-runtime/runtimes.mjs` のマニフェストへ登録する(追加手順: `packages/overlay-runtime/README.md`)。
|
|
43
44
|
- 決定的モーション、イージング、compositor 制約: [motion.md](motion.md)
|
|
44
45
|
- サムネイルの型、デザイン語彙、生成経路、HTML スクショ: [thumbnail.md](thumbnail.md)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Canvas 2D ワールド
|
|
2
|
+
|
|
3
|
+
> **Language**: Respond in the user's language — 対話・質問・承認確認・レポートはユーザーの使用言語に合わせる。
|
|
4
|
+
|
|
5
|
+
`kind: "flat"` の世界は、断片内の Canvas 背景と DOM 素材を同じ純粋な `camera(t)` で動かす。
|
|
6
|
+
断片は単一ルートを守り、その中に宣言を 1 個置く。
|
|
7
|
+
|
|
8
|
+
```html
|
|
9
|
+
<div class="world-fragment">
|
|
10
|
+
<div class="akari-world-sheet" data-world="paper">
|
|
11
|
+
<div class="akari-world-zone" data-zone="desk" style="left:10px;top:12px">
|
|
12
|
+
<!-- 素材・テロップ -->
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<script type="application/json" data-akari-world-scene>{
|
|
16
|
+
"schemaVersion": 1,
|
|
17
|
+
"kind": "flat",
|
|
18
|
+
"frame": { "width": 1920, "height": 1080 },
|
|
19
|
+
"worlds": [], "zones": [], "cameraStops": [], "edges": [], "retainedNodes": [],
|
|
20
|
+
"render": { "dotStep": 90, "margin": 0.25, "hazeAlpha": 0.92 }
|
|
21
|
+
}</script>
|
|
22
|
+
</div>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`worlds` / `zones` / `cameraStops` / `edges` / `retainedNodes` は `planning/world-map.json`
|
|
26
|
+
から `inventory` を除いて写す。`worlds[].flat` は `bounds`、`pattern`(`dots` / `grid` /
|
|
27
|
+
`none`)、任意の遠景 `far: [{ z, color }]` を持ち、`palette` は `background` / `dots` /
|
|
28
|
+
`accent` と任意の `haze` を持つ。
|
|
29
|
+
|
|
30
|
+
各 world に対応する `.akari-world-sheet[data-world]` を断片直下へ置く。zone は world の
|
|
31
|
+
`bounds` 原点基準の px で配置し、`.akari-world-zone[data-zone]` を付ける。ランタイムが毎 tick
|
|
32
|
+
書くのは sheet の `transform` / `transform-origin` と zone の画面外カリング用 `display` だけで、
|
|
33
|
+
素材やテロップの中身には触れない。
|
|
34
|
+
|
|
35
|
+
時刻は `render(container, seconds)` からだけ受け取り、乱数・壁時計・delta 積算を使わない。
|
|
36
|
+
同じ宣言と seconds は Canvas、DOM transform、カリングの同じ結果を返す。cut / portal の
|
|
37
|
+
`switchTime ± transition.cover / 2` は一様な haze で覆う。
|
|
38
|
+
|
|
39
|
+
俯瞰は `window.akari.worldRuntime.drawOverview(ctx, descriptor, view, seconds, options)` を使う。
|
|
40
|
+
`view` はカメラ位置に依存しない world → screen の affine `{ scale, ox, oy }`
|
|
41
|
+
(`screen = ox + p * scale`)。`options.frame === true` なら現在の撮影枠を `#EE82DF` で重ねる。
|
|
42
|
+
独自の地図描画を複製しない。
|
|
@@ -87,7 +87,7 @@ node の解決順は `AKARI_NODE_BIN` → PATH の node(20 以上)→ 同梱
|
|
|
87
87
|
タイムラインの初回確認にだけ、次のコマンドで L1 通しマップを作る。
|
|
88
88
|
|
|
89
89
|
```sh
|
|
90
|
-
|
|
90
|
+
akari storyboard <project-root>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
置き場は `<project>/.akari/reports/storyboard/`。絵コンテは初回だけ作り、タイムライン変更に合わせて
|