akari-video 0.1.36 → 0.1.38
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 +4 -2
- package/package.json +1 -1
- package/src/history-policy.mjs +140 -0
- package/src/store-command.mjs +208 -5
- package/vendor/.akari-capability-sources.json +3 -0
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +40 -0
- package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +104 -38
- package/vendor/docs/contract-2026-08-28-osr-export-v0.md +4 -0
- package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +2 -0
- package/vendor/docs/contract-2026-09-02-export-verify-declared-vs-measured-v0.md +3 -1
- package/vendor/packages/akari-launcher/README.md +4 -2
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-launcher/src/history-policy.mjs +140 -0
- package/vendor/packages/asset-resolver/README.md +11 -3
- package/vendor/packages/asset-resolver/bin/akari-assets.mjs +2 -1
- package/vendor/packages/asset-resolver/src/catalog.mjs +26 -17
- package/vendor/packages/asset-resolver/src/fetch-file.mjs +4 -1
- package/vendor/packages/asset-resolver/src/installed.mjs +116 -0
- package/vendor/packages/asset-resolver/src/resolve.mjs +1 -1
- package/vendor/packages/asset-resolver/src/state.mjs +2 -1
- package/vendor/packages/asset-resolver/test/installed-assets.test.mjs +184 -0
- package/vendor/packages/edit-lint/README.md +18 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +502 -11
- package/vendor/packages/edit-store/lib/internal-model.d.ts +9 -0
- package/vendor/packages/edit-store/lib/internal-model.js +31 -0
- package/vendor/packages/gpu-export/README.ja.md +39 -17
- package/vendor/packages/gpu-export/README.md +43 -18
- package/vendor/packages/matte-rvm/README.md +5 -0
- package/vendor/packages/matte-rvm/package.json +2 -2
- package/vendor/packages/overlay-runtime/README.md +7 -3
- package/vendor/packages/project-scaffold/src/index.mjs +6 -14
- package/vendor/packages/render-cut/README.ja.md +47 -0
- package/vendor/packages/render-cut/README.md +48 -0
- package/vendor/skills/analyze-footage/bin/person-matte/mask-from-alpha.mjs +15 -1
- package/vendor/skills/analyze-footage/bin/person-matte/mask-roundtrip.mjs +15 -1
- package/vendor/skills/analyze-footage/bin/person-matte/person-cutout.mjs +20 -4
- package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +134 -20
- package/vendor/skills/analyze-footage/bin/person-matte/resolve-packages.mjs +58 -0
- package/vendor/skills/analyze-footage/bin/test/person-matte-availability.test.mjs +51 -0
- package/vendor/skills/analyze-footage/person-matte.md +42 -16
- package/vendor/skills/edit-lint/SKILL.md +1 -1
- package/vendor/skills/manage-connections/SKILL.md +1 -0
- package/vendor/skills/manage-connections/bin/doctor.mjs +30 -6
- package/vendor/skills/manage-connections/bin/resolve-connections.mjs +30 -7
- package/vendor/skills/manage-connections/bin/resolve-packages.mjs +58 -0
- package/vendor/skills/manage-connections/bin/test/resolve-packages.test.mjs +106 -0
- package/vendor/skills/overlay-authoring/3d.md +16 -3
- package/vendor/skills/overlay-authoring/motion.md +19 -0
- package/vendor/skills/render-cut/SKILL.md +5 -0
- package/vendor/skills/setup-chat-approval/bin/doctor.mjs +30 -9
- package/vendor/skills/setup-chat-approval/bin/find-chat-id.mjs +28 -5
- package/vendor/skills/setup-chat-approval/bin/resolve-packages.mjs +58 -0
- package/vendor/skills/setup-chat-approval/setup-token.md +6 -2
- package/vendor/skills/verify/SKILL.md +5 -0
package/README.md
CHANGED
|
@@ -45,9 +45,11 @@ DL・sha256 検証・適用まで実行。それ以外(npm グローバル / g
|
|
|
45
45
|
`akari narration generate ...`(VOICEVOX / fal-qwen3 ナレーション生成)/
|
|
46
46
|
`akari internal beat-sync-<beatmap|probe-frame|render-when-idle> ...`(beat-sync-edit 内部実行物)/
|
|
47
47
|
`akari sounds [--variant wav] [--force]`(公式音源の一括ダウンロード。プロンプトなし・headless 可)/
|
|
48
|
-
`akari store <connect|status|download|disconnect>`(AKARI Store 連携。マイページで発行した
|
|
48
|
+
`akari store <connect|status|install|download|disconnect>`(AKARI Store 連携。マイページで発行した
|
|
49
49
|
接続トークンを `~/.akari/store-credentials.json`(0600)に保存し、購入済み一覧の確認と
|
|
50
|
-
配布物の取得ができる。`
|
|
50
|
+
配布物の取得ができる。`install <productId> [--from <zip>]` は購入パックを展開し、`PACK.json` の
|
|
51
|
+
収載素材を `~/.akari/assets/installed.json` へ登録する。`--from` は開発・オフライン導入時に手元の
|
|
52
|
+
zip を使う。`src/store-command.mjs`)/
|
|
51
53
|
`akari assets <list|fetch|sync|...>`(素材カタログの一覧・取得・同期。
|
|
52
54
|
`packages/asset-resolver` の CLI への薄い委譲で、カタログ合成・entitlements 判定・
|
|
53
55
|
sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-command.mjs`)/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
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": {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 変更履歴に何を入れるかの単一の宣言。
|
|
3
|
+
*
|
|
4
|
+
* `clean-manifest.mjs` が「ディスク上で消してよいか」を宣言するのに対し、本モジュールは
|
|
5
|
+
* 「変更履歴に入れるか」を宣言する。この 2 つは別の軸である(納品した mp4 はディスクでは
|
|
6
|
+
* 保持だが、履歴には入れない)。
|
|
7
|
+
*
|
|
8
|
+
* ここが単一の出所である理由: 同じ拡張子の知識が
|
|
9
|
+
* - プロジェクトの `.gitignore` 雛形(節目ごとの自動スナップショットが何を積むか)
|
|
10
|
+
* - `isInternalOrBinaryPath()`(「変更を見る」が何を差分表示しないか)
|
|
11
|
+
* の 2 か所に分かれて存在し、後者だけが育っていたため、書き出した動画が履歴に積まれ続けて
|
|
12
|
+
* `.git` が 4.2 GB に達した(issue #48)。以後はどちらも本モジュールから導出する。
|
|
13
|
+
*
|
|
14
|
+
* 置き場が akari-launcher/src なのは、`apps/shell` の akari-project 拡張が既に
|
|
15
|
+
* `akari-video/src/*.mjs` を直接 import しており(依存追加もロックファイル更新も要らない)、
|
|
16
|
+
* npm 配布 tarball では `src/` がそのまま同梱されるため。`packages/project-scaffold` からは
|
|
17
|
+
* 相対パスで参照する(prepack が本ファイルを vendor ミラーにも焼くので、モノレポと配布物の
|
|
18
|
+
* どちらでも同じ相対パスで解決できる)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 書き出し・レンダリング・キャッシュが作る映像/音声/画像の拡張子。
|
|
23
|
+
* 原本(`assets/`)はディレクトリ単位で別に除外するので、ここは「作り直せる生成物」だけを見る。
|
|
24
|
+
*/
|
|
25
|
+
export const GENERATED_MEDIA_EXTENSIONS = Object.freeze([
|
|
26
|
+
'.mp4',
|
|
27
|
+
'.mov',
|
|
28
|
+
'.m4v',
|
|
29
|
+
'.webm',
|
|
30
|
+
'.mkv',
|
|
31
|
+
'.avi',
|
|
32
|
+
'.png',
|
|
33
|
+
'.jpg',
|
|
34
|
+
'.jpeg',
|
|
35
|
+
'.gif',
|
|
36
|
+
'.webp',
|
|
37
|
+
'.bmp',
|
|
38
|
+
'.wav',
|
|
39
|
+
'.mp3'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const GENERATED_MEDIA_EXTENSION_SET = new Set(GENERATED_MEDIA_EXTENSIONS);
|
|
43
|
+
|
|
44
|
+
/** アプリが管理する囲みの開始行。利用者が書き足した行は囲みの外に残す。 */
|
|
45
|
+
export const HISTORY_BLOCK_BEGIN = '# >>> AKARI Video: 変更履歴に入れないもの(この囲みの中はアプリが更新します)>>>';
|
|
46
|
+
/** アプリが管理する囲みの終了行。 */
|
|
47
|
+
export const HISTORY_BLOCK_END = '# <<< AKARI Video: ここまで <<<';
|
|
48
|
+
|
|
49
|
+
const HISTORY_BLOCK_BODY = [
|
|
50
|
+
'# 元の映像と音声はディスクに残したまま、変更履歴には入れません。',
|
|
51
|
+
'assets/**',
|
|
52
|
+
'!assets/.gitkeep',
|
|
53
|
+
'',
|
|
54
|
+
'# 書き出した映像・音声・画像は edit.json と assets/ から作り直せます。',
|
|
55
|
+
'# ディスクには残ります。変更履歴に入れないだけです。',
|
|
56
|
+
...GENERATED_MEDIA_EXTENSIONS.map(extension => `*${extension}`),
|
|
57
|
+
'',
|
|
58
|
+
'# 書き出しの一時作業領域・キャッシュ・「変更を見る」の一時ファイル。',
|
|
59
|
+
'.akari/render-tmp/**',
|
|
60
|
+
'.akari/cache/**',
|
|
61
|
+
'.akari/diffs/**',
|
|
62
|
+
'!.akari/diffs/.gitkeep',
|
|
63
|
+
'',
|
|
64
|
+
'# 素材の分析結果は作り直すのに手間がかかるので、変更履歴に残します。',
|
|
65
|
+
'!.akari/sidecars/**',
|
|
66
|
+
'',
|
|
67
|
+
'# パソコンが作る一時ファイル。',
|
|
68
|
+
'.DS_Store',
|
|
69
|
+
'Thumbs.db'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** アプリが管理する囲みそのもの(前後の空行は含まない)。 */
|
|
73
|
+
export const HISTORY_BLOCK = [HISTORY_BLOCK_BEGIN, ...HISTORY_BLOCK_BODY, HISTORY_BLOCK_END].join('\n');
|
|
74
|
+
|
|
75
|
+
/** 新規プロジェクトへ書く `.gitignore` の全文。 */
|
|
76
|
+
export const PROJECT_GITIGNORE = `${HISTORY_BLOCK}\n`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 囲みを導入する前の世代の `.gitignore` 全文。利用者が一切触っていなければ全文を差し替える。
|
|
80
|
+
* 一致しなければ「利用者が書き換えた」と見なし、囲みを末尾へ足すだけにする。
|
|
81
|
+
*/
|
|
82
|
+
export const LEGACY_PROJECT_GITIGNORES = Object.freeze([
|
|
83
|
+
[
|
|
84
|
+
'# Source video and audio are intentionally kept outside the project history.',
|
|
85
|
+
'assets/**',
|
|
86
|
+
'!assets/.gitkeep',
|
|
87
|
+
'',
|
|
88
|
+
'# Temporary files used by the friendly "変更を見る" view.',
|
|
89
|
+
'.akari/diffs/**',
|
|
90
|
+
'!.akari/diffs/.gitkeep',
|
|
91
|
+
'',
|
|
92
|
+
'# Local operating-system files.',
|
|
93
|
+
'.DS_Store',
|
|
94
|
+
'Thumbs.db',
|
|
95
|
+
''
|
|
96
|
+
].join('\n')
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 作り直せる映像/音声/画像か。パス区切りは `/` でも `\` でもよい。
|
|
101
|
+
* 拡張子だけを見るので、置き場所の規則(sidecars は履歴に残す等)は呼び出し側が足す。
|
|
102
|
+
*/
|
|
103
|
+
export function hasGeneratedMediaExtension(file) {
|
|
104
|
+
const value = String(file ?? '');
|
|
105
|
+
const separator = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
|
106
|
+
const dot = value.lastIndexOf('.');
|
|
107
|
+
if (dot <= separator + 1) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return GENERATED_MEDIA_EXTENSION_SET.has(value.slice(dot).toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 既存の `.gitignore` 本文へ現行の囲みを反映した本文を返す。ファイルが無い場合は undefined を渡す。
|
|
115
|
+
*
|
|
116
|
+
* - 囲みがある → 囲みの中だけを差し替える(外に書き足した行はそのまま)
|
|
117
|
+
* - ファイルが無い・空・旧世代の全文と完全一致 → 全文を差し替える
|
|
118
|
+
* - それ以外(利用者が書き換えている) → 末尾へ囲みを足す(既存の行は消さない)
|
|
119
|
+
*/
|
|
120
|
+
export function applyHistoryPolicy(currentText) {
|
|
121
|
+
if (currentText === undefined || currentText === null || String(currentText).trim() === '') {
|
|
122
|
+
return { text: PROJECT_GITIGNORE, changed: true, mode: 'created' };
|
|
123
|
+
}
|
|
124
|
+
const text = String(currentText);
|
|
125
|
+
const begin = text.indexOf(HISTORY_BLOCK_BEGIN);
|
|
126
|
+
if (begin !== -1) {
|
|
127
|
+
const endStart = text.indexOf(HISTORY_BLOCK_END, begin);
|
|
128
|
+
if (endStart !== -1) {
|
|
129
|
+
const next = text.slice(0, begin) + HISTORY_BLOCK + text.slice(endStart + HISTORY_BLOCK_END.length);
|
|
130
|
+
return next === text
|
|
131
|
+
? { text, changed: false, mode: 'unchanged' }
|
|
132
|
+
: { text: next, changed: true, mode: 'updated-block' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (LEGACY_PROJECT_GITIGNORES.includes(text)) {
|
|
136
|
+
return { text: PROJECT_GITIGNORE, changed: true, mode: 'replaced' };
|
|
137
|
+
}
|
|
138
|
+
const separator = text.endsWith('\n') ? '\n' : '\n\n';
|
|
139
|
+
return { text: `${text}${separator}${HISTORY_BLOCK}\n`, changed: true, mode: 'appended' };
|
|
140
|
+
}
|
package/src/store-command.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import {
|
|
3
3
|
copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync,
|
|
4
|
-
readdirSync, rmSync, writeFileSync
|
|
4
|
+
readFileSync, readdirSync, renameSync, rmSync, writeFileSync
|
|
5
5
|
} from 'node:fs';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
@@ -53,6 +53,173 @@ function findFile(dir, name) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const INSTALLED_ASSETS_SCHEMA = 'akari-installed-assets/v0';
|
|
57
|
+
|
|
58
|
+
function readJsonFile(filePath) {
|
|
59
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function titleFromMeta(assetRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const meta = readJsonFile(path.join(assetRoot, 'meta.json'));
|
|
65
|
+
return typeof meta.title === 'string' && meta.title ? meta.title : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isSafePathSegment(value) {
|
|
72
|
+
return typeof value === 'string' && value.length > 0
|
|
73
|
+
&& value !== '.' && value !== '..'
|
|
74
|
+
&& !value.includes('/') && !value.includes('\\');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pathWithin(root, ...parts) {
|
|
78
|
+
const absoluteRoot = path.resolve(root);
|
|
79
|
+
const candidate = path.resolve(absoluteRoot, ...parts);
|
|
80
|
+
if (candidate !== absoluteRoot && !candidate.startsWith(`${absoluteRoot}${path.sep}`)) {
|
|
81
|
+
throw new Error(`PACK.json の path がパック外を指しています: ${parts.join('/')}`);
|
|
82
|
+
}
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function flattenPackContents(pack, packRoot) {
|
|
87
|
+
if (!Array.isArray(pack?.contents)) {
|
|
88
|
+
throw new Error('PACK.json に contents[] がありません');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const items = [];
|
|
92
|
+
for (const entry of pack.contents) {
|
|
93
|
+
const candidates = Array.isArray(entry?.assets)
|
|
94
|
+
? entry.assets.map((asset) => ({ asset, parentTitle: entry.title }))
|
|
95
|
+
: [{ asset: entry, parentTitle: null }];
|
|
96
|
+
if (candidates.length === 0) {
|
|
97
|
+
throw new Error('PACK.json の contents[] に空の assets[] があります');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const { asset, parentTitle } of candidates) {
|
|
101
|
+
if (!asset || !isSafePathSegment(asset.id)
|
|
102
|
+
|| typeof asset.path !== 'string' || !asset.path) {
|
|
103
|
+
throw new Error('PACK.json の contents[] に不正な id / path があります');
|
|
104
|
+
}
|
|
105
|
+
const assetRoot = pathWithin(packRoot, asset.path);
|
|
106
|
+
if (!Array.isArray(asset.files) || asset.files.length === 0) {
|
|
107
|
+
throw new Error(`PACK.json の item に files[] がありません: ${asset.id}`);
|
|
108
|
+
}
|
|
109
|
+
const files = asset.files.map((file) => {
|
|
110
|
+
if (!file || typeof file.path !== 'string' || !file.path
|
|
111
|
+
|| !Number.isInteger(file.bytes) || file.bytes < 0
|
|
112
|
+
|| typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)) {
|
|
113
|
+
throw new Error(`PACK.json の files[] が不正です: ${asset.id}`);
|
|
114
|
+
}
|
|
115
|
+
pathWithin(assetRoot, file.path);
|
|
116
|
+
return {
|
|
117
|
+
path: file.path,
|
|
118
|
+
bytes: file.bytes,
|
|
119
|
+
sha256: file.sha256
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
const version = asset.version ?? pack.version;
|
|
123
|
+
if (version === undefined || version === null) {
|
|
124
|
+
throw new Error(`PACK.json の item に version がありません: ${asset.id}`);
|
|
125
|
+
}
|
|
126
|
+
items.push({
|
|
127
|
+
id: asset.id,
|
|
128
|
+
title: (typeof asset.title === 'string' && asset.title)
|
|
129
|
+
|| titleFromMeta(assetRoot)
|
|
130
|
+
|| (typeof parentTitle === 'string' && parentTitle)
|
|
131
|
+
|| asset.id,
|
|
132
|
+
path: asset.path,
|
|
133
|
+
version,
|
|
134
|
+
files
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function registerInstalledPack(env, productId, packPath) {
|
|
142
|
+
const home = resolveAkariHome(env);
|
|
143
|
+
const indexPath = path.join(home, 'assets', 'installed.json');
|
|
144
|
+
const packRoot = path.dirname(packPath);
|
|
145
|
+
const pack = readJsonFile(packPath);
|
|
146
|
+
if ((typeof pack?.version !== 'string' && typeof pack?.version !== 'number')) {
|
|
147
|
+
throw new Error('PACK.json に version がありません');
|
|
148
|
+
}
|
|
149
|
+
let index = { schema: INSTALLED_ASSETS_SCHEMA, packs: {} };
|
|
150
|
+
|
|
151
|
+
if (existsSync(indexPath)) {
|
|
152
|
+
index = readJsonFile(indexPath);
|
|
153
|
+
if (index?.schema !== INSTALLED_ASSETS_SCHEMA
|
|
154
|
+
|| !index.packs || typeof index.packs !== 'object' || Array.isArray(index.packs)) {
|
|
155
|
+
throw new Error(`導入済み素材索引の形式が想定と違います: ${indexPath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const items = flattenPackContents(pack, packRoot);
|
|
160
|
+
index.packs[productId] = {
|
|
161
|
+
version: pack.version,
|
|
162
|
+
installedAt: new Date().toISOString(),
|
|
163
|
+
root: packRoot,
|
|
164
|
+
items
|
|
165
|
+
};
|
|
166
|
+
mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
167
|
+
const tempPath = `${indexPath}.tmp-${process.pid}`;
|
|
168
|
+
writeFileSync(tempPath, `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
|
|
169
|
+
renameSync(tempPath, indexPath);
|
|
170
|
+
return items;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const KNOWN_BUNDLE_COMPONENTS = new Map([
|
|
174
|
+
['multi-device-combo', ['phone-pro-titanium', 'laptop-slim-aluminum', 'app-icon-squircle']]
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
async function readJsonResponse(res) {
|
|
178
|
+
try {
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
return data && typeof data === 'object' ? data : null;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function componentIds(value) {
|
|
187
|
+
if (!Array.isArray(value)) return [];
|
|
188
|
+
return value
|
|
189
|
+
.map((component) => typeof component === 'string'
|
|
190
|
+
? component
|
|
191
|
+
: component?.id ?? component?.product_id)
|
|
192
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function bundleDetails(data, productId) {
|
|
196
|
+
const candidates = [data, data?.product, data?.status];
|
|
197
|
+
const products = Array.isArray(data?.products) ? data.products : [];
|
|
198
|
+
const matchingProduct = products.find((product) =>
|
|
199
|
+
product?.id === productId || product?.product_id === productId);
|
|
200
|
+
if (matchingProduct) candidates.push(matchingProduct);
|
|
201
|
+
|
|
202
|
+
const bundle = candidates.find((candidate) => candidate?.kind === 'bundle');
|
|
203
|
+
if (!bundle) return null;
|
|
204
|
+
return { components: componentIds(bundle.components ?? data?.components) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function resolveBundleDetails(fetchImpl, creds, productId, errorData) {
|
|
208
|
+
const knownComponents = KNOWN_BUNDLE_COMPONENTS.get(productId);
|
|
209
|
+
if (knownComponents) return { components: knownComponents };
|
|
210
|
+
|
|
211
|
+
const fromError = bundleDetails(errorData, productId);
|
|
212
|
+
if (fromError) return fromError;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const productsRes = await fetchImpl(`${creds.url}/products`);
|
|
216
|
+
if (!productsRes.ok) return null;
|
|
217
|
+
return bundleDetails(await readJsonResponse(productsRes), productId);
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
56
223
|
export async function runStoreCommand(args, options = {}) {
|
|
57
224
|
const log = options.log ?? ((line) => console.log(line));
|
|
58
225
|
const env = options.env ?? process.env;
|
|
@@ -149,6 +316,27 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
149
316
|
return { exitCode: 1 };
|
|
150
317
|
}
|
|
151
318
|
if (!res.ok) {
|
|
319
|
+
const data = await readJsonResponse(res);
|
|
320
|
+
if (res.status === 404) {
|
|
321
|
+
const bundle = await resolveBundleDetails(fetchImpl, creds, productId, data);
|
|
322
|
+
if (bundle) {
|
|
323
|
+
const componentList = bundle.components.length > 0
|
|
324
|
+
? `: ${bundle.components.join(', ')}`
|
|
325
|
+
: '';
|
|
326
|
+
log(`セット商品は構成商品を個別に download してください${componentList}`);
|
|
327
|
+
return { exitCode: 1 };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (res.status === 404 && data?.error === 'unknown_product') {
|
|
331
|
+
log(`${typeof data.message === 'string' ? data.message : '商品が見つかりません'}(${productId})`);
|
|
332
|
+
return { exitCode: 1 };
|
|
333
|
+
}
|
|
334
|
+
if (res.status === 404 && data?.error === 'artifact_missing') {
|
|
335
|
+
log(typeof data.message === 'string'
|
|
336
|
+
? data.message
|
|
337
|
+
: '配布物が未入稿です。サポートへご連絡ください');
|
|
338
|
+
return { exitCode: 1 };
|
|
339
|
+
}
|
|
152
340
|
log(`ダウンロードに失敗しました(${res.status})`);
|
|
153
341
|
return { exitCode: 1 };
|
|
154
342
|
}
|
|
@@ -164,13 +352,22 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
164
352
|
|
|
165
353
|
if (sub === 'install') {
|
|
166
354
|
const productId = args[1];
|
|
167
|
-
if (!productId || productId.startsWith('--')) {
|
|
168
|
-
log('使い方: akari store install <productId>');
|
|
355
|
+
if (!isSafePathSegment(productId) || productId.startsWith('--')) {
|
|
356
|
+
log('使い方: akari store install <productId> [--from <zip>]');
|
|
357
|
+
return { exitCode: 1 };
|
|
358
|
+
}
|
|
359
|
+
const hasFrom = args.includes('--from');
|
|
360
|
+
const fromZip = parseFlag(args, '--from');
|
|
361
|
+
if (hasFrom && !fromZip) {
|
|
362
|
+
log('使い方: akari store install <productId> [--from <zip>]');
|
|
169
363
|
return { exitCode: 1 };
|
|
170
364
|
}
|
|
171
365
|
const stage = mkdtempSync(path.join(tmpdir(), 'akari-store-install-'));
|
|
172
366
|
try {
|
|
173
|
-
const dl =
|
|
367
|
+
const dl = fromZip
|
|
368
|
+
? { exitCode: existsSync(fromZip) ? 0 : 1, filePath: path.resolve(fromZip) }
|
|
369
|
+
: await runStoreCommand(['download', productId, '--dest', stage], options);
|
|
370
|
+
if (fromZip && dl.exitCode !== 0) log(`zip が見つかりません: ${fromZip}`);
|
|
174
371
|
if (dl.exitCode !== 0) return { exitCode: 1 };
|
|
175
372
|
const extractDir = path.join(stage, 'x');
|
|
176
373
|
mkdirSync(extractDir, { recursive: true });
|
|
@@ -218,6 +415,12 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
218
415
|
const readme = findFile(destDir, 'README.md');
|
|
219
416
|
log(`展開しました: ${destDir}`);
|
|
220
417
|
if (readme) log(`導入手順: ${readme}`);
|
|
418
|
+
const packPath = findFile(destDir, 'PACK.json');
|
|
419
|
+
if (packPath) {
|
|
420
|
+
const items = registerInstalledPack(env, productId, packPath);
|
|
421
|
+
log(`akari assets list に ${items.length} 件を登録しました`);
|
|
422
|
+
if (items[0]) log(`次の一手: akari assets fetch ${items[0].id}`);
|
|
423
|
+
}
|
|
221
424
|
return { exitCode: 0 };
|
|
222
425
|
} finally {
|
|
223
426
|
rmSync(stage, { recursive: true, force: true });
|
|
@@ -236,7 +439,7 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
236
439
|
log('使い方: akari store <connect|status|install|download|disconnect>');
|
|
237
440
|
log(' connect ブラウザで承認して接続(既定。--token akst_... で手動 / --no-open でブラウザを開かない / --url <base>)');
|
|
238
441
|
log(' status 接続状態と購入済み一覧');
|
|
239
|
-
log(' install <productId>
|
|
442
|
+
log(' install <productId> [--from <zip>] 購入済み商品の導入(--from は手元 zip / PACK.json 素材を installed 索引へ登録)');
|
|
240
443
|
log(' download <productId> [--dest <dir>] 購入済み配布物の取得のみ');
|
|
241
444
|
log(' disconnect 接続解除');
|
|
242
445
|
return { exitCode: sub ? 1 : 0 };
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"packages/intake-form/package.json",
|
|
94
94
|
"packages/intake-form/README.md",
|
|
95
95
|
"packages/matte-rvm/package.json",
|
|
96
|
+
"packages/matte-rvm/README.md",
|
|
96
97
|
"packages/media-bin/package.json",
|
|
97
98
|
"packages/media-bin/README.ja.md",
|
|
98
99
|
"packages/media-bin/README.md",
|
|
@@ -104,6 +105,8 @@
|
|
|
104
105
|
"packages/preview-server/package.json",
|
|
105
106
|
"packages/project-scaffold/package.json",
|
|
106
107
|
"packages/render-cut/package.json",
|
|
108
|
+
"packages/render-cut/README.ja.md",
|
|
109
|
+
"packages/render-cut/README.md",
|
|
107
110
|
"packages/schemas/package.json",
|
|
108
111
|
"packages/template-render/package.json",
|
|
109
112
|
"packages/template-render/README.ja.md",
|
|
@@ -202,3 +202,43 @@ updated: 2026-08-26
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
```
|
|
205
|
+
|
|
206
|
+
## 10. 追記(2026-09-03)— 変更履歴に何を入れるか(`.gitignore` 雛形)
|
|
207
|
+
|
|
208
|
+
§2-2 と §9 が扱うのは「ディスク上で消してよいか」である。本節が定めるのは**別の軸**、
|
|
209
|
+
「変更履歴(節目ごとの自動スナップショット)に入れるか」である。納品した `exports/*.mp4` は
|
|
210
|
+
ディスクでは保持だが履歴には入れない、というように 2 つの軸は独立に決まる。
|
|
211
|
+
|
|
212
|
+
雛形の `.gitignore` が生成物を除外していなかったため、40 秒の動画 1 本のプロジェクトで
|
|
213
|
+
`.git` が 4.2 GB に達した(6 プロジェクト合計で `.git` が全体の 70%・素材は 4%)。
|
|
214
|
+
原因は「同じ拡張子の知識が 2 か所に分かれ、片方だけが育っていた」ことである。
|
|
215
|
+
|
|
216
|
+
| 変更履歴に入れる | 入れない |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `edit.json` / `captions.json` / `review.json` / `plan.json` | `assets/**`(原本) |
|
|
219
|
+
| `planning/**` / `.akari/events/**` / `motion/**` | 生成された映像・音声・画像(下表の拡張子) |
|
|
220
|
+
| `.akari/reports/**` の JSON・HTML(証跡の実体) | `.akari/render-tmp/**` / `.akari/cache/**` / `.akari/diffs/**` |
|
|
221
|
+
| `.akari/sidecars/**`(映像を含めて全部) | |
|
|
222
|
+
|
|
223
|
+
- 除外する拡張子: `.mp4` `.mov` `.m4v` `.webm` `.mkv` `.avi` `.png` `.jpg` `.jpeg` `.gif`
|
|
224
|
+
`.webp` `.bmp` `.wav` `.mp3`
|
|
225
|
+
- 例外は 2 つだけである。`.akari/sidecars/**` は分析に手間がかかるため映像でも履歴に残す。
|
|
226
|
+
`.akari/reports/**` は拡張子で分かれ、png は外れ JSON・HTML は残る
|
|
227
|
+
- 単一の出所は `packages/akari-launcher/src/history-policy.mjs`。`.gitignore` 雛形
|
|
228
|
+
(`templates/project-default/.gitignore` と `project-scaffold` の `PROJECT_GITIGNORE`)と、
|
|
229
|
+
「変更を見る」の除外判定(`isInternalOrBinaryPath()`)は**どちらもここから導出する**。
|
|
230
|
+
片方だけに拡張子を足すことは契約違反である
|
|
231
|
+
|
|
232
|
+
### 移行
|
|
233
|
+
|
|
234
|
+
アプリはプロジェクトを開いたとき、`.gitignore` を現行の内容へ揃え、新たに対象となった
|
|
235
|
+
追跡済みファイルを `git rm --cached` で履歴から外し、1 本コミットする。
|
|
236
|
+
|
|
237
|
+
- **ディスク上のファイルは消さない**(利用者の成果物であり、消してよいかは §9 の宣言表が別に判断する)
|
|
238
|
+
- 履歴そのもの(過去のコミットが抱える blob)は書き換えない。これは利用者の明示操作の領域であり、
|
|
239
|
+
移行だけでは `.git` は縮まず横ばいになる
|
|
240
|
+
- `.gitignore` のうちアプリが管理するのは `# >>> AKARI Video ... >>>` から `# <<< ... <<<` までの
|
|
241
|
+
囲みだけとする。囲みの外に利用者が書き足した行は消さない
|
|
242
|
+
- 自分がルートである git リポジトリのときだけ動く。親リポジトリの中に置かれたプロジェクトは、
|
|
243
|
+
こちらが管理していないリポジトリの追跡対象を黙って変えないため対象外とする(§3 の
|
|
244
|
+
「強制的にファイルを移動・削除するスクリプトは作らない」と同じ抑制)
|