akari-video 1.0.2 → 1.1.0-beta.1
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/self-update.mjs +11 -1
- package/src/update-check.mjs +34 -29
- package/src/version.mjs +23 -0
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-09-02-audio-envelope-v1.md +9 -0
- package/vendor/docs/contract-2026-10-06-timeline-ripple-v0.md +33 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/asset-resolver/src/place-library-asset.mjs +54 -0
- package/vendor/packages/asset-resolver/src/project-reference-listing.mjs +22 -0
- package/vendor/packages/asset-resolver/src/project-references.mjs +24 -1
- package/vendor/packages/asset-resolver/src/resolve-async-library.mjs +113 -0
- package/vendor/packages/asset-resolver/src/resolve.mjs +40 -13
- package/vendor/packages/asset-resolver/src/shell-reference.mjs +27 -14
- package/vendor/packages/asset-resolver/test/library-fetch-no-spawn-reference.test.mjs +61 -0
- package/vendor/packages/asset-resolver/test/resolve-legacy-meta-tier.test.mjs +52 -0
- package/vendor/packages/edit-lint/src/engine-capabilities.json +2 -0
- package/vendor/packages/edit-lint/src/lint/edit-v2.mjs +10 -0
- package/vendor/packages/edit-store/lib/caption-store.d.ts +6 -0
- package/vendor/packages/edit-store/lib/caption-store.js +3 -2
- package/vendor/packages/edit-store/lib/cut-ranges.js +12 -50
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +9 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +12 -2
- package/vendor/packages/edit-store/lib/envelope.d.ts +10 -0
- package/vendor/packages/edit-store/lib/envelope.js +95 -0
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +2 -2
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +3 -1
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +15 -4
- package/vendor/packages/edit-store/lib/ripple.d.ts +42 -0
- package/vendor/packages/edit-store/lib/ripple.js +438 -0
- package/vendor/packages/edit-store/lib/shape-markup-v1.js +17 -13
- package/vendor/packages/edit-store/lib/shape-markup.js +5 -2
- package/vendor/packages/edit-store/lib/webview-kernel.js +118 -16
- package/vendor/packages/overlay-runtime/src/interaction.css +7 -0
- package/vendor/packages/overlay-runtime/src/interaction.js +68 -5
- package/vendor/packages/schemas/edit.schema.json +11 -1
- package/vendor/packages/schemas/engine-capabilities.json +2 -0
- package/vendor/packages/schemas/test/audio-fade-shape.test.mjs +31 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +12 -0
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.1",
|
|
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/self-update.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
import { resolveAkariHome } from './update-check.mjs';
|
|
7
|
+
import { readUpdateChannel, resolveAkariHome } from './update-check.mjs';
|
|
8
|
+
import { compareVersions } from './version.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* `akari update` の実適用(update-and-versioning 契約(内部リポ)§11):
|
|
@@ -202,6 +203,15 @@ export async function stageSelfUpdate({
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
const version = feed.product;
|
|
206
|
+
if (readUpdateChannel(env) === 'stable' && version?.includes('-')) {
|
|
207
|
+
log('安定版だけを受け取る設定です。');
|
|
208
|
+
return { ok: false, reason: 'channel-mismatch' };
|
|
209
|
+
}
|
|
210
|
+
const installedVersion = readVersionAt(resolveAppDir(env));
|
|
211
|
+
if (installedVersion && compareVersions(version, installedVersion) < 0) {
|
|
212
|
+
log('現在より古い版は適用できません。');
|
|
213
|
+
return { ok: false, reason: 'older-version' };
|
|
214
|
+
}
|
|
205
215
|
const stagingRoot = resolveStagingRoot(env);
|
|
206
216
|
const stagingDir = resolveStagingDir(env, version);
|
|
207
217
|
const downloadPath = join(stagingRoot, `.download-${version}.tar.gz`);
|
package/src/update-check.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
import { isRunningFromAppDir, resolveStagingDir, stageSelfUpdate, swapStagedApp } from './self-update.mjs';
|
|
8
|
+
import { compareVersions } from './version.mjs';
|
|
9
|
+
export { compareVersions } from './version.mjs';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* CLI(と、同じ契約に従うシェル)の更新検知・通知の共通ロジック
|
|
@@ -35,6 +37,7 @@ import { isRunningFromAppDir, resolveStagingDir, stageSelfUpdate, swapStagedApp
|
|
|
35
37
|
*/
|
|
36
38
|
|
|
37
39
|
export const DEFAULT_UPDATE_FEED_URL = 'https://github.com/AkariLabs/akari-video/releases/download/updates/latest.json';
|
|
40
|
+
export const PRERELEASE_UPDATE_FEED_URL = 'https://github.com/AkariLabs/akari-video/releases/download/updates/prerelease.json';
|
|
38
41
|
|
|
39
42
|
const CACHE_SCHEMA = 1;
|
|
40
43
|
const FETCH_TIMEOUT_MS = 5000;
|
|
@@ -42,8 +45,18 @@ const FETCH_TIMEOUT_MS = 5000;
|
|
|
42
45
|
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
43
46
|
const PACKAGE_ROOT = dirname(dirname(THIS_FILE));
|
|
44
47
|
|
|
45
|
-
export function
|
|
46
|
-
return
|
|
48
|
+
export function resolveUpdateChannel(value) {
|
|
49
|
+
return value === 'prerelease' ? 'prerelease' : 'stable';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readUpdateChannel(env = process.env) {
|
|
53
|
+
try { return resolveUpdateChannel(JSON.parse(readFileSync(join(resolveAkariHome(env), 'update-preferences.json'), 'utf8')).channel); }
|
|
54
|
+
catch { return 'stable'; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 明示 URL はテスト・運用の完全上書き。チャンネルは評価側でも独立に検査する。
|
|
58
|
+
export function resolveFeedUrl(env = process.env, channel = readUpdateChannel(env)) {
|
|
59
|
+
return env.AKARI_UPDATE_FEED_URL || (channel === 'prerelease' ? PRERELEASE_UPDATE_FEED_URL : DEFAULT_UPDATE_FEED_URL);
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/** `~/.akari`(既定)または `AKARI_HOME`(テスト用にルートを差し替え可能)。 */
|
|
@@ -112,26 +125,6 @@ export function resolveInstalledVersionInfo({ env = process.env, cliVersion = re
|
|
|
112
125
|
};
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
/** "major.minor.patch" の先頭 3 要素だけを数値比較する(prerelease 考慮不要 — 契約 D4: stable のみ)。 */
|
|
116
|
-
export function compareVersions(a, b) {
|
|
117
|
-
const pa = parseVersionTriplet(a);
|
|
118
|
-
const pb = parseVersionTriplet(b);
|
|
119
|
-
if (!pa || !pb) {
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
for (let i = 0; i < 3; i++) {
|
|
123
|
-
if (pa[i] !== pb[i]) {
|
|
124
|
-
return pa[i] < pb[i] ? -1 : 1;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseVersionTriplet(value) {
|
|
131
|
-
const match = typeof value === 'string' ? value.trim().match(/^(\d+)\.(\d+)\.(\d+)/) : null;
|
|
132
|
-
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
128
|
/** `cache.feed` が最低限の形をしているか(壊れたフィードを弾く)。 */
|
|
136
129
|
export function isValidFeedShape(feed) {
|
|
137
130
|
return !!feed && typeof feed === 'object' && typeof feed.schema === 'number' && typeof feed.product === 'string';
|
|
@@ -141,7 +134,13 @@ export function isValidFeedShape(feed) {
|
|
|
141
134
|
export function readCacheSync(cachePath) {
|
|
142
135
|
try {
|
|
143
136
|
const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
144
|
-
|
|
137
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
138
|
+
// cli.mjs もこの関数でキャッシュを読む。設定切替直後の手動 update を含めて遮断。
|
|
139
|
+
let channel = 'stable';
|
|
140
|
+
try { channel = resolveUpdateChannel(JSON.parse(readFileSync(join(dirname(cachePath), 'update-preferences.json'), 'utf8')).channel); }
|
|
141
|
+
catch { /* 未設定は stable */ }
|
|
142
|
+
return channel === 'stable' && parsed.feed?.product?.includes('-')
|
|
143
|
+
? { ...parsed, feed: null, staged: undefined } : parsed;
|
|
145
144
|
} catch {
|
|
146
145
|
return null;
|
|
147
146
|
}
|
|
@@ -155,7 +154,7 @@ function writeCacheSync(cachePath, cache) {
|
|
|
155
154
|
/**
|
|
156
155
|
* キャッシュと現在版から、新版通知を出すべきかを判定する(同期・純粋関数・I/O なし)。
|
|
157
156
|
*/
|
|
158
|
-
export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
|
|
157
|
+
export function evaluateUpdateStatus({ currentVersion, cache, channel = 'stable', feedUrl, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
|
|
159
158
|
const versionDetails = {
|
|
160
159
|
currentVersion,
|
|
161
160
|
cliVersion,
|
|
@@ -168,7 +167,8 @@ export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = curre
|
|
|
168
167
|
mismatch
|
|
169
168
|
};
|
|
170
169
|
const feed = cache?.feed;
|
|
171
|
-
if (!isValidFeedShape(feed))
|
|
170
|
+
if (!isValidFeedShape(feed) || (channel === 'stable' && /-/.test(feed.product))
|
|
171
|
+
|| (cache?.feed_url && feedUrl && cache.feed_url !== feedUrl)) {
|
|
172
172
|
return { available: false, ...versionDetails };
|
|
173
173
|
}
|
|
174
174
|
const latest = feed.product;
|
|
@@ -200,7 +200,8 @@ export function checkForUpdateSync({ currentVersion, versionInfo, env = process.
|
|
|
200
200
|
?? (currentVersion === undefined
|
|
201
201
|
? resolveInstalledVersionInfo({ env })
|
|
202
202
|
: { cliVersion: currentVersion, appVersion: null, currentVersion, source: 'cli-fallback', mismatch: false });
|
|
203
|
-
return evaluateUpdateStatus({ ...versions, currentVersion: currentVersion ?? versions.currentVersion, cache
|
|
203
|
+
return evaluateUpdateStatus({ ...versions, currentVersion: currentVersion ?? versions.currentVersion, cache,
|
|
204
|
+
channel: readUpdateChannel(env), feedUrl: resolveFeedUrl(env) });
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
/** 「この版の通知を今後出さない」を記録する(同期・ローカル I/O のみ)。 */
|
|
@@ -234,7 +235,8 @@ export async function maybeStageInBackground({ env = process.env, feed, fetchImp
|
|
|
234
235
|
if (!appComponent?.url || !appComponent?.sha256) {
|
|
235
236
|
return null;
|
|
236
237
|
}
|
|
237
|
-
if (
|
|
238
|
+
if ((readUpdateChannel(env) === 'stable' && feed.product.includes('-'))
|
|
239
|
+
|| compareVersions(feed.product, resolveInstalledVersionInfo({ env }).currentVersion) <= 0) {
|
|
238
240
|
return null;
|
|
239
241
|
}
|
|
240
242
|
// npm 側 CLI と本体が分離していても、install-ref があれば管理対象の本体を更新できる。
|
|
@@ -262,7 +264,7 @@ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalT
|
|
|
262
264
|
return;
|
|
263
265
|
}
|
|
264
266
|
const feed = await response.json();
|
|
265
|
-
if (!isValidFeedShape(feed)) {
|
|
267
|
+
if (!isValidFeedShape(feed) || (readUpdateChannel(env) === 'stable' && feed.product.includes('-'))) {
|
|
266
268
|
return null;
|
|
267
269
|
}
|
|
268
270
|
const existing = readCacheSync(cachePath);
|
|
@@ -270,6 +272,7 @@ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalT
|
|
|
270
272
|
schema: CACHE_SCHEMA,
|
|
271
273
|
fetched_at: new Date().toISOString(),
|
|
272
274
|
feed,
|
|
275
|
+
feed_url: feedUrl,
|
|
273
276
|
dismissed: existing?.dismissed ?? {}
|
|
274
277
|
};
|
|
275
278
|
writeCacheSync(cachePath, next);
|
|
@@ -347,7 +350,9 @@ export function maybeApplyPendingUpdateOnLaunch({ env = process.env, log = () =>
|
|
|
347
350
|
const cache = readCacheSync(cachePath);
|
|
348
351
|
const staged = cache?.staged;
|
|
349
352
|
const feed = cache?.feed;
|
|
350
|
-
if (!staged?.version || !isValidFeedShape(feed) || staged.version !== feed.product
|
|
353
|
+
if (!staged?.version || !isValidFeedShape(feed) || staged.version !== feed.product
|
|
354
|
+
|| (readUpdateChannel(env) === 'stable' && feed.product.includes('-'))
|
|
355
|
+
|| (cache.feed_url && cache.feed_url !== resolveFeedUrl(env))) {
|
|
351
356
|
return { applied: false, reason: 'no-matching-staged-update' };
|
|
352
357
|
}
|
|
353
358
|
|
package/src/version.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** npm 依存ゼロの launcher 用 semver 比較。build metadata は順序に影響しない。 */
|
|
2
|
+
export function compareVersions(a, b) {
|
|
3
|
+
const parse = value => {
|
|
4
|
+
const match = typeof value === 'string' ? value.trim().match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/) : null;
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
const pre = match[4]?.split('.') ?? null;
|
|
7
|
+
if (pre?.some(id => /^0\d+$/.test(id))) return null;
|
|
8
|
+
return { core: match.slice(1, 4).map(Number), pre };
|
|
9
|
+
};
|
|
10
|
+
const left = parse(a), right = parse(b);
|
|
11
|
+
if (!left || !right) return 0;
|
|
12
|
+
for (let i = 0; i < 3; i++) if (left.core[i] !== right.core[i]) return Math.sign(left.core[i] - right.core[i]);
|
|
13
|
+
if (!left.pre || !right.pre) return left.pre === right.pre ? 0 : left.pre ? -1 : 1;
|
|
14
|
+
for (let i = 0; i < Math.min(left.pre.length, right.pre.length); i++) {
|
|
15
|
+
const x = left.pre[i], y = right.pre[i];
|
|
16
|
+
if (x === y) continue;
|
|
17
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
|
|
18
|
+
if (xn && yn) return Math.sign(BigInt(x) > BigInt(y) ? 1 : -1);
|
|
19
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
20
|
+
return x < y ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
return Math.sign(left.pre.length - right.pre.length);
|
|
23
|
+
}
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"docs/contract-2026-10-02-fragment-font-subset-v0.md",
|
|
83
83
|
"docs/contract-2026-10-02-textstyle-v1-rich.md",
|
|
84
84
|
"docs/contract-2026-10-04-asset-tier-v1.md",
|
|
85
|
+
"docs/contract-2026-10-06-timeline-ripple-v0.md",
|
|
85
86
|
"packages/akari-launcher/package.json",
|
|
86
87
|
"packages/akari-launcher/README.md",
|
|
87
88
|
"packages/akari-tools/package.json",
|
|
@@ -89,3 +89,12 @@ schema と reader は型・範囲・時刻順を error にする。lint は実
|
|
|
89
89
|
| 許容する差 | デコーダとサンプル境界 | デコーダとサンプル境界 |
|
|
90
90
|
|
|
91
91
|
afftdn、loudnorm、true peak guard は最終マスターだけの責務であり、プレビューには実装しない。
|
|
92
|
+
|
|
93
|
+
## 7. フェードの形
|
|
94
|
+
|
|
95
|
+
音声アイテムは `fade_in_shape` / `fade_out_shape` を任意に持つ。省略時は `linear` とし、
|
|
96
|
+
既存のフェード秒数、クランプ規則、フィルタ文字列を維持する。進行率 `p`(0〜1)に対する
|
|
97
|
+
ゲイン係数は `linear` = `p`、`equal_power` = `sin(pπ/2)`、
|
|
98
|
+
`s_curve` = `(1−cos(pπ))/2`、`slow` = `p²` とする。フェードアウトは時間方向を反転する。
|
|
99
|
+
書き出しの `afade` 曲線は順に `tri`、`qsin`、`hsin`、`qua` を使う。
|
|
100
|
+
プレビューではフェード区間を少なくとも 16 区間の折れ線で近似する。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: implemented
|
|
3
|
+
created: 2026-10-06
|
|
4
|
+
updated: 2026-10-06
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# タイムラインの範囲操作と詰めカーネル v0
|
|
8
|
+
|
|
9
|
+
実装モジュールは `packages/edit-store/src/ripple.ts`。`cut-ranges.ts` はこのカーネルを呼び出す。edit.json v2 の時刻は整数フレーム。`tracks[]` の配列順は合成順として維持する。範囲編集関数は入力を変更せず `{ edit, changed, removedFrames?, blocked?, reason? }` を返す。`changed: false` では `reason` に操作不能の理由を入れる。`setTrackRippleMode` は新しい edit を直接返し、`findGapAt` と `editPoints` は読み取り値を返す。
|
|
10
|
+
|
|
11
|
+
## トラックの札
|
|
12
|
+
|
|
13
|
+
`tracks[]` に任意の `target` / `sync` 真偽値を保存する。`resolveTrackRippleMode(track)` は `target: true` を `cut`、`target: false, sync: true` を `shift`、`target: false` とそれ以外の組を `fixed` とする。両キー省略時は visual が `cut`、audio は 1 件以上の全アイテムが `role: "bgm"` なら `fixed`、それ以外は `cut`。`sync` だけ指定したときは true が `shift`、false が `fixed`。`setTrackRippleMode` は `cut = (true,true)`、`shift = (false,true)`、`fixed = (false,false)` を保存する。
|
|
14
|
+
|
|
15
|
+
操作ごとの `modeOverride` は一時指定で、`lockedTrackIds` は常に固定として優先する。固定トラックは範囲操作で変えない。選択アイテムを明示して消す場合はそのトラックを削除対象とするが、ロック中なら操作全体を拒否する。
|
|
16
|
+
|
|
17
|
+
| 関数 | 内容 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `splitAtFrame` | 再生ヘッドをまたぐ cut トラックの media / html / telop / filter / 独立音声を分割。`itemIds` または `trackIds` で絞れる。リンクした映像・音声は同時に分割する。リンク相手がロック行にある場合、または左右どちらかが `ceil(fps × 0.15)` フレーム未満なら、全体を拒否して `changed: false` と `reason: "リンク相手が固定中か、片側が最小尺未満です"` を返す。 |
|
|
20
|
+
| `liftRange` | cut トラックの範囲内を取り除き、隙間を残す。 |
|
|
21
|
+
| `extractRange` | cut トラックの範囲内を取り除き、後続を範囲長だけ前へ送る。shift トラックは中身を取り除かず、範囲終端以降に始まるアイテムだけを前へ送る。 |
|
|
22
|
+
| `rippleDeleteItems` | 選択アイテムの区間を詰める。他トラックは中身を消さずに後続を送る。リンク相手も既定で対象とし、`oneSide` で片側だけにできる。離れた選択は右から処理する。 |
|
|
23
|
+
| `findGapAt` / `closeGapAt` | 指定トラックでフレームを含む先頭またはアイテム間の隙間を見つけ、その長さだけ後続を送る。末尾の空きは対象外。 |
|
|
24
|
+
| `rippleTrimToPlayhead` | 最も近い前後の編集点から再生ヘッドまでを `extractRange` する。編集点が無ければ理由を返す。 |
|
|
25
|
+
| `editPoints` | cut トラックのアイテム端を昇順・重複なしで返す。 |
|
|
26
|
+
| `compactTrackGaps` | cut の visual トラックにある media だけを配列順に詰める。`fromItemId` があればそのアイテムの後続だけを詰める。既定では anchor 付きアイテムを直接動かさない。 |
|
|
27
|
+
| `applyCutRanges` | 素材秒で重なる media を選び、共通の区間除去部品で切除する。触った visual トラックの media だけを `at = 0` から配列順に詰める旧挙動を保つ。 |
|
|
28
|
+
|
|
29
|
+
shift は前のアイテムの終端より前に入らない。必要な移動量を確保できなかったアイテムの id を `blocked` に返し、重なりを作らない。`anchor` を持つアイテムは shift で直接動かさない。cut トラックなら範囲内の削除対象にはなる。位置の再解決は `refreshItemAnchors` の責務とする。`applyCutRanges` は旧挙動を維持するため、`compactTrackGaps` に `includeAnchored: true` を渡して触ったトラックの media を詰める。
|
|
30
|
+
|
|
31
|
+
分割片の `source.in/out` は、素材秒をアイテムのフレーム比率(speed 込み)で按分する。音声の `keyframes[].gain_db` と映像の数値キーフレームは境界点を両片に入れ、ローカルフレーム時刻へ振り分ける。境界値は区間の終点側キーの `easing` を `easingProgress` で評価する。文字列指定は全プロパティ、Record 指定はプロパティ別(例: `gain_db`、`opacity`、`transform.x`)に適用し、左片の新しい境界点にも終点側の easing を引き継ぐ。線形区間の全フレーム値は分割前と一致する。非線形区間は境界値を一致させるが、分割後の区間内部の曲線は近似になる。`fade_in` は左片、`fade_out` は右片だけに残し、ダッキングなど他の属性は両片へ複製する。リンク音声の右片は映像の右片へ `link` を付け替える。
|
|
32
|
+
|
|
33
|
+
字幕本文は `captions.json` のソース秒が正本であり、ここでは変更しない。切除後の表示位置は `timeline-map` の `sourceToOutput` で追従する。通常の Delete は隙間を残す。詰める処理は明示した操作でだけ行う。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.1",
|
|
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": [
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
3
|
+
import { recordProjectReference } from './project-references.mjs';
|
|
4
|
+
import { appendLibraryUsage } from './library-usage.mjs';
|
|
5
|
+
import { ASSET_CATEGORIES } from './library.mjs';
|
|
6
|
+
import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
7
|
+
|
|
8
|
+
const within = (root, target) => {
|
|
9
|
+
const rel = relative(root, target);
|
|
10
|
+
return rel === '' || (rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function placeLibraryAsset(source, projectPath, env = process.env) {
|
|
14
|
+
if (!source || !ASSET_CATEGORIES.includes(source.category)
|
|
15
|
+
|| typeof source.id !== 'string' || !source.id || source.id === '.'
|
|
16
|
+
|| source.id.includes('..') || source.id.includes('/') || source.id.includes(String.fromCharCode(92))
|
|
17
|
+
|| typeof source.libraryDir !== 'string' || !isAbsolute(source.libraryDir)) {
|
|
18
|
+
throw new Error('素材の種類・名前・置き場が不正です');
|
|
19
|
+
}
|
|
20
|
+
const actual = await realpath(source.libraryDir);
|
|
21
|
+
if (basename(actual) !== source.id || basename(dirname(actual)) !== source.category
|
|
22
|
+
|| !(await stat(actual)).isDirectory()) {
|
|
23
|
+
throw new Error('素材の置き場と種類・名前が一致しません');
|
|
24
|
+
}
|
|
25
|
+
let allowed = false;
|
|
26
|
+
for (const root of resolveAssetLibraryRoots(env).read) {
|
|
27
|
+
try { if (within(await realpath(root), actual)) allowed = true; }
|
|
28
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
29
|
+
}
|
|
30
|
+
if (!allowed) throw new Error('素材がライブラリの置き場の外にあります');
|
|
31
|
+
const project = await realpath(projectPath);
|
|
32
|
+
if (!(await stat(project)).isDirectory()) throw new Error('プロジェクトがフォルダではありません');
|
|
33
|
+
const destination = join(project, 'assets', source.category, source.id);
|
|
34
|
+
let parent = dirname(destination);
|
|
35
|
+
let actualDestination;
|
|
36
|
+
while (true) {
|
|
37
|
+
try {
|
|
38
|
+
const actualParent = await realpath(parent);
|
|
39
|
+
if (!within(project, actualParent)) throw new Error('配置先がプロジェクトの外にあります');
|
|
40
|
+
actualDestination = join(actualParent, relative(parent, destination));
|
|
41
|
+
break;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code !== 'ENOENT') throw error;
|
|
44
|
+
parent = dirname(parent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (within(actualDestination, actual) || within(actual, actualDestination)) {
|
|
48
|
+
throw new Error('素材の大元と重なる配置はできません');
|
|
49
|
+
}
|
|
50
|
+
await recordProjectReference(project, { category: source.category, id: source.id });
|
|
51
|
+
await appendLibraryUsage({ category: source.category, id: source.id, project });
|
|
52
|
+
return { success: true, projectAssetPath: join(projectPath, 'assets', source.category, source.id),
|
|
53
|
+
reference: true, libraryDir: actual };
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { listProjectReferenceAssets } from './shell-reference.mjs';
|
|
3
|
+
import { sourceFields } from './library.mjs';
|
|
4
|
+
import { readCatalogCache } from './catalog.mjs';
|
|
5
|
+
import { resolveCatalogSource } from './env.mjs';
|
|
6
|
+
|
|
7
|
+
export async function listProjectAssetReferences(project, env = process.env) {
|
|
8
|
+
let catalog = await readCatalogCache(env);
|
|
9
|
+
const source = resolveCatalogSource(env);
|
|
10
|
+
if (source.kind === 'file') { try { catalog = JSON.parse(await readFile(source.value, 'utf8')); } catch {} }
|
|
11
|
+
const entries = await listProjectReferenceAssets(project, env);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
let meta;
|
|
14
|
+
try { meta = JSON.parse(await readFile(entry.files.find(file => file.name === 'meta.json').path, 'utf8')); } catch {}
|
|
15
|
+
entry.tags = Array.isArray(meta?.tags) ? meta.tags.filter(tag => typeof tag === 'string') : [];
|
|
16
|
+
const known = catalog?.items?.find(item => item.id === entry.id && item.category === entry.category);
|
|
17
|
+
entry.title = typeof meta?.title === 'string' ? meta.title : typeof known?.title === 'string' ? known.title : entry.id;
|
|
18
|
+
if (!meta && known) entry.tags = Array.isArray(known.tags) ? known.tags.filter(tag => typeof tag === 'string') : [];
|
|
19
|
+
if (meta || known) entry.sourceKind = sourceFields(meta ?? known, !!known).sourceKind;
|
|
20
|
+
}
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { lstatSync, realpathSync } from 'node:fs';
|
|
3
|
-
import fsPromises, { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import fsPromises, { lstat, realpath, mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -219,3 +219,26 @@ export function resolveLibraryFallback({ declaredPath, references, akariAssetsDi
|
|
|
219
219
|
return null;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
export async function resolveLibraryFallbackAsync({ declaredPath, references, akariAssetsDir, rootRealpaths }) {
|
|
224
|
+
const parsed = parseDeclaredAssetPath(declaredPath);
|
|
225
|
+
if (!parsed || typeof akariAssetsDir !== 'string' || akariAssetsDir.length === 0) return null;
|
|
226
|
+
if (!normalizeReferences(references).some(
|
|
227
|
+
entry => entry.category === parsed.category && entry.id === parsed.id,
|
|
228
|
+
)) return null;
|
|
229
|
+
const lexicalRoot = path.resolve(akariAssetsDir);
|
|
230
|
+
const lexicalTarget = path.resolve(lexicalRoot, parsed.category, parsed.id, ...parsed.rest);
|
|
231
|
+
if (!isWithin(lexicalRoot, lexicalTarget)) return null;
|
|
232
|
+
try {
|
|
233
|
+
let actualRoot = rootRealpaths?.get(lexicalRoot);
|
|
234
|
+
if (!actualRoot) {
|
|
235
|
+
actualRoot = realpath(lexicalRoot);
|
|
236
|
+
rootRealpaths?.set(lexicalRoot, actualRoot);
|
|
237
|
+
}
|
|
238
|
+
const [root, target] = await Promise.all([actualRoot, realpath(lexicalTarget)]);
|
|
239
|
+
if (!isWithin(root, target) || !(await lstat(target)).isFile()) return null;
|
|
240
|
+
return target;
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readdir, stat, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
4
|
+
import { assetTier } from './tier.mjs';
|
|
5
|
+
import { loadCatalog, readCatalogCache } from './catalog.mjs';
|
|
6
|
+
import { mergeInstalledItems, INSTALLED_ASSETS_SCHEMA } from './installed.mjs';
|
|
7
|
+
import { resolveCatalogSource } from './env.mjs';
|
|
8
|
+
|
|
9
|
+
export async function cachedAssetDirAsync(env, category, id) {
|
|
10
|
+
for (const root of resolveAssetLibraryRoots(env).read) {
|
|
11
|
+
const dir = path.resolve(root, category, id);
|
|
12
|
+
try {
|
|
13
|
+
if ((await stat(dir)).isDirectory() && (await readdir(dir)).length) return dir;
|
|
14
|
+
} catch { /* next root */ }
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const safe = value => typeof value === 'string' && value.length > 0
|
|
20
|
+
&& value !== '.' && value !== '..' && !/[\\/]/.test(value);
|
|
21
|
+
|
|
22
|
+
function within(root, ...parts) {
|
|
23
|
+
const base = path.resolve(root);
|
|
24
|
+
const candidate = path.resolve(base, ...parts);
|
|
25
|
+
if (candidate !== base && !candidate.startsWith(`${base}${path.sep}`)) {
|
|
26
|
+
throw new Error(`導入済み素材のパスがパック外を指しています: ${parts.join('/')}`);
|
|
27
|
+
}
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function installedItemsAsync(env) {
|
|
32
|
+
const roots = resolveAssetLibraryRoots(env).read;
|
|
33
|
+
const byId = new Map();
|
|
34
|
+
for (const libraryRoot of [...roots].reverse()) {
|
|
35
|
+
const indexPath = path.join(libraryRoot, 'installed.json');
|
|
36
|
+
let index;
|
|
37
|
+
try { index = JSON.parse(await readFile(indexPath, 'utf8')); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error?.code === 'ENOENT') continue;
|
|
40
|
+
throw new Error(`導入済み素材索引を読めません: ${indexPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
if (index?.schema !== INSTALLED_ASSETS_SCHEMA
|
|
43
|
+
|| !index.packs || typeof index.packs !== 'object' || Array.isArray(index.packs)) {
|
|
44
|
+
throw new Error(`導入済み素材索引の形式が想定と違います: ${indexPath}`);
|
|
45
|
+
}
|
|
46
|
+
for (const [packId, pack] of Object.entries(index.packs)) {
|
|
47
|
+
if (!safe(packId) || !pack || typeof pack.root !== 'string' || !path.isAbsolute(pack.root)
|
|
48
|
+
|| (typeof pack.version !== 'string' && typeof pack.version !== 'number')
|
|
49
|
+
|| typeof pack.installedAt !== 'string' || !pack.installedAt || !Array.isArray(pack.items)) {
|
|
50
|
+
throw new Error(`導入済み素材索引に不正な pack があります: ${packId}`);
|
|
51
|
+
}
|
|
52
|
+
const originalRoot = roots.find(root => {
|
|
53
|
+
try { within(path.join(root, 'store', packId), pack.root); return true; } catch { return false; }
|
|
54
|
+
});
|
|
55
|
+
if (!originalRoot) throw new Error(`導入済み素材の root がパック外を指しています: ${pack.root}`);
|
|
56
|
+
const relativeRoot = path.relative(originalRoot, pack.root);
|
|
57
|
+
const readRoots = roots.map(root => path.join(root, relativeRoot));
|
|
58
|
+
for (const item of pack.items) {
|
|
59
|
+
if (!item || !safe(item.id) || typeof item.title !== 'string' || !item.title
|
|
60
|
+
|| typeof item.path !== 'string' || !item.path || item.version == null) {
|
|
61
|
+
throw new Error(`導入済み素材索引に不正な item があります: ${packId}`);
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(item.files) || item.files.length === 0) {
|
|
64
|
+
throw new Error(`導入済み素材索引の item に files[] がありません: ${item.id}`);
|
|
65
|
+
}
|
|
66
|
+
const itemRoots = readRoots.map(root => within(root, item.path));
|
|
67
|
+
let metaTier;
|
|
68
|
+
for (const root of itemRoots) {
|
|
69
|
+
try {
|
|
70
|
+
const meta = JSON.parse(await readFile(path.join(root, 'meta.json'), 'utf8'));
|
|
71
|
+
if (Object.hasOwn(meta, 'tier') || Object.hasOwn(meta, 'price')) metaTier = assetTier(meta);
|
|
72
|
+
break;
|
|
73
|
+
} catch (error) { if (error?.code !== 'ENOENT') break; }
|
|
74
|
+
}
|
|
75
|
+
const categoryParts = item.path.replaceAll('\\', '/').split('/').filter(Boolean);
|
|
76
|
+
const category = categoryParts[0] === 'assets' && safe(categoryParts[1]) ? categoryParts[1] : 'pack';
|
|
77
|
+
const files = await Promise.all(item.files.map(async file => {
|
|
78
|
+
if (!file || typeof file.path !== 'string' || !file.path
|
|
79
|
+
|| !Number.isInteger(file.bytes) || file.bytes < 0
|
|
80
|
+
|| typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)) {
|
|
81
|
+
throw new Error(`導入済み素材索引の files[] が不正です: ${item.id}`);
|
|
82
|
+
}
|
|
83
|
+
const candidates = itemRoots.map(root => within(root, file.path));
|
|
84
|
+
let localPath = candidates[0];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
if (await stat(candidate).then(() => true, () => false)) { localPath = candidate; break; }
|
|
87
|
+
}
|
|
88
|
+
return { name: file.path, local_path: localPath, sha256: file.sha256, bytes: file.bytes };
|
|
89
|
+
}));
|
|
90
|
+
byId.set(item.id, { id: item.id, title: item.title, category, version: item.version,
|
|
91
|
+
price: 0, ...((Object.hasOwn(item, 'tier') ? assetTier(item) : metaTier)
|
|
92
|
+
? { tier: Object.hasOwn(item, 'tier') ? assetTier(item) : metaTier } : {}),
|
|
93
|
+
source: 'installed', files });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...byId.values()];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function loadCatalogForResolveAsync(ref, { env = process.env, fetchImpl = fetch, timeouts } = {}) {
|
|
101
|
+
const matches = typeof ref === 'string' && ref.includes('/')
|
|
102
|
+
? item => `${item.category}/${item.id}` === ref : item => item.id === ref;
|
|
103
|
+
const installedItems = await installedItemsAsync(env);
|
|
104
|
+
if (installedItems.some(matches)) {
|
|
105
|
+
return { schema: 'akari-assets-catalog/v0', version: null, base: null, items: installedItems };
|
|
106
|
+
}
|
|
107
|
+
if (resolveCatalogSource(env).kind === 'url') {
|
|
108
|
+
const cached = await readCatalogCache(env);
|
|
109
|
+
if (cached && cached.items.some(matches)) return mergeInstalledItems(cached, installedItems);
|
|
110
|
+
}
|
|
111
|
+
const catalog = await loadCatalog({ env, fetchImpl, timeouts, fallbackToCache: false, includeInstalled: false });
|
|
112
|
+
return mergeInstalledItems(catalog, installedItems);
|
|
113
|
+
}
|
|
@@ -11,19 +11,18 @@ import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
|
11
11
|
// なら resolvePaidZip() が `/api/store/v1/download/<id>` から zip を取得し、展開 →
|
|
12
12
|
// checksums.txt 検証(paid-zip.mjs)→ 同じ validate-asset / 原子的 move の経路に合流する。
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import { constants
|
|
16
|
-
import { cp, mkdir, mkdtemp, rename, rm } from 'node:fs/promises';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { constants } from 'node:fs';
|
|
16
|
+
import { cp, mkdir, mkdtemp, readFile, rename, rm, realpath, stat, writeFile } from 'node:fs/promises';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
import { format } from 'node:util';
|
|
20
|
-
import {
|
|
20
|
+
import { loadCatalogForResolveAsync, cachedAssetDirAsync } from './resolve-async-library.mjs';
|
|
21
21
|
import { resolveEffectiveBase } from './env.mjs';
|
|
22
22
|
import { AssetResolverError } from './errors.mjs';
|
|
23
23
|
import { fetchEntitlements, readStoreCredentials } from './entitlements.mjs';
|
|
24
24
|
import { materialize, resolveFileLocation } from './fetch-file.mjs';
|
|
25
25
|
import { sha256File } from './hash.mjs';
|
|
26
|
-
import { cachedAssetDir, localAssetDir } from './library.mjs';
|
|
27
26
|
import { downloadPaidZip, extractZip, verifyPaidZipContents } from './paid-zip.mjs';
|
|
28
27
|
import { recordProjectReference } from './project-references.mjs';
|
|
29
28
|
import { assetTier, isAssetEntitled } from './tier.mjs';
|
|
@@ -81,7 +80,15 @@ async function validateAsset(assetDir) {
|
|
|
81
80
|
return await run;
|
|
82
81
|
} catch {
|
|
83
82
|
// Module loading failures are outside the validator's ordinary NG result.
|
|
84
|
-
return
|
|
83
|
+
return new Promise((resolveResult, reject) => {
|
|
84
|
+
const child = spawn(process.execPath, [VALIDATE_ASSET_SCRIPT, assetDir]);
|
|
85
|
+
let stdout = '';
|
|
86
|
+
let stderr = '';
|
|
87
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
88
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
89
|
+
child.on('error', reject);
|
|
90
|
+
child.on('close', status => resolveResult({ status, stdout, stderr }));
|
|
91
|
+
});
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -110,7 +117,7 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
|
|
|
110
117
|
// 素材箱側が「meta.json を含むディレクトリ = 1 カード」でグルーピングする際、
|
|
111
118
|
// 深さではなくディレクトリ形で判定するため、置き場の形をライブラリと合わせておく必要はないが、
|
|
112
119
|
// カテゴリ別に整理された配置の方が人間が見ても分かりやすいのでライブラリ型に統一する。
|
|
113
|
-
const realSourceDir =
|
|
120
|
+
const realSourceDir = await realpath(sourceDir);
|
|
114
121
|
const dest = path.join(path.resolve(projectDir), 'assets', category, id);
|
|
115
122
|
await mkdir(path.dirname(dest), { recursive: true });
|
|
116
123
|
await rm(dest, { recursive: true, force: true });
|
|
@@ -119,8 +126,12 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
|
|
|
119
126
|
// このマシンの Node(libuv)は clonefileat 相当が ENOSYS を返し、fs.cp の
|
|
120
127
|
// COPYFILE_FICLONE では節約が効かない(前段 2026-08-09-project-copy-cow-clone で実測確認済み)。
|
|
121
128
|
// BSD cp -c は clonefile(2) を Node を介さず直接使うため、同じ OS/FS 上で実際にクローンできる。
|
|
122
|
-
const
|
|
123
|
-
|
|
129
|
+
const cloned = await new Promise(resolveResult => {
|
|
130
|
+
const child = spawn('/bin/cp', ['-Rc', realSourceDir, dest], { stdio: 'ignore' });
|
|
131
|
+
child.on('error', () => resolveResult(false));
|
|
132
|
+
child.on('close', code => resolveResult(code === 0));
|
|
133
|
+
});
|
|
134
|
+
if (cloned) {
|
|
124
135
|
return dest;
|
|
125
136
|
}
|
|
126
137
|
// クロスボリューム等で -c が失敗したケース。部分的に書かれた dest を掃除してから
|
|
@@ -136,7 +147,7 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
|
|
|
136
147
|
|
|
137
148
|
async function moveIntoLibrary(tempDir, destDir) {
|
|
138
149
|
await mkdir(path.dirname(destDir), { recursive: true });
|
|
139
|
-
if (
|
|
150
|
+
if (await stat(destDir).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; })) {
|
|
140
151
|
await rm(destDir, { recursive: true, force: true });
|
|
141
152
|
}
|
|
142
153
|
try {
|
|
@@ -152,6 +163,19 @@ async function moveIntoLibrary(tempDir, destDir) {
|
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
async function backfillLegacyMetaTier(metaPath, item) {
|
|
167
|
+
let meta;
|
|
168
|
+
try {
|
|
169
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
170
|
+
} catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta) || Object.hasOwn(meta, 'tier')) return;
|
|
174
|
+
// R2 に公開済みの旧 meta.json(tier 導入前・price のみ)を、sha256 検証済みのうえでカタログの tier で補う互換経路。R2 の meta.json を tier 付きで上げ直したら外せる。
|
|
175
|
+
meta.tier = assetTier(item);
|
|
176
|
+
await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`);
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
/**
|
|
156
180
|
* @param {string} ref カタログの category/id。bare id は一意な場合のみ互換解決する
|
|
157
181
|
* @param {{ env?: object, fetchImpl?: Function, project?: string|null, force?: boolean, reference?: boolean }} options
|
|
@@ -161,16 +185,16 @@ export async function resolve(
|
|
|
161
185
|
{ env = process.env, fetchImpl = fetch, project = null, force = false, reference = false, timeouts } = {},
|
|
162
186
|
) {
|
|
163
187
|
const home = resolveAssetLibraryRoots(env).write;
|
|
164
|
-
const catalog = await
|
|
188
|
+
const catalog = await loadCatalogForResolveAsync(ref, { env, fetchImpl, timeouts });
|
|
165
189
|
const item = findCatalogItem(catalog, ref);
|
|
166
190
|
|
|
167
191
|
const tier = assetTier(item);
|
|
168
192
|
const hasFiles = Array.isArray(item.files) && item.files.length > 0;
|
|
169
|
-
const destDir =
|
|
193
|
+
const destDir = path.join(home, item.category, item.id);
|
|
170
194
|
|
|
171
195
|
// キャッシュヒット → 即返す(未購入だったとしても、一度取得済みなら手元にある実体をそのまま使う。
|
|
172
196
|
// ゲートは「新規に取得するとき」だけにかける)
|
|
173
|
-
const cachedDir =
|
|
197
|
+
const cachedDir = await cachedAssetDirAsync(env, item.category, item.id);
|
|
174
198
|
if (!force && cachedDir) {
|
|
175
199
|
const result = { id: item.id, category: item.category, dir: cachedDir, cached: true };
|
|
176
200
|
if (project && reference) {
|
|
@@ -244,6 +268,7 @@ export async function resolve(
|
|
|
244
268
|
|
|
245
269
|
// still / scene3d 等、meta.json を実体に持つ素材は validate-asset で契約検証してから登録する
|
|
246
270
|
if (hasMeta) {
|
|
271
|
+
await backfillLegacyMetaTier(path.join(tempAssetDir, 'meta.json'), item);
|
|
247
272
|
const result = await validateAsset(tempAssetDir);
|
|
248
273
|
if (result.status !== 0) {
|
|
249
274
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
@@ -327,6 +352,8 @@ async function resolvePaidZip(item, { env, fetchImpl, project, reference, home,
|
|
|
327
352
|
});
|
|
328
353
|
}
|
|
329
354
|
|
|
355
|
+
await backfillLegacyMetaTier(path.join(tempAssetDir, 'meta.json'), item);
|
|
356
|
+
|
|
330
357
|
{
|
|
331
358
|
const result = await validateAsset(tempAssetDir);
|
|
332
359
|
if (result.status !== 0) {
|