akari-video 0.1.32 → 0.1.34
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 +3 -1
- package/bin/akari.mjs +3 -1
- package/package.json +1 -1
- package/src/clean-command.mjs +191 -0
- package/src/clean-manifest.mjs +435 -0
- package/src/messages.mjs +1 -0
- package/vendor/.akari-capability-sources.json +11 -0
- package/vendor/docs/contract-2026-07-14-edit-json-v1-audio.md +4 -0
- package/vendor/docs/contract-2026-07-20-edit-json-v1-narration.md +3 -0
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +41 -0
- package/vendor/docs/contract-2026-08-02-creator-root-v1.md +2 -1
- package/vendor/docs/contract-2026-08-02-preview-parity.md +10 -1
- package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +21 -1
- package/vendor/docs/contract-2026-08-28-osr-export-v0.md +93 -5
- package/vendor/docs/contract-2026-08-28-v2-audio-roles-v0.md +16 -11
- package/vendor/docs/contract-2026-08-29-media-inspect-cli-v0.md +3 -1
- package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +4 -0
- package/vendor/docs/contract-2026-09-02-audio-clip-fx-v1.md +56 -0
- package/vendor/docs/contract-2026-09-02-audio-envelope-v1.md +91 -0
- package/vendor/docs/contract-2026-09-02-audio-insert-level-v1.md +110 -0
- package/vendor/docs/contract-2026-09-02-captions-style-preset-v0.md +84 -0
- package/vendor/docs/contract-2026-09-02-export-verify-declared-vs-measured-v0.md +105 -0
- package/vendor/docs/contract-2026-09-02-item-caption-anchor-v0.md +106 -0
- package/vendor/docs/contract-2026-09-02-transcript-unrecognized-spans-v0.md +78 -0
- package/vendor/docs/contract-2026-09-02-word-book-v0.md +402 -0
- package/vendor/packages/akari-launcher/README.md +3 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/README.md +21 -0
- package/vendor/packages/akari-tools/package.json +3 -2
- package/vendor/packages/audio-library-setup/README.md +11 -0
- package/vendor/packages/audio-library-setup/shared/insert-level.mjs +105 -0
- package/vendor/packages/audio-library-setup/test/insert-level.test.mjs +110 -0
- package/vendor/packages/edit-lint/README.md +30 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +641 -4
- package/vendor/packages/edit-store/lib/audio-schedule.d.ts +17 -3
- package/vendor/packages/edit-store/lib/audio-schedule.js +98 -33
- package/vendor/packages/edit-store/lib/caption-clock.d.ts +56 -0
- package/vendor/packages/edit-store/lib/caption-clock.js +80 -0
- package/vendor/packages/edit-store/lib/caption-store.d.ts +8 -0
- package/vendor/packages/edit-store/lib/caption-store.js +43 -3
- package/vendor/packages/edit-store/lib/caption-style-preset.d.ts +16 -0
- package/vendor/packages/edit-store/lib/caption-style-preset.js +87 -0
- package/vendor/packages/edit-store/lib/cut-ranges.d.ts +17 -0
- package/vendor/packages/edit-store/lib/cut-ranges.js +206 -0
- package/vendor/packages/edit-store/lib/ducking.d.ts +1 -6
- package/vendor/packages/edit-store/lib/ducking.js +3 -12
- package/vendor/packages/edit-store/lib/edit-store.d.ts +38 -0
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +24 -1
- package/vendor/packages/edit-store/lib/edit-v2.js +52 -5
- package/vendor/packages/edit-store/lib/envelope.d.ts +34 -0
- package/vendor/packages/edit-store/lib/envelope.js +289 -0
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +10 -10
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +18 -4
- package/vendor/packages/edit-store/lib/generated/textstyle-catalog.d.ts +2 -0
- package/vendor/packages/edit-store/lib/generated/textstyle-catalog.js +251 -0
- package/vendor/packages/edit-store/lib/index.d.ts +7 -0
- package/vendor/packages/edit-store/lib/index.js +7 -0
- package/vendor/packages/edit-store/lib/internal-model.d.ts +6 -0
- package/vendor/packages/edit-store/lib/internal-model.js +76 -8
- package/vendor/packages/edit-store/lib/item-anchor.d.ts +55 -0
- package/vendor/packages/edit-store/lib/item-anchor.js +179 -0
- package/vendor/packages/edit-store/lib/legacy-audio-view.js +16 -4
- package/vendor/packages/edit-store/lib/migrate/index.js +96 -12
- package/vendor/packages/edit-store/lib/timeline-map.d.ts +21 -0
- package/vendor/packages/edit-store/lib/timeline-map.js +66 -0
- package/vendor/packages/edit-store/lib/track-z.d.ts +41 -0
- package/vendor/packages/edit-store/lib/track-z.js +65 -0
- package/vendor/packages/edit-store/lib/tree-ops.d.ts +13 -0
- package/vendor/packages/edit-store/lib/tree-ops.js +23 -0
- package/vendor/packages/edit-store/lib/webview-kernel.d.ts +5 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +2472 -49
- package/vendor/packages/edit-store/package.json +2 -1
- package/vendor/packages/gpu-export/README.ja.md +89 -0
- package/vendor/packages/gpu-export/README.md +101 -0
- package/vendor/packages/media-bin/README.ja.md +19 -0
- package/vendor/packages/media-bin/README.md +19 -0
- package/vendor/packages/media-bin/bin/audio-measure.mjs +64 -0
- package/vendor/packages/media-bin/package.json +3 -0
- package/vendor/packages/media-bin/src/audio-measure.mjs +119 -0
- package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +436 -123
- package/vendor/packages/media-bin/test/audio-clip-fx.test.mjs +103 -0
- package/vendor/packages/media-bin/test/audio-measure.test.mjs +123 -0
- package/vendor/packages/media-bin/test/fixtures/audio-measure/click-300ms.stderr +15 -0
- package/vendor/packages/media-bin/test/fixtures/audio-measure/silence-3s.stderr +12 -0
- package/vendor/packages/media-bin/test/fixtures/audio-measure/tone-1k-minus20db.stderr +15 -0
- package/vendor/packages/media-bin/test/preview-audio-sidecar-async.test.mjs +257 -0
- package/vendor/packages/osr-export/README.md +5 -1
- package/vendor/packages/overlay-runtime/README.md +29 -2
- package/vendor/packages/schemas/analysis.schema.json +16 -0
- package/vendor/packages/schemas/bin/validate-captions.mjs +42 -0
- package/vendor/packages/schemas/bin/validate-edit.mjs +85 -0
- package/vendor/packages/schemas/bin/validate-word-book.mjs +165 -0
- package/vendor/packages/schemas/captions.schema.json +18 -0
- package/vendor/packages/schemas/edit.schema.json +107 -4
- package/vendor/packages/schemas/engine-capabilities.json +111 -0
- package/vendor/packages/schemas/examples/edit-audio-clip-fx-invalid/edit.json +17 -0
- package/vendor/packages/schemas/examples/edit-audio-clip-fx-valid/edit.json +47 -0
- package/vendor/packages/schemas/examples/edit-audio-keyframes-t-order-invalid/edit.json +16 -0
- package/vendor/packages/schemas/examples/edit-audio-keyframes-valid/edit.json +19 -0
- package/vendor/packages/schemas/examples/edit-duck-db-out-of-range/edit.json +14 -0
- package/vendor/packages/schemas/examples/edit-sfx-ducking-valid/edit.json +19 -0
- package/vendor/packages/schemas/examples/word-book-v0-invalid-reading-only-variants/word-book.json +6 -0
- package/vendor/packages/schemas/examples/word-book-v0-invalid-variant-conflict/word-book.json +7 -0
- package/vendor/packages/schemas/examples/word-book-v0-invalid-version-1/word-book.json +4 -0
- package/vendor/packages/schemas/examples/word-book-v0-valid/word-book.json +27 -0
- package/vendor/packages/schemas/test/analysis-observations.test.mjs +30 -0
- package/vendor/packages/schemas/test/audio-clip-fx.test.mjs +39 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +37 -0
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +71 -0
- package/vendor/packages/schemas/test/validate-captions.test.mjs +46 -0
- package/vendor/packages/schemas/test/validate-edit.test.mjs +22 -0
- package/vendor/packages/schemas/test/validate-word-book.test.mjs +83 -0
- package/vendor/packages/schemas/word-book.schema.json +33 -0
- package/vendor/packages/word-book/package.json +11 -0
- package/vendor/skills/edit-lint/SKILL.md +8 -2
- package/vendor/skills/edit-plan/SKILL.md +1 -0
- package/vendor/skills/edit-plan/workflow.md +1 -0
- package/vendor/skills/render-cut/SKILL.md +1 -0
- package/vendor/skills/verify/SKILL.md +2 -1
- package/vendor/templates/project-default/.akari/work/keep/.gitkeep +0 -0
- package/vendor/templates/project-default/.akari/work/tmp/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -50,7 +50,9 @@ DL・sha256 検証・適用まで実行。それ以外(npm グローバル / g
|
|
|
50
50
|
配布物の取得ができる。`src/store-command.mjs`)/
|
|
51
51
|
`akari assets <list|fetch|sync|...>`(素材カタログの一覧・取得・同期。
|
|
52
52
|
`packages/asset-resolver` の CLI への薄い委譲で、カタログ合成・entitlements 判定・
|
|
53
|
-
sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-command.mjs
|
|
53
|
+
sha256 検証・fail-closed は resolver 側の責務のまま。`src/assets-command.mjs`)/
|
|
54
|
+
`akari clean [project-dir] [--dry-run] [--yes] [--json]`(使い捨ての中間ファイル、保持する
|
|
55
|
+
正本、判断が必要なものを容量付きで一覧する。既定は一覧のみで、削除可能なものだけを承認後に削除)。
|
|
54
56
|
|
|
55
57
|
`akari` に渡した引数はそのまま `opencode` に転送する(例: `akari --continue` は
|
|
56
58
|
`opencode --continue` を起動する)。
|
package/bin/akari.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { runCapabilityCommand } from '../src/capability-command.mjs';
|
|
|
11
11
|
import { runStoreCommand } from '../src/store-command.mjs';
|
|
12
12
|
import { runAssetsCommand } from '../src/assets-command.mjs';
|
|
13
13
|
import { runMigrateCommand } from '../src/migrate-command.mjs';
|
|
14
|
+
import { runCleanCommand } from '../src/clean-command.mjs';
|
|
14
15
|
import { runDoctorCommand } from '../src/doctor-command.mjs';
|
|
15
16
|
import { resolveRuntimePaths } from '../src/runtime-diagnostics.mjs';
|
|
16
17
|
import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
|
|
@@ -59,7 +60,7 @@ try {
|
|
|
59
60
|
|
|
60
61
|
// `akari update` / `akari init` / `akari new` / `akari narration` / `akari internal` /
|
|
61
62
|
// `akari sounds` / `akari status` / `akari accept` / `akari capability` / `akari store` /
|
|
62
|
-
// `akari assets` は claude へ転送せず、専用のサブコマンドとして扱う(契約 §4-1 /
|
|
63
|
+
// `akari assets` / `akari clean` は claude へ転送せず、専用のサブコマンドとして扱う(契約 §4-1 /
|
|
63
64
|
// タスク契約 launcher-init(内部リポ)/ 音源カタログ既定化のオーナー裁定 2026-08-03 /
|
|
64
65
|
// AKARI Store 連携 / タスク契約 2026-08-09-agent-assets-discovery)。
|
|
65
66
|
// それ以外の引数はすべて従来どおり claude へ転送する。
|
|
@@ -78,6 +79,7 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
|
|
|
78
79
|
: argv[0] === 'store' ? runStoreCommand(argv.slice(1))
|
|
79
80
|
: argv[0] === 'assets' ? runAssetsCommand(argv.slice(1))
|
|
80
81
|
: argv[0] === 'migrate' ? runMigrateCommand(argv.slice(1))
|
|
82
|
+
: argv[0] === 'clean' ? runCleanCommand(argv.slice(1))
|
|
81
83
|
: run(argv);
|
|
82
84
|
|
|
83
85
|
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.34",
|
|
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,191 @@
|
|
|
1
|
+
import { lstat, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
|
|
5
|
+
import { classifyProject } from './clean-manifest.mjs';
|
|
6
|
+
|
|
7
|
+
export async function runCleanCommand(args, options = {}) {
|
|
8
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
9
|
+
const error = options.error ?? ((line) => console.error(line));
|
|
10
|
+
const cwd = options.cwd ?? process.cwd();
|
|
11
|
+
const parsed = parseArguments(args, cwd);
|
|
12
|
+
if (!parsed.ok) {
|
|
13
|
+
error(parsed.message);
|
|
14
|
+
for (const line of cleanHelp()) error(line);
|
|
15
|
+
return { exitCode: 2 };
|
|
16
|
+
}
|
|
17
|
+
if (parsed.help) {
|
|
18
|
+
for (const line of cleanHelp()) log(line);
|
|
19
|
+
return { exitCode: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statPath = options.lstat ?? lstat;
|
|
23
|
+
try {
|
|
24
|
+
const editStat = await statPath(path.join(parsed.projectRoot, 'edit.json'));
|
|
25
|
+
if (!editStat.isFile()) throw Object.assign(new Error('not a file'), { code: 'ENOENT' });
|
|
26
|
+
} catch {
|
|
27
|
+
error(`edit.json が見つかりません: ${parsed.projectRoot}`);
|
|
28
|
+
return { exitCode: 2 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let classification;
|
|
32
|
+
try {
|
|
33
|
+
classification = await (options.classify ?? classifyProject)(parsed.projectRoot, {
|
|
34
|
+
now: options.now,
|
|
35
|
+
lstat: options.lstat,
|
|
36
|
+
readdir: options.readdir,
|
|
37
|
+
readFile: options.readFile,
|
|
38
|
+
});
|
|
39
|
+
} catch (cause) {
|
|
40
|
+
error(`プロジェクトを調べられません: ${messageOf(cause)}`);
|
|
41
|
+
return { exitCode: 2 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (parsed.json) {
|
|
45
|
+
log(JSON.stringify(classification));
|
|
46
|
+
} else {
|
|
47
|
+
for (const line of formatClassification(classification)) log(line);
|
|
48
|
+
for (const warning of provenanceWarnings(classification)) log(warning);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parsed.dryRun || classification.disposable.length === 0) {
|
|
52
|
+
if (!parsed.json && classification.disposable.length === 0) log('削除対象はありません。');
|
|
53
|
+
return { exitCode: 0, classification, removed: [], failures: [] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!parsed.yes) {
|
|
57
|
+
const isTTY = options.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
58
|
+
if (!isTTY) {
|
|
59
|
+
error('非 TTY では明示承認が必要です。一覧を確認し、--yes を付けて再実行してください。');
|
|
60
|
+
return { exitCode: 2, classification, removed: [], failures: [] };
|
|
61
|
+
}
|
|
62
|
+
const answer = options.prompt ? await options.prompt() : await promptForConfirmation();
|
|
63
|
+
const accepted = typeof answer === 'boolean' ? answer : /^(?:y|yes)$/iu.test(String(answer).trim());
|
|
64
|
+
if (!accepted) {
|
|
65
|
+
if (!parsed.json) log('削除しませんでした。プロジェクトは変更されていません。');
|
|
66
|
+
return { exitCode: 0, classification, removed: [], failures: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const remove = options.remove ?? ((target) => rm(target, { recursive: true, force: true }));
|
|
71
|
+
const removed = [];
|
|
72
|
+
const failures = [];
|
|
73
|
+
for (const entry of classification.disposable) {
|
|
74
|
+
const target = path.resolve(parsed.projectRoot, entry.path);
|
|
75
|
+
if (!isInside(parsed.projectRoot, target) || target === parsed.projectRoot) {
|
|
76
|
+
const failure = { path: entry.path, code: 'OUTSIDE_PROJECT' };
|
|
77
|
+
failures.push(failure);
|
|
78
|
+
error(`削除に失敗しました: ${entry.path} (${failure.code})`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await remove(target, { recursive: true, force: true });
|
|
83
|
+
removed.push(entry.path);
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
const code = errorCode(cause);
|
|
86
|
+
failures.push({ path: entry.path, code });
|
|
87
|
+
error(`削除に失敗しました: ${entry.path} (${code})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (failures.length > 0) {
|
|
92
|
+
error('一部を削除できませんでした。AKARI Video と書き出し処理を終了してから再実行してください。');
|
|
93
|
+
return { exitCode: 1, classification, removed, failures };
|
|
94
|
+
}
|
|
95
|
+
if (!parsed.json) log(`削除しました: ${removed.length} 件`);
|
|
96
|
+
return { exitCode: 0, classification, removed, failures };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseArguments(args, cwd = process.cwd()) {
|
|
100
|
+
const allowed = new Set(['--dry-run', '--yes', '--json', '--help']);
|
|
101
|
+
const flags = args.filter((value) => value.startsWith('-'));
|
|
102
|
+
const unknown = [...new Set(flags.filter((value) => !allowed.has(value)))];
|
|
103
|
+
if (unknown.length > 0) return { ok: false, message: `未知のオプションです: ${unknown.join(', ')}` };
|
|
104
|
+
const positional = args.filter((value) => !value.startsWith('-'));
|
|
105
|
+
if (positional.length > 1) return { ok: false, message: '引数のプロジェクトディレクトリは 1 つだけ指定できます。' };
|
|
106
|
+
const flagSet = new Set(flags);
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
help: flagSet.has('--help'),
|
|
110
|
+
yes: flagSet.has('--yes'),
|
|
111
|
+
dryRun: flagSet.has('--dry-run'),
|
|
112
|
+
json: flagSet.has('--json'),
|
|
113
|
+
projectRoot: path.resolve(cwd, positional[0] ?? '.'),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function cleanHelp() {
|
|
118
|
+
return [
|
|
119
|
+
'使い方: akari clean [dir] [--dry-run] [--yes] [--json] [--help]',
|
|
120
|
+
'',
|
|
121
|
+
'使い捨ての中間ファイル、保持する正本、判断が必要なものを一覧します。',
|
|
122
|
+
'既定では一覧だけを表示し、削除前には必ず確認します。',
|
|
123
|
+
'',
|
|
124
|
+
' --dry-run 一覧だけを表示して削除しない',
|
|
125
|
+
' --yes 一覧を表示した後、削除可能なものだけを削除',
|
|
126
|
+
' --json 分類結果を JSON で表示',
|
|
127
|
+
' --help このヘルプを表示',
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatClassification(classification) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
for (const [className, heading] of [
|
|
134
|
+
['disposable', '削除可能:'],
|
|
135
|
+
['keep', '保持:'],
|
|
136
|
+
['undecided', '判断保留:'],
|
|
137
|
+
]) {
|
|
138
|
+
lines.push(heading);
|
|
139
|
+
const entries = classification[className];
|
|
140
|
+
if (entries.length === 0) lines.push(' (なし)');
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const provenance = entry.provenance
|
|
143
|
+
? ` / 由来: ${entry.provenance.origin} / 生成器: ${entry.provenance.generator}`
|
|
144
|
+
: '';
|
|
145
|
+
lines.push(` ${entry.path} | ${entry.files} ファイル | ${formatBytes(entry.bytes)} | ${entry.reason}${provenance}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
lines.push(`削除可能 合計 ${formatBytes(classification.totals.disposable.bytes)}`);
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatBytes(bytes) {
|
|
153
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
154
|
+
const units = ['KB', 'MB', 'GB'];
|
|
155
|
+
let value = bytes / 1024;
|
|
156
|
+
let unit = units[0];
|
|
157
|
+
for (let index = 1; index < units.length && value >= 1024; index += 1) {
|
|
158
|
+
value /= 1024;
|
|
159
|
+
unit = units[index];
|
|
160
|
+
}
|
|
161
|
+
return `${value.toFixed(1)} ${unit}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function provenanceWarnings(classification) {
|
|
165
|
+
return classification.keep
|
|
166
|
+
.filter((entry) => entry.provenance?.origin_exists === false)
|
|
167
|
+
.map((entry) => `[警告] 由来の計画ファイルが見当たりません: ${entry.provenance.origin}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function promptForConfirmation() {
|
|
171
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
172
|
+
try {
|
|
173
|
+
return await readline.question('削除可能な中間ファイルだけを削除しますか? [y/N] ');
|
|
174
|
+
} finally {
|
|
175
|
+
readline.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isInside(root, candidate) {
|
|
180
|
+
const relative = path.relative(root, candidate);
|
|
181
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function errorCode(error) {
|
|
185
|
+
if (error && typeof error === 'object') return String(error.code ?? error.errno ?? messageOf(error));
|
|
186
|
+
return messageOf(error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function messageOf(error) {
|
|
190
|
+
return error instanceof Error ? error.message : String(error);
|
|
191
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { lstat as fsLstat, readdir as fsReaddir, readFile as fsReadFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const RECENT_GUARD_MS = 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
export const CLEAN_MANIFEST = Object.freeze([
|
|
7
|
+
manifest('.akari/render-tmp/*', 'disposable', '書き出しの一時作業領域', 'render-cut'),
|
|
8
|
+
manifest('.akari/cache/**', 'disposable', '原本から再生成できるキャッシュ', 'AKARI Video'),
|
|
9
|
+
manifest('.akari/diffs/*', 'disposable', '「変更を見る」で再生成できる差分', 'AKARI Video'),
|
|
10
|
+
manifest('exports/*.gpu-video.mp4', 'disposable', 'GPU 書き出しの映像中間ファイル', 'render-cut'),
|
|
11
|
+
manifest('exports/*.osr-video.mp4', 'disposable', 'OSR 書き出しの映像中間ファイル', 'render-cut'),
|
|
12
|
+
manifest('exports/run.json', 'disposable', '書き出し子プロセスの一時記録', 'render-cut'),
|
|
13
|
+
manifest('.akari/work/tmp/**', 'disposable', '明示された使い捨て作業領域', '実行したスキル'),
|
|
14
|
+
manifest('.akari/work/** (with .akari-disposable)', 'disposable', '使い捨て目印がある作業領域', '実行したスキル'),
|
|
15
|
+
|
|
16
|
+
manifest('edit.json', 'keep', '編集内容の正本', null),
|
|
17
|
+
manifest('captions.json', 'keep', '字幕データ', null),
|
|
18
|
+
manifest('review.json', 'keep', 'レビュー注釈', null),
|
|
19
|
+
manifest('plan.json', 'keep', '承認済みの計画', null),
|
|
20
|
+
manifest('motion/**', 'keep', '再生成できないキーフレーム曲線', null),
|
|
21
|
+
manifest('assets/**', 'keep', '素材の原本', null),
|
|
22
|
+
manifest('planning/**', 'keep', '企画・計画文書', null),
|
|
23
|
+
manifest('exports/*.mp4', 'keep', '納品用の書き出し', null),
|
|
24
|
+
manifest('exports/nle/**', 'keep', 'NLE 用の書き出し', null),
|
|
25
|
+
manifest('.akari/reports/**', 'keep', '検証と人間確認の証跡', null),
|
|
26
|
+
manifest('.akari/sidecars/**', 'keep', '素材分析などの高価なサイドカー', null),
|
|
27
|
+
manifest('.akari/*.json', 'keep', 'プロジェクト状態の契約ファイル', null),
|
|
28
|
+
manifest('.akari/backup/**', 'keep', '移行前の退避', null),
|
|
29
|
+
manifest('.akari/events/**', 'keep', '節目の記録', null),
|
|
30
|
+
manifest('.akari/work/keep/**', 'keep', '作り直せない作業成果', null),
|
|
31
|
+
manifest('.akari/work/** (with .akari-keep)', 'keep', '保持目印がある作業領域', null),
|
|
32
|
+
manifest('.akari/work/**/.akari-disposable', 'keep', '作業領域の分類目印', null),
|
|
33
|
+
manifest('.akari/work/**/.akari-keep', 'keep', '作業領域の分類目印', null),
|
|
34
|
+
manifest('.claude/**', 'keep', 'プロジェクトのエージェント設定', null),
|
|
35
|
+
manifest('.opencode/**', 'keep', 'プロジェクトのエージェント設定', null),
|
|
36
|
+
manifest('AGENTS.md', 'keep', 'プロジェクトのエージェント向け案内', null),
|
|
37
|
+
manifest('CLAUDE.md', 'keep', 'プロジェクトのエージェント向け案内', null),
|
|
38
|
+
manifest('akari.sh', 'keep', 'プロジェクトの起動スクリプト', null),
|
|
39
|
+
manifest('.gitignore', 'keep', 'プロジェクトの除外設定', null),
|
|
40
|
+
|
|
41
|
+
manifest('.akari/work/**', 'undecided', '使い捨てと正本の区別がない作業領域', null),
|
|
42
|
+
manifest('<recent-disposable>', 'undecided', '実行中の可能性', null),
|
|
43
|
+
manifest('<symbolic-link>', 'undecided', 'シンボリックリンク(参照先は調べません)', null),
|
|
44
|
+
manifest('<unknown-top-level>', 'undecided', '宣言表に分類がありません', null),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const RULES = new Map(CLEAN_MANIFEST.map((entry) => [entry.pattern, entry]));
|
|
48
|
+
|
|
49
|
+
function manifest(pattern, className, reason, regeneratedBy) {
|
|
50
|
+
return Object.freeze({ pattern, class: className, reason, regenerated_by: regeneratedBy });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rule(pattern) {
|
|
54
|
+
const entry = RULES.get(pattern);
|
|
55
|
+
if (!entry) throw new Error(`clean manifest rule is missing: ${pattern}`);
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Project contents are grouped at boundaries that can be removed independently. The returned
|
|
61
|
+
* paths never contain absolute machine paths and every list is sorted with a platform-neutral
|
|
62
|
+
* code-point comparison.
|
|
63
|
+
*/
|
|
64
|
+
export async function classifyProject(root, options = {}) {
|
|
65
|
+
const projectRoot = path.resolve(root);
|
|
66
|
+
const io = {
|
|
67
|
+
lstat: options.lstat ?? fsLstat,
|
|
68
|
+
readdir: options.readdir ?? fsReaddir,
|
|
69
|
+
readFile: options.readFile ?? fsReadFile,
|
|
70
|
+
};
|
|
71
|
+
const nowValue = typeof options.now === 'function' ? options.now() : (options.now ?? Date.now());
|
|
72
|
+
const nowMs = nowValue instanceof Date ? nowValue.getTime() : Number(nowValue);
|
|
73
|
+
if (!Number.isFinite(nowMs)) throw new TypeError('now must be a Date or a finite timestamp');
|
|
74
|
+
|
|
75
|
+
const rootNode = await scanNode(projectRoot, '', io);
|
|
76
|
+
if (rootNode.kind !== 'directory') throw new Error(`project root is not a directory: ${projectRoot}`);
|
|
77
|
+
|
|
78
|
+
const buckets = { disposable: [], keep: [], undecided: [] };
|
|
79
|
+
const context = { projectRoot, io, nowMs, buckets };
|
|
80
|
+
for (const child of rootNode.children) await classifyTopLevel(child, context);
|
|
81
|
+
|
|
82
|
+
for (const className of ['disposable', 'keep', 'undecided']) {
|
|
83
|
+
buckets[className].sort((left, right) => comparePath(left.path, right.path));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
disposable: buckets.disposable,
|
|
88
|
+
keep: buckets.keep,
|
|
89
|
+
undecided: buckets.undecided,
|
|
90
|
+
totals: {
|
|
91
|
+
disposable: totalOf(buckets.disposable),
|
|
92
|
+
keep: totalOf(buckets.keep),
|
|
93
|
+
undecided: totalOf(buckets.undecided),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function classifyTopLevel(node, context) {
|
|
99
|
+
if (node.kind === 'symlink') return addSymlink(node, context);
|
|
100
|
+
|
|
101
|
+
const exactKeep = new Map([
|
|
102
|
+
['edit.json', 'edit.json'],
|
|
103
|
+
['captions.json', 'captions.json'],
|
|
104
|
+
['review.json', 'review.json'],
|
|
105
|
+
['plan.json', 'plan.json'],
|
|
106
|
+
['AGENTS.md', 'AGENTS.md'],
|
|
107
|
+
['CLAUDE.md', 'CLAUDE.md'],
|
|
108
|
+
['akari.sh', 'akari.sh'],
|
|
109
|
+
['.gitignore', '.gitignore'],
|
|
110
|
+
]);
|
|
111
|
+
if (exactKeep.has(node.rel)) return addKnownKeep(node, rule(exactKeep.get(node.rel)), context);
|
|
112
|
+
|
|
113
|
+
if (node.rel === 'motion' && node.kind === 'directory') return addKnownKeep(node, rule('motion/**'), context);
|
|
114
|
+
if (node.rel === 'planning' && node.kind === 'directory') return addKnownKeep(node, rule('planning/**'), context);
|
|
115
|
+
if (node.rel === '.claude' && node.kind === 'directory') return addKnownKeep(node, rule('.claude/**'), context);
|
|
116
|
+
if (node.rel === '.opencode' && node.kind === 'directory') return addKnownKeep(node, rule('.opencode/**'), context);
|
|
117
|
+
if (node.rel === 'assets' && node.kind === 'directory') return classifyAssets(node, context);
|
|
118
|
+
if (node.rel === 'exports' && node.kind === 'directory') return classifyExports(node, context);
|
|
119
|
+
if (node.rel === '.akari' && node.kind === 'directory') return classifyAkari(node, context);
|
|
120
|
+
|
|
121
|
+
addEntry(node, rule('<unknown-top-level>'), context);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function classifyAssets(assets, context) {
|
|
125
|
+
if (assets.children.length === 0) return addEntry(assets, rule('assets/**'), context);
|
|
126
|
+
for (const child of assets.children) {
|
|
127
|
+
if (child.kind === 'symlink') addSymlink(child, context);
|
|
128
|
+
else if (child.name === 'generated' && child.kind === 'directory') await classifyGeneratedAssets(child, context);
|
|
129
|
+
else addKnownKeep(child, rule('assets/**'), context);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function classifyGeneratedAssets(directory, context) {
|
|
134
|
+
if (directory.children.length === 0) {
|
|
135
|
+
addEntry(directory, rule('assets/**'), context);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const consumed = new Set();
|
|
140
|
+
const childrenByName = new Map(directory.children.map((child) => [child.name, child]));
|
|
141
|
+
for (const child of directory.children) {
|
|
142
|
+
if (consumed.has(child.name)) continue;
|
|
143
|
+
if (child.kind === 'symlink') {
|
|
144
|
+
addSymlink(child, context);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (child.kind === 'directory') {
|
|
148
|
+
await classifyGeneratedAssets(child, context);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (child.name.endsWith('.meta.json')) {
|
|
152
|
+
addEntry(child, rule('assets/**'), context);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sidecar = childrenByName.get(`${child.name}.meta.json`);
|
|
157
|
+
let provenance;
|
|
158
|
+
let stats = statsOf(child);
|
|
159
|
+
if (sidecar?.kind === 'file') {
|
|
160
|
+
consumed.add(sidecar.name);
|
|
161
|
+
stats = combineStats(child, sidecar);
|
|
162
|
+
provenance = await readProvenance(sidecar.abs, context);
|
|
163
|
+
}
|
|
164
|
+
addEntry(child, rule('assets/**'), context, { stats, provenance });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function classifyExports(exportsNode, context) {
|
|
169
|
+
for (const child of exportsNode.children) {
|
|
170
|
+
if (child.kind === 'symlink') {
|
|
171
|
+
addSymlink(child, context);
|
|
172
|
+
} else if (child.name === 'nle' && child.kind === 'directory') {
|
|
173
|
+
addKnownKeep(child, rule('exports/nle/**'), context);
|
|
174
|
+
} else if (child.kind === 'file' && child.name.endsWith('.gpu-video.mp4')) {
|
|
175
|
+
addDisposable(child, rule('exports/*.gpu-video.mp4'), context);
|
|
176
|
+
} else if (child.kind === 'file' && child.name.endsWith('.osr-video.mp4')) {
|
|
177
|
+
addDisposable(child, rule('exports/*.osr-video.mp4'), context);
|
|
178
|
+
} else if (child.kind === 'file' && child.name === 'run.json') {
|
|
179
|
+
addDisposable(child, rule('exports/run.json'), context);
|
|
180
|
+
} else if (child.kind === 'file' && child.name.endsWith('.mp4')) {
|
|
181
|
+
addEntry(child, rule('exports/*.mp4'), context);
|
|
182
|
+
} else {
|
|
183
|
+
addEntry(child, rule('<unknown-top-level>'), context);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function classifyAkari(akari, context) {
|
|
189
|
+
const keepDirectories = new Map([
|
|
190
|
+
['reports', '.akari/reports/**'],
|
|
191
|
+
['sidecars', '.akari/sidecars/**'],
|
|
192
|
+
['backup', '.akari/backup/**'],
|
|
193
|
+
['events', '.akari/events/**'],
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
for (const child of akari.children) {
|
|
197
|
+
if (child.kind === 'symlink') {
|
|
198
|
+
addSymlink(child, context);
|
|
199
|
+
} else if (child.name === 'render-tmp' && child.kind === 'directory') {
|
|
200
|
+
for (const candidate of child.children) addDisposable(candidate, rule('.akari/render-tmp/*'), context);
|
|
201
|
+
} else if (child.name === 'cache' && child.kind === 'directory') {
|
|
202
|
+
for (const candidate of child.children) addDisposable(candidate, rule('.akari/cache/**'), context);
|
|
203
|
+
} else if (child.name === 'diffs' && child.kind === 'directory') {
|
|
204
|
+
for (const candidate of child.children) addDisposable(candidate, rule('.akari/diffs/*'), context);
|
|
205
|
+
} else if (child.name === 'work' && child.kind === 'directory') {
|
|
206
|
+
await classifyWork(child, context);
|
|
207
|
+
} else if (keepDirectories.has(child.name) && child.kind === 'directory') {
|
|
208
|
+
addKnownKeep(child, rule(keepDirectories.get(child.name)), context);
|
|
209
|
+
} else if (child.kind === 'file' && child.name.endsWith('.json')) {
|
|
210
|
+
addEntry(child, rule('.akari/*.json'), context);
|
|
211
|
+
} else {
|
|
212
|
+
addEntry(child, rule('<unknown-top-level>'), context);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function classifyWork(work, context) {
|
|
218
|
+
for (const child of work.children) {
|
|
219
|
+
if (child.kind === 'symlink') {
|
|
220
|
+
addSymlink(child, context);
|
|
221
|
+
} else if (child.name === 'tmp' && child.kind === 'directory') {
|
|
222
|
+
await classifyWorkNode(child, 'disposable', rule('.akari/work/tmp/**'), context);
|
|
223
|
+
} else if (child.name === 'keep' && child.kind === 'directory') {
|
|
224
|
+
addKnownKeep(child, rule('.akari/work/keep/**'), context);
|
|
225
|
+
} else {
|
|
226
|
+
await classifyWorkNode(child, 'undecided', rule('.akari/work/**'), context);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function classifyWorkNode(node, inheritedClass, inheritedRule, context) {
|
|
232
|
+
if (node.kind === 'symlink') return addSymlink(node, context);
|
|
233
|
+
if (node.kind !== 'directory') {
|
|
234
|
+
if (isMarker(node.name)) return addEntry(node, markerRule(node.name), context);
|
|
235
|
+
if (inheritedClass === 'disposable') return addDisposable(node, inheritedRule, context);
|
|
236
|
+
return addEntry(node, inheritedRule, context);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ownKeep = node.directKeepMarker;
|
|
240
|
+
const ownDisposable = node.directDisposableMarker;
|
|
241
|
+
if (inheritedClass === 'keep' || ownKeep) {
|
|
242
|
+
addKnownKeep(node, ownKeep ? rule('.akari/work/** (with .akari-keep)') : inheritedRule, context);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const effectiveClass = ownDisposable ? 'disposable' : inheritedClass;
|
|
247
|
+
const effectiveRule = ownDisposable ? rule('.akari/work/** (with .akari-disposable)') : inheritedRule;
|
|
248
|
+
const hasNestedKeep = node.children.some((child) => child.kind === 'directory' && child.subtreeKeepMarker);
|
|
249
|
+
const mustSplit = hasNestedKeep || node.hasSymlink;
|
|
250
|
+
|
|
251
|
+
if (effectiveClass === 'disposable' && !mustSplit) {
|
|
252
|
+
addDisposable(node, effectiveRule, context);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const hasNestedClassification = node.children.some((child) => (
|
|
256
|
+
child.kind === 'directory' && (child.subtreeKeepMarker || child.subtreeDisposableMarker)
|
|
257
|
+
));
|
|
258
|
+
if (effectiveClass === 'undecided' && !hasNestedClassification && !node.hasSymlink) {
|
|
259
|
+
addEntry(node, effectiveRule, context);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (node.children.length === 0) {
|
|
263
|
+
addEntry(node, effectiveRule, context);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const child of node.children) {
|
|
268
|
+
if (isMarker(child.name) && child.kind === 'file') {
|
|
269
|
+
addEntry(child, markerRule(child.name), context);
|
|
270
|
+
} else {
|
|
271
|
+
await classifyWorkNode(child, effectiveClass, effectiveRule, context);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function addKnownKeep(node, keepRule, context) {
|
|
277
|
+
if (node.kind === 'symlink') return addSymlink(node, context);
|
|
278
|
+
if (node.kind !== 'directory' || !node.hasSymlink) {
|
|
279
|
+
addEntry(node, keepRule, context);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (node.children.length === 0) addEntry(node, keepRule, context);
|
|
283
|
+
for (const child of node.children) addKnownKeep(child, keepRule, context);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function addDisposable(node, disposableRule, context) {
|
|
287
|
+
if (node.kind === 'symlink') return addSymlink(node, context);
|
|
288
|
+
if (node.hasSymlink) {
|
|
289
|
+
addEntry(node, rule('<symbolic-link>'), context, { heldReason: 'シンボリックリンクを含むため' });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
addEntry(node, disposableRule, context, { guardRecent: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function addSymlink(node, context) {
|
|
296
|
+
addEntry(node, rule('<symbolic-link>'), context);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function addEntry(node, manifestRule, context, options = {}) {
|
|
300
|
+
const stats = options.stats ?? statsOf(node);
|
|
301
|
+
let className = manifestRule.class;
|
|
302
|
+
let reason = manifestRule.reason;
|
|
303
|
+
let heldReason = options.heldReason;
|
|
304
|
+
if (options.guardRecent && stats.latestMtimeMs >= context.nowMs - RECENT_GUARD_MS) {
|
|
305
|
+
className = 'undecided';
|
|
306
|
+
heldReason = '実行中の可能性';
|
|
307
|
+
reason = rule('<recent-disposable>').reason;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const entry = {
|
|
311
|
+
path: portablePath(node.rel),
|
|
312
|
+
class: className,
|
|
313
|
+
reason,
|
|
314
|
+
files: stats.files,
|
|
315
|
+
bytes: stats.bytes,
|
|
316
|
+
};
|
|
317
|
+
if (heldReason) entry.held_reason = heldReason;
|
|
318
|
+
if (options.provenance) entry.provenance = options.provenance;
|
|
319
|
+
context.buckets[className].push(entry);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function readProvenance(sidecarPath, context) {
|
|
323
|
+
try {
|
|
324
|
+
const value = JSON.parse(await context.io.readFile(sidecarPath, 'utf8'));
|
|
325
|
+
const provenance = value?.provenance;
|
|
326
|
+
if (!provenance || typeof provenance.origin !== 'string' || typeof provenance.generator !== 'string') return undefined;
|
|
327
|
+
const origin = portablePath(provenance.origin);
|
|
328
|
+
const originPath = path.resolve(context.projectRoot, provenance.origin);
|
|
329
|
+
let originExists = false;
|
|
330
|
+
if (isInside(context.projectRoot, originPath)) {
|
|
331
|
+
try {
|
|
332
|
+
const stat = await context.io.lstat(originPath);
|
|
333
|
+
originExists = stat.isFile();
|
|
334
|
+
} catch {
|
|
335
|
+
originExists = false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
origin,
|
|
340
|
+
generator: provenance.generator,
|
|
341
|
+
...(Array.isArray(provenance.inputs) ? { inputs: provenance.inputs.map(portablePath) } : {}),
|
|
342
|
+
...(typeof provenance.created_at === 'string' ? { created_at: provenance.created_at } : {}),
|
|
343
|
+
origin_exists: originExists,
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function scanNode(absolutePath, relativePath, io) {
|
|
351
|
+
const stat = await io.lstat(absolutePath);
|
|
352
|
+
const base = {
|
|
353
|
+
name: relativePath ? path.basename(absolutePath) : '',
|
|
354
|
+
abs: absolutePath,
|
|
355
|
+
rel: portablePath(relativePath),
|
|
356
|
+
files: 0,
|
|
357
|
+
bytes: 0,
|
|
358
|
+
latestMtimeMs: stat.mtimeMs,
|
|
359
|
+
hasSymlink: false,
|
|
360
|
+
directKeepMarker: false,
|
|
361
|
+
directDisposableMarker: false,
|
|
362
|
+
subtreeKeepMarker: false,
|
|
363
|
+
subtreeDisposableMarker: false,
|
|
364
|
+
};
|
|
365
|
+
if (stat.isSymbolicLink()) {
|
|
366
|
+
return { ...base, kind: 'symlink', files: 1, bytes: stat.size, hasSymlink: true, children: [] };
|
|
367
|
+
}
|
|
368
|
+
if (!stat.isDirectory()) {
|
|
369
|
+
return { ...base, kind: 'file', files: 1, bytes: stat.size, children: [] };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const names = await io.readdir(absolutePath);
|
|
373
|
+
names.sort(comparePath);
|
|
374
|
+
const children = [];
|
|
375
|
+
for (const name of names) {
|
|
376
|
+
children.push(await scanNode(path.join(absolutePath, name), relativePath ? path.join(relativePath, name) : name, io));
|
|
377
|
+
}
|
|
378
|
+
const directKeepMarker = children.some((child) => child.name === '.akari-keep' && child.kind === 'file');
|
|
379
|
+
const directDisposableMarker = children.some((child) => child.name === '.akari-disposable' && child.kind === 'file');
|
|
380
|
+
return {
|
|
381
|
+
...base,
|
|
382
|
+
kind: 'directory',
|
|
383
|
+
children,
|
|
384
|
+
files: children.reduce((sum, child) => sum + child.files, 0),
|
|
385
|
+
bytes: children.reduce((sum, child) => sum + child.bytes, 0),
|
|
386
|
+
latestMtimeMs: Math.max(stat.mtimeMs, ...children.map((child) => child.latestMtimeMs)),
|
|
387
|
+
hasSymlink: children.some((child) => child.hasSymlink),
|
|
388
|
+
directKeepMarker,
|
|
389
|
+
directDisposableMarker,
|
|
390
|
+
subtreeKeepMarker: directKeepMarker || children.some((child) => child.subtreeKeepMarker),
|
|
391
|
+
subtreeDisposableMarker: directDisposableMarker || children.some((child) => child.subtreeDisposableMarker),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function statsOf(node) {
|
|
396
|
+
return { files: node.files, bytes: node.bytes, latestMtimeMs: node.latestMtimeMs };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function combineStats(...nodes) {
|
|
400
|
+
return {
|
|
401
|
+
files: nodes.reduce((sum, node) => sum + node.files, 0),
|
|
402
|
+
bytes: nodes.reduce((sum, node) => sum + node.bytes, 0),
|
|
403
|
+
latestMtimeMs: Math.max(...nodes.map((node) => node.latestMtimeMs)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function totalOf(entries) {
|
|
408
|
+
return {
|
|
409
|
+
files: entries.reduce((sum, entry) => sum + entry.files, 0),
|
|
410
|
+
bytes: entries.reduce((sum, entry) => sum + entry.bytes, 0),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function markerRule(name) {
|
|
415
|
+
return rule(name === '.akari-keep'
|
|
416
|
+
? '.akari/work/**/.akari-keep'
|
|
417
|
+
: '.akari/work/**/.akari-disposable');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isMarker(name) {
|
|
421
|
+
return name === '.akari-keep' || name === '.akari-disposable';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function portablePath(value) {
|
|
425
|
+
return String(value).split(path.sep).join('/');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function comparePath(left, right) {
|
|
429
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isInside(root, candidate) {
|
|
433
|
+
const relative = path.relative(root, candidate);
|
|
434
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
435
|
+
}
|
package/src/messages.mjs
CHANGED
|
@@ -330,6 +330,7 @@ export function describeCliHelp() {
|
|
|
330
330
|
' update [--force] 更新を確認する(--force で install.sh 経路の本体を入れ直す)',
|
|
331
331
|
' status 接続状態を確認する',
|
|
332
332
|
' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
|
|
333
|
+
' akari clean [dir] 使い捨ての中間ファイルを一覧・削除(既定は一覧のみ)',
|
|
333
334
|
'',
|
|
334
335
|
'開発者向け:',
|
|
335
336
|
' --opencode Claude Code の代わりに opencode を起動する',
|