akari-video 0.1.68 → 0.1.70
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/package.json +1 -1
- package/src/internal-command.mjs +4 -6
- package/src/repo-assets.mjs +44 -13
- package/src/vendor-sources.mjs +47 -0
- package/src/world-command.mjs +2 -2
- package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +4 -0
- package/vendor/docs/contract-2026-09-13-world-map-v0.md +4 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/asset-resolver/src/state.mjs +32 -1
- package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +36 -0
- package/vendor/packages/edit-store/lib/generation-meta.d.ts +13 -0
- package/vendor/packages/edit-store/lib/generation-meta.js +35 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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/internal-command.mjs
CHANGED
|
@@ -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
|
|
35
|
-
// repo-assets.mjs
|
|
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,
|
|
37
|
+
? path.join(assets.repoRoot, FINGER_FRAME_SCRIPT_RELATIVE)
|
|
40
38
|
: null;
|
|
41
39
|
|
|
42
40
|
const definitions = {
|
package/src/repo-assets.mjs
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
|
@@ -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
|
+
];
|
package/src/world-command.mjs
CHANGED
|
@@ -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,
|
|
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 };
|
|
@@ -70,6 +70,10 @@ claude plugin install akari-kits@akari-kits
|
|
|
70
70
|
|
|
71
71
|
Codex、Cursor、opencode では、プロジェクトの `.agents/.codex/.cursor/.opencode/skills` へキットスキルも合成する。同名があれば純正スキルを優先する。plugin が利用できない環境でも `~/.akari/kits/plugin/skills/<name>/SKILL.md` を直接読める。
|
|
72
72
|
|
|
73
|
+
## 5. アプリ(ホームの拡張キットカード)
|
|
74
|
+
|
|
75
|
+
ホームの AKARI Store カードの隣に拡張キットカードを 1 枚出し、未接続では出さず、導入済みは id・version・スキル名・素材数の一覧と未有効化時の有効化案内、購入済み・未導入は `akari store install <id>` の案内、未購入は教材の引換ページの案内、という 3 状態とする(アプリはコマンドを実行せず、コピーと外部ブラウザ起動だけを行う)。
|
|
76
|
+
|
|
73
77
|
## 6. 更新と版
|
|
74
78
|
|
|
75
79
|
キットの版は Store の整数版とする。`akari store status` が導入済みの id、version、スキル名、素材数を表示し、同じ `akari store install` で新版へ置換する。
|
|
@@ -39,6 +39,10 @@ spatial world は three 断片で構成し、座標・床・背景・霧を宣
|
|
|
39
39
|
|
|
40
40
|
## 5. 地図 UI
|
|
41
41
|
|
|
42
|
+
- 実装のマーカー判定は `akari-shell-strip` の ContextKey `akari.worldMap` に一元化する。
|
|
43
|
+
- main の「地図」タブは `akari-world-view` が担う。
|
|
44
|
+
- タイムラインのワールド帯と地図インスペクターは `akari-annotations` が担う。
|
|
45
|
+
|
|
42
46
|
地図 UI は world-map を読み取り専用で表示する。2D 俯瞰、ワールド帯、再生時刻に追従する撮影枠、選択中の stop / edge 詳細を提供し、データの編集機能は持たない。
|
|
43
47
|
|
|
44
48
|
## 6. 制作フロー
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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": [
|
|
@@ -2,10 +2,37 @@
|
|
|
2
2
|
// 「このアカウントで使える素材 = 無料全部 + 購入済み」の 1 ビュー(設計契約 §8)の核。
|
|
3
3
|
|
|
4
4
|
import { loadCatalog } from './catalog.mjs';
|
|
5
|
-
import { resolveAkariHome, resolveEffectiveBase } from './env.mjs';
|
|
5
|
+
import { resolveAkariHome, resolveEffectiveBase, resolveEntitlementsUrl } from './env.mjs';
|
|
6
6
|
import { fetchEntitlements, readStoreCredentials } from './entitlements.mjs';
|
|
7
7
|
import { scanLocalLibrary } from './library.mjs';
|
|
8
8
|
|
|
9
|
+
async function fetchEntitledProducts({ env, fetchImpl }) {
|
|
10
|
+
const credentials = await readStoreCredentials(env);
|
|
11
|
+
if (!credentials) return [];
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetchImpl(resolveEntitlementsUrl(env, credentials), {
|
|
14
|
+
headers: { authorization: `Bearer ${credentials.token}` },
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) return [];
|
|
17
|
+
const data = await response.json();
|
|
18
|
+
if (!Array.isArray(data?.entitlements)) return [];
|
|
19
|
+
return data.entitlements.flatMap((entry) => {
|
|
20
|
+
if (typeof entry === 'string') {
|
|
21
|
+
return entry ? [{ id: entry, kind: null, currentVersion: null }] : [];
|
|
22
|
+
}
|
|
23
|
+
const id = entry?.product_id ?? entry?.id;
|
|
24
|
+
if (typeof id !== 'string' || !id) return [];
|
|
25
|
+
return [{
|
|
26
|
+
id,
|
|
27
|
+
kind: typeof entry.kind === 'string' ? entry.kind : null,
|
|
28
|
+
currentVersion: Number.isFinite(entry.current_version) ? entry.current_version : null,
|
|
29
|
+
}];
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
/**
|
|
10
37
|
* @returns {Promise<{ home: string, base: string, catalogVersion: string|null, entitlementsStatus: 'ok'|'no_credentials'|'unauthorized'|'error', items: Array }>}
|
|
11
38
|
* items の各要素はカタログ項目に `state`('cached' | 'available' | 'locked')を足したもの。
|
|
@@ -22,6 +49,9 @@ export async function composeState({ env = process.env, fetchImpl = fetch } = {}
|
|
|
22
49
|
const entitlementsResult = hasPaidItems
|
|
23
50
|
? await fetchEntitlements({ env, fetchImpl })
|
|
24
51
|
: { ids: new Set(), status: await readStoreCredentials(env) ? 'ok' : 'no_credentials' };
|
|
52
|
+
// entitlements.mjs は id/status だけを返し編集できないため、商品 kind/version は
|
|
53
|
+
// 同じ API への 2 回目の fail-soft 取得で補う。
|
|
54
|
+
const entitledProducts = await fetchEntitledProducts({ env, fetchImpl });
|
|
25
55
|
|
|
26
56
|
const items = catalog.items.map((item) => {
|
|
27
57
|
const key = `${item.category}/${item.id}`;
|
|
@@ -38,6 +68,7 @@ export async function composeState({ env = process.env, fetchImpl = fetch } = {}
|
|
|
38
68
|
base,
|
|
39
69
|
catalogVersion: catalog.version ?? null,
|
|
40
70
|
entitlementsStatus: entitlementsResult.status,
|
|
71
|
+
entitledProducts,
|
|
41
72
|
items,
|
|
42
73
|
};
|
|
43
74
|
}
|
|
@@ -50,3 +50,39 @@ test('composeState: ローカルに実体があるものは cached になる', a
|
|
|
50
50
|
const free = items.find((i) => i.id === 'mini-still');
|
|
51
51
|
assert.equal(free.state, 'cached');
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
test('composeState: entitledProducts に kit 商品を載せ、資格情報なしでは空配列にする', async () => {
|
|
55
|
+
const connected = setupFixtureEnv();
|
|
56
|
+
writeFileSync(
|
|
57
|
+
path.join(connected.home, 'store-credentials.json'),
|
|
58
|
+
`${JSON.stringify({ url: 'https://example.invalid/api/store', token: 'akst_test' }, null, 2)}\n`,
|
|
59
|
+
);
|
|
60
|
+
let fetchCount = 0;
|
|
61
|
+
const state = await composeState({
|
|
62
|
+
env: connected.env,
|
|
63
|
+
fetchImpl: async () => {
|
|
64
|
+
fetchCount += 1;
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
status: 200,
|
|
68
|
+
json: async () => ({
|
|
69
|
+
entitlements: [{ product_id: 'world-kit', kind: 'kit', current_version: 3 }],
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(state.entitledProducts, [{ id: 'world-kit', kind: 'kit', currentVersion: 3 }]);
|
|
75
|
+
assert.equal(fetchCount, 2);
|
|
76
|
+
|
|
77
|
+
const disconnected = setupFixtureEnv();
|
|
78
|
+
let disconnectedFetchCount = 0;
|
|
79
|
+
const disconnectedState = await composeState({
|
|
80
|
+
env: disconnected.env,
|
|
81
|
+
fetchImpl: async () => {
|
|
82
|
+
disconnectedFetchCount += 1;
|
|
83
|
+
throw new Error('資格情報なしでは呼ばれない');
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
assert.deepEqual(disconnectedState.entitledProducts, []);
|
|
87
|
+
assert.equal(disconnectedFetchCount, 0);
|
|
88
|
+
});
|
|
@@ -11,6 +11,8 @@ export interface GenerationMetaV1 {
|
|
|
11
11
|
inputs?: {
|
|
12
12
|
first_frame?: {
|
|
13
13
|
sha256?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
source_id?: string | null;
|
|
14
16
|
} | null;
|
|
15
17
|
[key: string]: unknown;
|
|
16
18
|
};
|
|
@@ -47,3 +49,14 @@ export declare function bindingShaFor(meta: GenerationMetaV1 | null | undefined)
|
|
|
47
49
|
* `job.stale_after_s` が未指定・不正な場合は既定 900 秒を使う。
|
|
48
50
|
*/
|
|
49
51
|
export declare function resolveGenerationState(meta: GenerationMetaV1 | null | undefined, now: Date | string | number): GenerationState;
|
|
52
|
+
/**
|
|
53
|
+
* item が現在指している素材へ、生成物側の video サイドカーを逆引きする。
|
|
54
|
+
* 選択だけを行い、各 surface 固有の orphan / 契約外 status の解決は呼び出し側へ残す。
|
|
55
|
+
*/
|
|
56
|
+
export declare function selectGenerationSidecarForSource<T extends {
|
|
57
|
+
sourcePath: string;
|
|
58
|
+
meta?: GenerationMetaV1 | null;
|
|
59
|
+
binding?: {
|
|
60
|
+
matches?: boolean;
|
|
61
|
+
} | null;
|
|
62
|
+
}>(sourcePath: string | undefined, entries: readonly T[], now: Date | string | number): T | undefined;
|
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.sidecarPathFor = sidecarPathFor;
|
|
9
9
|
exports.bindingShaFor = bindingShaFor;
|
|
10
10
|
exports.resolveGenerationState = resolveGenerationState;
|
|
11
|
+
exports.selectGenerationSidecarForSource = selectGenerationSidecarForSource;
|
|
11
12
|
function sidecarPathFor(sourcePath) {
|
|
12
13
|
return `${sourcePath}.meta.json`;
|
|
13
14
|
}
|
|
@@ -15,8 +16,7 @@ function bindingShaFor(meta) {
|
|
|
15
16
|
if (meta?.status === 'done' && typeof meta.result?.sha256 === 'string') {
|
|
16
17
|
return { sha256: meta.result.sha256, source: 'result' };
|
|
17
18
|
}
|
|
18
|
-
if (
|
|
19
|
-
&& typeof meta.inputs?.first_frame?.sha256 === 'string') {
|
|
19
|
+
if (typeof meta?.inputs?.first_frame?.sha256 === 'string') {
|
|
20
20
|
return { sha256: meta.inputs.first_frame.sha256, source: 'first_frame' };
|
|
21
21
|
}
|
|
22
22
|
return null;
|
|
@@ -45,3 +45,36 @@ function resolveGenerationState(meta, now) {
|
|
|
45
45
|
}
|
|
46
46
|
return meta.status;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* item が現在指している素材へ、生成物側の video サイドカーを逆引きする。
|
|
50
|
+
* 選択だけを行い、各 surface 固有の orphan / 契約外 status の解決は呼び出し側へ残す。
|
|
51
|
+
*/
|
|
52
|
+
function selectGenerationSidecarForSource(sourcePath, entries, now) {
|
|
53
|
+
if (typeof sourcePath !== 'string')
|
|
54
|
+
return undefined;
|
|
55
|
+
const normalizePath = (value) => value.trim().replace(/\\/gu, '/').replace(/^(?:\.\/)+/u, '');
|
|
56
|
+
const normalizedSourcePath = normalizePath(sourcePath);
|
|
57
|
+
const direct = entries.find(entry => normalizePath(entry.sourcePath) === normalizedSourcePath);
|
|
58
|
+
if (direct?.meta?.kind === 'video')
|
|
59
|
+
return direct;
|
|
60
|
+
let selected;
|
|
61
|
+
let selectedStartedAt = Number.NEGATIVE_INFINITY;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const meta = entry.meta;
|
|
64
|
+
if (meta?.kind !== 'video' || entry.binding?.matches === false)
|
|
65
|
+
continue;
|
|
66
|
+
const firstFramePath = meta.inputs?.first_frame?.path;
|
|
67
|
+
if (typeof firstFramePath !== 'string' || normalizePath(firstFramePath) !== normalizedSourcePath)
|
|
68
|
+
continue;
|
|
69
|
+
const state = resolveGenerationState(meta, now);
|
|
70
|
+
if (state !== 'generating' && state !== 'stale' && state !== 'failed')
|
|
71
|
+
continue;
|
|
72
|
+
const startedAt = Date.parse(String(meta.job?.started_at ?? ''));
|
|
73
|
+
const sortableStartedAt = Number.isFinite(startedAt) ? startedAt : Number.NEGATIVE_INFINITY;
|
|
74
|
+
if (!selected || sortableStartedAt > selectedStartedAt) {
|
|
75
|
+
selected = entry;
|
|
76
|
+
selectedStartedAt = sortableStartedAt;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return selected ?? direct;
|
|
80
|
+
}
|