@file-viewer/cli 3.0.0

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/dist/cli.js ADDED
@@ -0,0 +1,1992 @@
1
+ #!/usr/bin/env node
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { cp, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, rmdir, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { createRequire } from 'node:module';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { assertFileViewerProjectAdapterCanWrite, inspectFileViewerProjectAdapter } from './project-adapters.js';
11
+ import { createFileViewerCarrierCommand, createFileViewerRegistryEnvironment, detectYarnGeneration } from './carrier-command.js';
12
+ import { DEFAULT_FILE_VIEWER_CONFIG, createFileViewerInstallPlan, doctorFileViewerProject, detectPackageManager, executeFileViewerPlanStep, initializeFileViewerProject, inferFileViewerLocalAssetBaseUrl, normalizeFileViewerConfig, readFileViewerProjectConfig, updateFileViewerProjectSelection, generateFileViewerIntegrationModule, listFileViewerCapabilities, renderFileViewerCapabilityList, reconcileFileViewerAssetOwners, reconcileFileViewerManagedDependencies, scaffoldFileViewerQuickstart, patchFileViewerApplicationEntry, listFileViewerApplicationEntries, loadFileViewerCliCatalog, normalizeFileViewerRegistryUrl, prepareFileViewerOfflineDirectory, resolveFileViewerOfflinePackages } from './index.js';
13
+ const primaryHelpCommands = [
14
+ 'create',
15
+ 'add',
16
+ 'config add <token>',
17
+ 'config remove <token>'
18
+ ];
19
+ const advancedHelpCommands = [
20
+ 'list',
21
+ 'plan',
22
+ 'init',
23
+ 'generate',
24
+ 'install',
25
+ 'assets',
26
+ 'prepare',
27
+ 'cache',
28
+ 'copy-assets',
29
+ 'doctor',
30
+ 'verify'
31
+ ];
32
+ const helpOptions = [
33
+ '--project <dir>',
34
+ '--config <file>',
35
+ '--framework <name>',
36
+ '--profile <name>',
37
+ '--formats <csv>',
38
+ '--capabilities <csv>',
39
+ '--asset-target <dir>',
40
+ '--output <file>',
41
+ '--entry <file>',
42
+ '--package-manager <name>',
43
+ '--package-manager-version <v>',
44
+ '--registry <url>',
45
+ '--offline-dir <dir>',
46
+ '--cache-dir <dir>',
47
+ '--concurrency <n>',
48
+ '--lang <en|zh-CN|ja-JP|de-DE>',
49
+ '--locale <en|zh-CN|ja-JP|de-DE>',
50
+ '--file-viewer-version <ver>',
51
+ '--framework-version <ver>',
52
+ '--asset-base-url <url>',
53
+ '--write',
54
+ '--yes',
55
+ '--force',
56
+ '--json',
57
+ '--non-interactive',
58
+ '--dry-run',
59
+ '--version',
60
+ '--help'
61
+ ];
62
+ const helpCopy = {
63
+ en: {
64
+ usage: 'file-viewer <create|add> [options]',
65
+ primary: 'Primary commands',
66
+ advanced: 'Advanced commands',
67
+ options: 'Options',
68
+ offline: 'Offline preparation',
69
+ offlineDescription: 'Downloads the exact File Viewer-owned closure atomically. Air-gapped installs also need a package-manager cache for third-party framework dependencies.',
70
+ commands: {
71
+ create: 'Create a runnable File Viewer project.',
72
+ add: 'Detect and integrate an existing package.json project.',
73
+ 'config add <token>': 'Add a format or renderer capability.',
74
+ 'config remove <token>': 'Remove an explicitly selected capability.',
75
+ list: 'List supported formats and explicit capabilities.',
76
+ plan: 'Print pinned packages, assets, licenses, weight, and commands.',
77
+ init: 'Create file-viewer.config.json without installing packages.',
78
+ generate: 'Generate the deterministic renderer registration module.',
79
+ install: 'Install pinned packages and required modular assets.',
80
+ assets: 'Install or repair modular assets; without config, use the legacy contract.',
81
+ prepare: 'Prepare a verified offline tgz closure from an explicit registry.',
82
+ cache: 'Alias of prepare for a reusable verified offline directory.',
83
+ 'copy-assets': 'Run the complete legacy copy-assets compatibility command.',
84
+ doctor: 'Inspect dependency, generated-entry, receipt, and asset drift.',
85
+ verify: 'Run doctor and exit non-zero when errors are present.'
86
+ },
87
+ flags: {
88
+ '--project <dir>': 'Project root (default: current directory).',
89
+ '--config <file>': 'Config filename (default: file-viewer.config.json).',
90
+ '--framework <name>': 'web, vue3, vue2.7, vue2.6, react, react-legacy, svelte, or jquery.',
91
+ '--profile <name>': 'standard, lite, office, engineering, all, full, or custom.',
92
+ '--formats <csv>': 'Extra file extensions to enable.',
93
+ '--capabilities <csv>': 'Extra capability IDs to enable.',
94
+ '--asset-target <dir>': 'Contained project-relative asset directory.',
95
+ '--output <file>': 'Generated integration module path.',
96
+ '--entry <file>': 'Contained client entry to integrate.',
97
+ '--package-manager <name>': 'npm, pnpm, yarn, or bun.',
98
+ '--package-manager-version <v>': 'Exact package-manager version; distinguishes Yarn 1 and 4.',
99
+ '--registry <url>': 'Explicit npm/private registry used during installation.',
100
+ '--offline-dir <dir>': 'Verified exact-catalog tgz directory.',
101
+ '--cache-dir <dir>': 'Controlled package-manager cache/store directory.',
102
+ '--concurrency <n>': 'prepare/cache: 1-8; install network/script work: 1-32.',
103
+ '--lang <en|zh-CN|ja-JP|de-DE>': 'CLI language (default: system language).',
104
+ '--locale <en|zh-CN|ja-JP|de-DE>': 'Alias of --lang.',
105
+ '--file-viewer-version <ver>': 'Require an exact CLI catalog version.',
106
+ '--framework-version <ver>': 'Exact validated scaffold runtime version.',
107
+ '--asset-base-url <url>': 'Approved self-hosted/CDN asset base; local assets remain default.',
108
+ '--write': 'Allow init/config/generate/modular-assets writes.',
109
+ '--yes': 'Allow create/add/install/prepare to invoke package managers or downloads.',
110
+ '--force': 'Replace conflicting CLI-managed scaffold/config/generated files.',
111
+ '--json': 'Emit machine-readable JSON with stable English keys.',
112
+ '--non-interactive': 'Disable prompts for reproducible CI.',
113
+ '--dry-run': 'Force mutating workflows to perform no writes, installs, or downloads.',
114
+ '--version': 'Print the installed CLI catalog version.',
115
+ '--help': 'Show localized help.'
116
+ }
117
+ },
118
+ 'zh-CN': {
119
+ usage: 'file-viewer <create|add> [选项]',
120
+ primary: '主要命令',
121
+ advanced: '高级命令',
122
+ options: '选项',
123
+ offline: '离线准备',
124
+ offlineDescription: '原子下载精确的 File Viewer 自有依赖闭包。完全隔离安装还需要为第三方框架依赖准备包管理器缓存。',
125
+ commands: {
126
+ create: '创建可运行的 File Viewer 项目。',
127
+ add: '检测并接入已有 package.json 项目。',
128
+ 'config add <token>': '添加格式或 renderer 能力。',
129
+ 'config remove <token>': '移除显式选择的能力。',
130
+ list: '列出支持格式和显式能力。',
131
+ plan: '显示精确包、资产、许可证、体积和命令。',
132
+ init: '创建 file-viewer.config.json,不安装依赖。',
133
+ generate: '生成确定性的 renderer 注册模块。',
134
+ install: '安装精确依赖和必需的模块化资产。',
135
+ assets: '安装或修复模块化资产;无配置时使用旧兼容契约。',
136
+ prepare: '从显式 registry 准备经过校验的离线 tgz 闭包。',
137
+ cache: 'prepare 的别名,用于生成可复用的已校验离线目录。',
138
+ 'copy-assets': '执行完整的旧版 copy-assets 兼容命令。',
139
+ doctor: '检查依赖、生成入口、回执和资产漂移。',
140
+ verify: '执行 doctor;存在错误时以非零状态退出。'
141
+ },
142
+ flags: {
143
+ '--project <dir>': '项目根目录(默认当前目录)。',
144
+ '--config <file>': '配置文件名(默认 file-viewer.config.json)。',
145
+ '--framework <name>': 'web、vue3、vue2.7、vue2.6、react、react-legacy、svelte 或 jquery。',
146
+ '--profile <name>': 'standard、lite、office、engineering、all、full 或 custom。',
147
+ '--formats <csv>': '额外启用的文件扩展名。',
148
+ '--capabilities <csv>': '额外启用的 capability ID。',
149
+ '--asset-target <dir>': '项目边界内的相对资产目录。',
150
+ '--output <file>': '生成的集成模块路径。',
151
+ '--entry <file>': '需要接入的项目内客户端入口。',
152
+ '--package-manager <name>': 'npm、pnpm、yarn 或 bun。',
153
+ '--package-manager-version <v>': '精确包管理器版本,用于区分 Yarn 1 与 Yarn 4。',
154
+ '--registry <url>': '安装阶段使用的显式 npm/私有源。',
155
+ '--offline-dir <dir>': '经过校验的精确目录 tgz 目录。',
156
+ '--cache-dir <dir>': '受控包管理器 cache/store 目录。',
157
+ '--concurrency <n>': 'prepare/cache 为 1-8;安装网络/脚本并发为 1-32。',
158
+ '--lang <en|zh-CN|ja-JP|de-DE>': 'CLI 语言(默认跟随系统)。',
159
+ '--locale <en|zh-CN|ja-JP|de-DE>': '--lang 的别名。',
160
+ '--file-viewer-version <ver>': '要求精确匹配 CLI 能力目录版本。',
161
+ '--framework-version <ver>': '精确且已校验的脚手架运行时版本。',
162
+ '--asset-base-url <url>': '批准的自托管/CDN 资产根地址;默认仍使用本地资产。',
163
+ '--write': '允许 init/config/generate/模块化 assets 写入。',
164
+ '--yes': '允许 create/add/install/prepare 调用包管理器或下载。',
165
+ '--force': '替换冲突的 CLI 管理脚手架、配置或生成文件。',
166
+ '--json': '输出稳定英文键的机器可读 JSON。',
167
+ '--non-interactive': '关闭提示,供可复现 CI 使用。',
168
+ '--dry-run': '强制所有变更流程不写入、不安装、不下载。',
169
+ '--version': '显示已安装 CLI 能力目录版本。',
170
+ '--help': '显示本地化帮助。'
171
+ }
172
+ },
173
+ 'ja-JP': {
174
+ usage: 'file-viewer <create|add> [オプション]',
175
+ primary: '主要コマンド',
176
+ advanced: '高度なコマンド',
177
+ options: 'オプション',
178
+ offline: 'オフライン準備',
179
+ offlineDescription: 'File Viewer 所有の正確な依存クロージャを原子的に取得します。完全な隔離環境では、外部フレームワーク依存用のパッケージマネージャーキャッシュも必要です。',
180
+ commands: {
181
+ create: '実行可能な File Viewer プロジェクトを作成します。',
182
+ add: '既存の package.json プロジェクトを検出して統合します。',
183
+ 'config add <token>': '形式または renderer 機能を追加します。',
184
+ 'config remove <token>': '明示的に選択した機能を削除します。',
185
+ list: '対応形式と明示機能を一覧表示します。',
186
+ plan: '固定パッケージ、アセット、ライセンス、サイズ、コマンドを表示します。',
187
+ init: '依存をインストールせず file-viewer.config.json を作成します。',
188
+ generate: '決定的な renderer 登録モジュールを生成します。',
189
+ install: '固定依存と必要なモジュール式アセットをインストールします。',
190
+ assets: 'モジュール式アセットを修復します。設定がなければ旧互換契約です。',
191
+ prepare: '明示 Registry から検証済みオフライン tgz クロージャを準備します。',
192
+ cache: '再利用可能な検証済みオフラインディレクトリを作る prepare の別名です。',
193
+ 'copy-assets': '従来の copy-assets 互換コマンドを完全に実行します。',
194
+ doctor: '依存、生成 entry、receipt、アセットのずれを検査します。',
195
+ verify: 'doctor を実行し、エラーがあれば非ゼロで終了します。'
196
+ },
197
+ flags: {
198
+ '--project <dir>': 'プロジェクトルート(既定は現在のディレクトリ)。',
199
+ '--config <file>': '設定ファイル名(既定は file-viewer.config.json)。',
200
+ '--framework <name>': 'web、vue3、vue2.7、vue2.6、react、react-legacy、svelte、jquery。',
201
+ '--profile <name>': 'standard、lite、office、engineering、all、full、custom。',
202
+ '--formats <csv>': '追加で有効にする拡張子。',
203
+ '--capabilities <csv>': '追加で有効にする capability ID。',
204
+ '--asset-target <dir>': 'プロジェクト内の相対アセットディレクトリ。',
205
+ '--output <file>': '生成する統合モジュールのパス。',
206
+ '--entry <file>': '統合対象のプロジェクト内クライアント entry。',
207
+ '--package-manager <name>': 'npm、pnpm、yarn、bun。',
208
+ '--package-manager-version <v>': '正確なバージョン。Yarn 1 と Yarn 4 を区別します。',
209
+ '--registry <url>': 'インストール時に使う明示 npm/プライベート Registry。',
210
+ '--offline-dir <dir>': '検証済みの正確なカタログ tgz ディレクトリ。',
211
+ '--cache-dir <dir>': '管理されたキャッシュ/ストアディレクトリ。',
212
+ '--concurrency <n>': 'prepare/cache は 1-8、インストールのネットワーク/スクリプトは 1-32。',
213
+ '--lang <en|zh-CN|ja-JP|de-DE>': 'CLI 言語(既定はシステム言語)。',
214
+ '--locale <en|zh-CN|ja-JP|de-DE>': '--lang の別名。',
215
+ '--file-viewer-version <ver>': 'CLI カタログの正確なバージョンを要求します。',
216
+ '--framework-version <ver>': '検証済み scaffold runtime の正確なバージョン。',
217
+ '--asset-base-url <url>': '承認済み self-hosted/CDN 基底 URL。既定はローカルです。',
218
+ '--write': 'init/config/generate/モジュール式 assets の書き込みを許可します。',
219
+ '--yes': 'create/add/install/prepare のパッケージ処理または取得を許可します。',
220
+ '--force': '競合する CLI 管理 scaffold/config/生成ファイルを置換します。',
221
+ '--json': '安定した英語キーの JSON を出力します。',
222
+ '--non-interactive': '再現可能な CI のためプロンプトを無効にします。',
223
+ '--dry-run': '変更処理の書き込み、インストール、取得をすべて無効にします。',
224
+ '--version': 'インストール済み CLI カタログのバージョンを表示します。',
225
+ '--help': 'ローカライズされたヘルプを表示します。'
226
+ }
227
+ },
228
+ 'de-DE': {
229
+ usage: 'file-viewer <create|add> [Optionen]',
230
+ primary: 'Hauptbefehle',
231
+ advanced: 'Erweiterte Befehle',
232
+ options: 'Optionen',
233
+ offline: 'Offline-Vorbereitung',
234
+ offlineDescription: 'Lädt den exakten File-Viewer-eigenen Abschluss atomar. Für vollständig isolierte Installationen ist zusätzlich ein Paketmanager-Cache für externe Framework-Abhängigkeiten nötig.',
235
+ commands: {
236
+ create: 'Erstellt ein lauffähiges File-Viewer-Projekt.',
237
+ add: 'Erkennt und integriert ein vorhandenes package.json-Projekt.',
238
+ 'config add <token>': 'Fügt ein Format oder eine Renderer-Fähigkeit hinzu.',
239
+ 'config remove <token>': 'Entfernt eine explizit gewählte Fähigkeit.',
240
+ list: 'Listet unterstützte Formate und explizite Fähigkeiten.',
241
+ plan: 'Zeigt fixierte Pakete, Assets, Lizenzen, Größe und Befehle.',
242
+ init: 'Erstellt file-viewer.config.json ohne Paketinstallation.',
243
+ generate: 'Erzeugt das deterministische Renderer-Registrierungsmodul.',
244
+ install: 'Installiert fixierte Pakete und erforderliche modulare Assets.',
245
+ assets: 'Repariert modulare Assets; ohne Konfiguration gilt der alte Vertrag.',
246
+ prepare: 'Bereitet einen geprüften Offline-tgz-Abschluss aus einer expliziten Registry vor.',
247
+ cache: 'Alias von prepare für ein wiederverwendbares geprüftes Offline-Verzeichnis.',
248
+ 'copy-assets': 'Führt den vollständigen alten copy-assets-Kompatibilitätsbefehl aus.',
249
+ doctor: 'Prüft Abhängigkeiten, generierten Entry, Receipts und Asset-Abweichungen.',
250
+ verify: 'Führt doctor aus und endet bei Fehlern ungleich null.'
251
+ },
252
+ flags: {
253
+ '--project <dir>': 'Projektwurzel (Standard: aktuelles Verzeichnis).',
254
+ '--config <file>': 'Konfigurationsdatei (Standard: file-viewer.config.json).',
255
+ '--framework <name>': 'web, vue3, vue2.7, vue2.6, react, react-legacy, svelte oder jquery.',
256
+ '--profile <name>': 'standard, lite, office, engineering, all, full oder custom.',
257
+ '--formats <csv>': 'Zusätzlich zu aktivierende Dateiendungen.',
258
+ '--capabilities <csv>': 'Zusätzlich zu aktivierende Fähigkeits-IDs.',
259
+ '--asset-target <dir>': 'Projektinternes relatives Asset-Verzeichnis.',
260
+ '--output <file>': 'Pfad des erzeugten Integrationsmoduls.',
261
+ '--entry <file>': 'Projektinterner Client-Entry für die Integration.',
262
+ '--package-manager <name>': 'npm, pnpm, yarn oder bun.',
263
+ '--package-manager-version <v>': 'Exakte Version; unterscheidet Yarn 1 und Yarn 4.',
264
+ '--registry <url>': 'Explizite npm/private Registry für die Installation.',
265
+ '--offline-dir <dir>': 'Geprüftes tgz-Verzeichnis des exakten Katalogs.',
266
+ '--cache-dir <dir>': 'Kontrolliertes Cache-/Store-Verzeichnis.',
267
+ '--concurrency <n>': 'prepare/cache: 1-8; Installation Netzwerk/Skripte: 1-32.',
268
+ '--lang <en|zh-CN|ja-JP|de-DE>': 'CLI-Sprache (Standard: Systemsprache).',
269
+ '--locale <en|zh-CN|ja-JP|de-DE>': 'Alias für --lang.',
270
+ '--file-viewer-version <ver>': 'Fordert eine exakte CLI-Katalogversion.',
271
+ '--framework-version <ver>': 'Exakte validierte Scaffold-Runtime-Version.',
272
+ '--asset-base-url <url>': 'Freigegebene Self-hosted/CDN-Basis; lokale Assets bleiben Standard.',
273
+ '--write': 'Erlaubt Schreibzugriffe von init/config/generate/modularen Assets.',
274
+ '--yes': 'Erlaubt Paketmanager- oder Download-Schritte von create/add/install/prepare.',
275
+ '--force': 'Ersetzt kollidierende CLI-verwaltete Scaffold-/Config-/Generatordateien.',
276
+ '--json': 'Gibt maschinenlesbares JSON mit stabilen englischen Schlüsseln aus.',
277
+ '--non-interactive': 'Deaktiviert Prompts für reproduzierbare CI.',
278
+ '--dry-run': 'Erzwingt für Änderungen: keine Schreibzugriffe, Installationen oder Downloads.',
279
+ '--version': 'Zeigt die installierte CLI-Katalogversion.',
280
+ '--help': 'Zeigt lokalisierte Hilfe.'
281
+ }
282
+ }
283
+ };
284
+ const renderUsage = (locale) => {
285
+ const copy = helpCopy[locale];
286
+ const rows = (items, descriptions) => items.map((item) => ` ${item.padEnd(36)}${descriptions[item]}`).join('\n');
287
+ return `${copy.usage}
288
+
289
+ ${copy.primary}:
290
+ ${rows(primaryHelpCommands, copy.commands)}
291
+
292
+ ${copy.advanced}:
293
+ ${rows(advancedHelpCommands, copy.commands)}
294
+
295
+ ${copy.options}:
296
+ ${rows(helpOptions, copy.flags)}
297
+
298
+ ${copy.offline}:
299
+ file-viewer prepare --profile <name> --registry <url> --offline-dir <dir> --yes
300
+ ${copy.offlineDescription}
301
+ `;
302
+ };
303
+ const localizedUsage = Object.fromEntries(Object.keys(helpCopy).map((locale) => [locale, renderUsage(locale)]));
304
+ const commandHelp = {
305
+ en: {
306
+ create: 'Create a new runnable project. Dry-run unless --yes.',
307
+ add: 'Detect and integrate the current package.json project. Dry-run unless --yes.',
308
+ plan: 'Print pinned packages, assets, licenses, weight, and commands.',
309
+ install: 'Install pinned packages and all required assets. Requires --yes.',
310
+ assets: 'Install or repair selected assets. Without config, preserves the legacy copy-assets contract: --renderers selects groups; --clean --confirm enables bounded replacement; FILE_VIEWER_PUBLIC_DIR sets the target; FILE_VIEWER_SKIP_ASSET_COPY skips copying.',
311
+ prepare: 'Atomically prepare verified File Viewer tarballs from an explicit registry.',
312
+ cache: 'Alias of prepare for a verified reusable offline cache.',
313
+ 'copy-assets': 'Run the complete legacy copy-assets compatibility command.',
314
+ doctor: 'Check versions, generated entry integration, receipts, hashes, and missing assets.',
315
+ verify: 'Run doctor and exit non-zero when errors exist.',
316
+ list: 'List formats and explicit capabilities.',
317
+ generate: 'Generate the deterministic renderer registration module.',
318
+ init: 'Create file-viewer.config.json; requires --write.',
319
+ config: 'Use config add/remove <token> to update explicit selections.'
320
+ },
321
+ 'zh-CN': {
322
+ create: '创建可运行项目;除非使用 --yes,否则只预览。',
323
+ add: '检测并集成当前 package.json 项目;除非使用 --yes,否则只预览。',
324
+ plan: '显示锁定包、资产、许可证、体积和命令。',
325
+ install: '安装锁定包及全部必需资产,需要 --yes。',
326
+ assets: '安装或修复所选资产;无配置时保持旧 copy-assets 契约:--renderers 选择资源组;--clean --confirm 启用边界内替换;FILE_VIEWER_PUBLIC_DIR 指定目标;FILE_VIEWER_SKIP_ASSET_COPY 跳过复制。',
327
+ prepare: '从显式 registry 原子准备校验过的离线包。',
328
+ cache: 'prepare 的别名,用于可复用的完整性缓存。',
329
+ 'copy-assets': '执行完整的旧版 copy-assets 兼容命令。',
330
+ doctor: '检查版本、入口集成、收据、哈希和缺失资产。',
331
+ verify: '执行 doctor,有错误时非零退出。',
332
+ list: '列出格式和显式能力。',
333
+ generate: '生成确定性的 renderer 注册模块。',
334
+ init: '创建 file-viewer.config.json,需要 --write。',
335
+ config: '使用 config add/remove <token> 修改显式选择。'
336
+ },
337
+ 'ja-JP': {
338
+ create: '実行可能な新規プロジェクトを作成します。--yes がなければプレビューのみです。',
339
+ add: '現在の package.json プロジェクトを検出して統合します。',
340
+ plan: '固定パッケージ、アセット、ライセンス、サイズ、コマンドを表示します。',
341
+ install: '固定パッケージと必要な全アセットをインストールします。--yes が必要です。',
342
+ assets: '選択アセットを修復します。設定がなければ旧 copy-assets 契約です。--renderers はグループを選択し、--clean --confirm は境界内の置換を有効にし、FILE_VIEWER_PUBLIC_DIR は出力先、FILE_VIEWER_SKIP_ASSET_COPY はコピー省略を指定します。',
343
+ prepare: '明示 Registry から検証済みオフライン tarball を原子的に準備します。',
344
+ cache: '再利用可能な検証済みキャッシュを作る prepare の別名です。',
345
+ 'copy-assets': '従来の copy-assets 互換コマンドを完全に実行します。',
346
+ doctor: 'バージョン、entry、receipt、hash、不足アセットを検査します。',
347
+ verify: 'doctor を実行し、エラー時は非ゼロ終了します。',
348
+ list: '形式と明示機能を一覧します。',
349
+ generate: '決定的な renderer 登録モジュールを生成します。',
350
+ init: 'file-viewer.config.json を作成します。--write が必要です。',
351
+ config: 'config add/remove <token> で明示選択を更新します。'
352
+ },
353
+ 'de-DE': {
354
+ create: 'Erstellt ein lauffähiges Projekt; ohne --yes nur Vorschau.',
355
+ add: 'Erkennt und integriert das aktuelle package.json-Projekt.',
356
+ plan: 'Zeigt fixierte Pakete, Assets, Lizenzen, Größe und Befehle.',
357
+ install: 'Installiert Pakete und erforderliche Assets; benötigt --yes.',
358
+ assets: 'Installiert/repariert Assets; ohne Konfiguration gilt der alte copy-assets-Vertrag: --renderers wählt Gruppen; --clean --confirm erlaubt begrenztes Ersetzen; FILE_VIEWER_PUBLIC_DIR setzt das Ziel; FILE_VIEWER_SKIP_ASSET_COPY überspringt das Kopieren.',
359
+ prepare: 'Bereitet verifizierte Offline-Tarballs atomar aus einer expliziten Registry vor.',
360
+ cache: 'Alias für prepare für einen wiederverwendbaren verifizierten Cache.',
361
+ 'copy-assets': 'Führt den vollständigen alten copy-assets-Kompatibilitätsbefehl aus.',
362
+ doctor: 'Prüft Versionen, Entry-Integration, Receipts, Hashes und fehlende Assets.',
363
+ verify: 'Führt doctor aus und endet bei Fehlern ungleich null.',
364
+ list: 'Listet Formate und explizite Fähigkeiten.',
365
+ generate: 'Erzeugt das deterministische Renderer-Registrierungsmodul.',
366
+ init: 'Erstellt file-viewer.config.json; benötigt --write.',
367
+ config: 'config add/remove <token> ändert explizite Auswahl.'
368
+ }
369
+ };
370
+ const renderHelp = (locale, command) => {
371
+ const normalized = command.startsWith('config-')
372
+ ? 'config'
373
+ : command === 'capabilities'
374
+ ? 'list'
375
+ : command;
376
+ const detail = commandHelp[locale][normalized];
377
+ return detail ? `${localizedUsage[locale]}\n${normalized}: ${detail}\n` : localizedUsage[locale];
378
+ };
379
+ const legacyCopyAssetsHelp = {
380
+ en: {
381
+ usage: 'Usage: file-viewer-copy-assets [target-directory] [--renderers <csv>] [--clean --confirm]',
382
+ options: 'Options: --renderers copies exact renderer groups; --clean --confirm replaces only a dedicated safe target; --no-clean keeps merge mode; --lang selects help language.',
383
+ environment: 'Environment: FILE_VIEWER_PUBLIC_DIR sets the target, FILE_VIEWER_SKIP_ASSET_COPY skips copying, and INIT_CWD supplies the project root.'
384
+ },
385
+ 'zh-CN': {
386
+ usage: '用法:file-viewer-copy-assets [目标目录] [--renderers <csv>] [--clean --confirm]',
387
+ options: '选项:--renderers 复制精确 renderer 资源组;--clean --confirm 仅替换专用且安全的目标;--no-clean 保持合并模式;--lang 选择帮助语言。',
388
+ environment: '环境变量:FILE_VIEWER_PUBLIC_DIR 指定目标,FILE_VIEWER_SKIP_ASSET_COPY 跳过复制,INIT_CWD 指定项目根目录。'
389
+ },
390
+ 'ja-JP': {
391
+ usage: '使用法: file-viewer-copy-assets [出力先] [--renderers <csv>] [--clean --confirm]',
392
+ options: 'オプション: --renderers は正確な renderer グループだけをコピーします。--clean --confirm は専用の安全な出力先だけを置換し、--no-clean はマージを維持し、--lang はヘルプ言語を選択します。',
393
+ environment: '環境変数: FILE_VIEWER_PUBLIC_DIR は出力先、FILE_VIEWER_SKIP_ASSET_COPY はコピー省略、INIT_CWD はプロジェクトルートを指定します。'
394
+ },
395
+ 'de-DE': {
396
+ usage: 'Verwendung: file-viewer-copy-assets [Zielverzeichnis] [--renderers <csv>] [--clean --confirm]',
397
+ options: 'Optionen: --renderers kopiert exakte Renderer-Gruppen; --clean --confirm ersetzt nur ein dediziertes sicheres Ziel; --no-clean behält den Merge-Modus; --lang wählt die Hilfesprache.',
398
+ environment: 'Umgebung: FILE_VIEWER_PUBLIC_DIR setzt das Ziel, FILE_VIEWER_SKIP_ASSET_COPY überspringt das Kopieren und INIT_CWD setzt die Projektwurzel.'
399
+ }
400
+ };
401
+ const renderLegacyCopyAssetsHelp = (locale, version) => {
402
+ const copy = legacyCopyAssetsHelp[locale];
403
+ return `file-viewer-copy-assets ${version}\n\n${copy.usage}\n${copy.options}\n${copy.environment}\n`;
404
+ };
405
+ const normalizeCliLocale = (value) => {
406
+ const normalized = String(value ?? '')
407
+ .trim()
408
+ .replace(/_/g, '-')
409
+ .split(/[.@]/, 1)[0]
410
+ .toLowerCase();
411
+ if (normalized === 'zh' || normalized === 'zh-cn' || normalized === 'zh-hans')
412
+ return 'zh-CN';
413
+ if (normalized === 'ja' || normalized.startsWith('ja-'))
414
+ return 'ja-JP';
415
+ if (normalized === 'de' || normalized.startsWith('de-'))
416
+ return 'de-DE';
417
+ if (normalized === 'en' || normalized.startsWith('en-'))
418
+ return 'en';
419
+ return null;
420
+ };
421
+ const requireCliLocale = (value) => {
422
+ const locale = normalizeCliLocale(value);
423
+ if (!locale)
424
+ throw new Error(`Unsupported language ${value}. Use en, zh-CN, ja-JP, or de-DE.`);
425
+ return locale;
426
+ };
427
+ function parseArgs(argv) {
428
+ if (basename(process.argv[1] || '') === 'file-viewer-copy-assets')
429
+ argv = ['copy-assets', ...argv];
430
+ const configCommand = argv[0] === 'config' && (argv[1] === 'add' || argv[1] === 'remove') ? `config-${argv[1]}` : null;
431
+ const requestedLocaleIndex = argv.findIndex((arg) => arg === '--lang' || arg === '--locale');
432
+ if (requestedLocaleIndex >= 0 && !argv[requestedLocaleIndex + 1]) {
433
+ throw new Error(`${argv[requestedLocaleIndex]} requires a value.`);
434
+ }
435
+ const requestedLocale = requestedLocaleIndex >= 0 ? requireCliLocale(argv[requestedLocaleIndex + 1]) : undefined;
436
+ const result = {
437
+ command: configCommand ?? (argv[0] && !argv[0].startsWith('-') ? argv[0] : 'plan'),
438
+ projectRoot: process.cwd(),
439
+ configFile: DEFAULT_FILE_VIEWER_CONFIG,
440
+ formats: [],
441
+ capabilities: [],
442
+ json: false,
443
+ write: false,
444
+ yes: false,
445
+ force: false,
446
+ dryRun: false,
447
+ locale: requestedLocale ??
448
+ normalizeCliLocale(process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '') ??
449
+ 'en',
450
+ nonInteractive: false,
451
+ version: false,
452
+ sourceSpecified: false,
453
+ assetBaseUrlSpecified: false,
454
+ assetTargetSpecified: false,
455
+ passthrough: [],
456
+ positionals: []
457
+ };
458
+ let projectOptionUsed = false;
459
+ const start = configCommand ? 2 : result.command === argv[0] ? 1 : 0;
460
+ for (let index = start; index < argv.length; index += 1) {
461
+ const arg = argv[index];
462
+ const value = () => {
463
+ const next = argv[++index];
464
+ if (!next)
465
+ throw new Error(`${arg} requires a value.`);
466
+ return next;
467
+ };
468
+ if (arg === '--project') {
469
+ result.projectRoot = resolve(value());
470
+ projectOptionUsed = true;
471
+ }
472
+ else if (arg === '--config')
473
+ result.configFile = value();
474
+ else if (arg === '--framework')
475
+ result.framework = value();
476
+ else if (arg === '--profile')
477
+ result.profile = value();
478
+ else if (arg === '--formats')
479
+ result.formats.push(value());
480
+ else if (arg === '--capabilities')
481
+ result.capabilities.push(value());
482
+ else if (arg === '--package-manager')
483
+ result.packageManager = value();
484
+ else if (arg === '--package-manager-version')
485
+ result.packageManagerVersion = value();
486
+ else if (arg === '--asset-target') {
487
+ result.assetTarget = value();
488
+ result.assetTargetSpecified = true;
489
+ }
490
+ else if (arg === '--output')
491
+ result.output = value();
492
+ else if (arg === '--entry')
493
+ result.entry = value();
494
+ else if (arg === '--registry') {
495
+ result.registry = value();
496
+ result.sourceSpecified = true;
497
+ }
498
+ else if (arg === '--offline-dir') {
499
+ result.offlineDirectory = value();
500
+ result.sourceSpecified = true;
501
+ }
502
+ else if (arg === '--cache-dir') {
503
+ result.cacheDir = value();
504
+ }
505
+ else if (arg === '--concurrency') {
506
+ result.concurrency = Number(value());
507
+ }
508
+ else if (arg === '--locale' || arg === '--lang')
509
+ result.locale = requireCliLocale(value());
510
+ else if (arg === '--file-viewer-version')
511
+ result.fileViewerVersion = value();
512
+ else if (arg === '--framework-version')
513
+ result.frameworkVersion = value();
514
+ else if (arg === '--asset-base-url') {
515
+ result.assetBaseUrl = value();
516
+ result.assetBaseUrlSpecified = true;
517
+ }
518
+ else if (arg === '--json')
519
+ result.json = true;
520
+ else if (arg === '--write')
521
+ result.write = true;
522
+ else if (arg === '--yes')
523
+ result.yes = true;
524
+ else if (arg === '--force')
525
+ result.force = true;
526
+ else if (arg === '--non-interactive')
527
+ result.nonInteractive = true;
528
+ else if (arg === '--dry-run')
529
+ result.dryRun = true;
530
+ else if ((arg === '--help' || arg === '-h') && result.command === 'copy-assets')
531
+ result.passthrough.push(arg);
532
+ else if ((arg === '--version' || arg === '-v') && result.command === 'copy-assets')
533
+ result.passthrough.push(arg);
534
+ else if (arg === '--version' || arg === '-v')
535
+ result.version = true;
536
+ else if ((result.command === 'copy-assets' || result.command === 'assets') &&
537
+ ['--clean', '--confirm', '--no-clean'].includes(arg))
538
+ result.passthrough.push(arg);
539
+ else if ((result.command === 'copy-assets' || result.command === 'assets') &&
540
+ arg === '--renderers') {
541
+ result.passthrough.push(arg, value());
542
+ }
543
+ else if (arg === '--help' || arg === '-h') {
544
+ process.stdout.write(renderHelp(result.locale, result.command));
545
+ process.exit(0);
546
+ }
547
+ else if (result.command === 'copy-assets')
548
+ result.passthrough.push(arg);
549
+ else if (!arg.startsWith('-'))
550
+ result.positionals.push(arg);
551
+ else
552
+ throw new Error(`Unknown option ${arg}.`);
553
+ }
554
+ if (result.command === 'create' || result.command === 'add') {
555
+ if (result.positionals.length > 1)
556
+ throw new Error(`${result.command} accepts at most one project directory.`);
557
+ if (result.positionals[0]) {
558
+ if (projectOptionUsed)
559
+ throw new Error(`Use either ${result.command} <directory> or --project, not both.`);
560
+ result.projectRoot = resolve(result.positionals[0]);
561
+ }
562
+ }
563
+ else if (['config-add', 'config-remove', 'select', 'remove'].includes(result.command)) {
564
+ if (result.positionals.length !== 1)
565
+ throw new Error(`${result.command} requires exactly one format or capability token.`);
566
+ result.token = result.positionals[0];
567
+ }
568
+ else if (result.command !== 'assets' && result.positionals.length) {
569
+ throw new Error(`Unexpected positional argument for ${result.command}: ${result.positionals.join(', ')}.`);
570
+ }
571
+ else if (result.command === 'assets' && result.positionals.length > 1) {
572
+ throw new Error('assets accepts at most one target directory.');
573
+ }
574
+ return result;
575
+ }
576
+ const messages = {
577
+ en: {
578
+ framework: 'Framework',
579
+ profile: 'Profile',
580
+ version: 'File Viewer release',
581
+ packageManager: 'Package manager',
582
+ packageManagerVersion: 'Package manager version',
583
+ formats: 'File formats (comma-separated)',
584
+ capabilities: 'Capability ids (comma-separated)',
585
+ assetTarget: 'Asset target',
586
+ source: 'Package source',
587
+ sourceRegistry: 'Registry URL',
588
+ sourceOffline: 'Offline tarball directory',
589
+ delivery: 'Runtime asset delivery',
590
+ assetBaseUrl: 'Self-hosted/CDN asset base URL',
591
+ entry: 'Application entry',
592
+ choose: 'Choose a number',
593
+ back: 'b=back',
594
+ cancel: '0=cancel',
595
+ cancelled: 'Cancelled without changes.',
596
+ confirm: 'Install and write files? (y/N)',
597
+ existing: 'No package.json was found. Use file-viewer create for a new project.'
598
+ },
599
+ 'zh-CN': {
600
+ framework: '选择框架',
601
+ profile: '选择集成方案',
602
+ version: 'File Viewer 版本',
603
+ packageManager: '包管理器',
604
+ packageManagerVersion: '包管理器版本',
605
+ formats: '文件格式(逗号分隔)',
606
+ capabilities: '能力 ID(逗号分隔)',
607
+ assetTarget: '静态资源目录',
608
+ source: '软件包来源',
609
+ sourceRegistry: 'Registry 地址',
610
+ sourceOffline: '离线 tgz 目录',
611
+ delivery: '运行时资源分发',
612
+ assetBaseUrl: '自托管/CDN 资源基地址',
613
+ entry: '应用入口',
614
+ choose: '请选择编号',
615
+ back: 'b=返回',
616
+ cancel: '0=取消',
617
+ cancelled: '已取消,未修改文件。',
618
+ confirm: '确认安装并写入文件?(y/N)',
619
+ existing: '未找到 package.json。新项目请使用 file-viewer create。'
620
+ },
621
+ 'ja-JP': {
622
+ framework: 'フレームワーク',
623
+ profile: '統合プロファイル',
624
+ version: 'File Viewer リリース',
625
+ packageManager: 'パッケージマネージャー',
626
+ packageManagerVersion: 'パッケージマネージャーのバージョン',
627
+ formats: 'ファイル形式(カンマ区切り)',
628
+ capabilities: '機能 ID(カンマ区切り)',
629
+ assetTarget: 'アセット出力先',
630
+ source: 'パッケージ取得元',
631
+ sourceRegistry: 'Registry URL',
632
+ sourceOffline: 'オフライン tgz ディレクトリ',
633
+ delivery: 'ランタイムアセット配信',
634
+ assetBaseUrl: 'セルフホスト/CDN ベース URL',
635
+ entry: 'アプリケーションエントリ',
636
+ choose: '番号を選択',
637
+ back: 'b=戻る',
638
+ cancel: '0=キャンセル',
639
+ cancelled: '変更せずにキャンセルしました。',
640
+ confirm: 'インストールしてファイルを書き込みますか? (y/N)',
641
+ existing: 'package.json がありません。新規プロジェクトには file-viewer create を使用してください。'
642
+ },
643
+ 'de-DE': {
644
+ framework: 'Framework',
645
+ profile: 'Integrationsprofil',
646
+ version: 'File-Viewer-Version',
647
+ packageManager: 'Paketmanager',
648
+ packageManagerVersion: 'Paketmanager-Version',
649
+ formats: 'Dateiformate (kommagetrennt)',
650
+ capabilities: 'Fähigkeits-IDs (kommagetrennt)',
651
+ assetTarget: 'Asset-Ziel',
652
+ source: 'Paketquelle',
653
+ sourceRegistry: 'Registry-URL',
654
+ sourceOffline: 'Offline-tgz-Verzeichnis',
655
+ delivery: 'Runtime-Asset-Bereitstellung',
656
+ assetBaseUrl: 'Self-hosted/CDN-Basis-URL',
657
+ entry: 'Anwendungseinstieg',
658
+ choose: 'Nummer auswählen',
659
+ back: 'b=zurück',
660
+ cancel: '0=abbrechen',
661
+ cancelled: 'Ohne Änderungen abgebrochen.',
662
+ confirm: 'Installieren und Dateien schreiben? (y/N)',
663
+ existing: 'Keine package.json gefunden. Verwenden Sie file-viewer create für ein neues Projekt.'
664
+ }
665
+ };
666
+ const installSource = (args) => {
667
+ if (args.registry && args.offlineDirectory)
668
+ throw new Error('--registry and --offline-dir are mutually exclusive.');
669
+ if (args.concurrency !== undefined &&
670
+ (!Number.isInteger(args.concurrency) || args.concurrency < 1 || args.concurrency > 32)) {
671
+ throw new Error('--concurrency must be an integer from 1 to 32.');
672
+ }
673
+ if (args.offlineDirectory)
674
+ return {
675
+ kind: 'offline-directory',
676
+ directory: args.offlineDirectory,
677
+ cacheDir: args.cacheDir,
678
+ concurrency: args.concurrency
679
+ };
680
+ if (args.registry || args.cacheDir || args.concurrency)
681
+ return {
682
+ kind: 'registry',
683
+ registry: args.registry,
684
+ cacheDir: args.cacheDir,
685
+ concurrency: args.concurrency
686
+ };
687
+ return undefined;
688
+ };
689
+ function print(value, json) {
690
+ if (json)
691
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
692
+ else if (typeof value === 'string')
693
+ process.stdout.write(`${value}\n`);
694
+ else
695
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
696
+ }
697
+ async function snapshotProjectFiles(paths) {
698
+ const backups = [];
699
+ for (const path of [...new Set(paths)]) {
700
+ const content = await readFile(path).catch((error) => {
701
+ if (error.code === 'ENOENT')
702
+ return null;
703
+ throw error;
704
+ });
705
+ backups.push({ path, content });
706
+ }
707
+ return backups;
708
+ }
709
+ async function restoreProjectFiles(projectRoot, backups, absentArtifacts) {
710
+ for (const backup of backups) {
711
+ if (backup.content === null)
712
+ await rm(backup.path, { force: true });
713
+ else {
714
+ await mkdir(dirname(backup.path), { recursive: true });
715
+ await writeFile(backup.path, backup.content);
716
+ }
717
+ }
718
+ for (const artifact of absentArtifacts)
719
+ await rm(artifact, { recursive: true, force: true });
720
+ const root = resolve(projectRoot);
721
+ const directories = [
722
+ ...new Set(backups
723
+ .filter((backup) => backup.content === null)
724
+ .flatMap((backup) => {
725
+ const values = [];
726
+ let current = dirname(backup.path);
727
+ while (current !== root && current.startsWith(`${root}${sep}`)) {
728
+ values.push(current);
729
+ current = dirname(current);
730
+ }
731
+ return values;
732
+ }))
733
+ ].sort((left, right) => right.length - left.length);
734
+ for (const directory of directories) {
735
+ await rmdir(directory).catch((error) => {
736
+ if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code ?? ''))
737
+ throw error;
738
+ });
739
+ }
740
+ }
741
+ async function snapshotProjectDirectories(paths) {
742
+ const temporary = await mkdtemp(join(tmpdir(), 'file-viewer-directory-rollback-'));
743
+ const backups = [];
744
+ for (const [index, path] of [...new Set(paths)].entries()) {
745
+ if (!existsSync(path)) {
746
+ backups.push({ path, backupPath: null });
747
+ continue;
748
+ }
749
+ const backupPath = join(temporary, String(index));
750
+ await cp(path, backupPath, { recursive: true, preserveTimestamps: true });
751
+ backups.push({ path, backupPath });
752
+ }
753
+ return { temporary, backups };
754
+ }
755
+ async function restoreProjectDirectories(backups) {
756
+ for (const backup of backups) {
757
+ await rm(backup.path, { recursive: true, force: true });
758
+ if (backup.backupPath) {
759
+ await mkdir(dirname(backup.path), { recursive: true });
760
+ await cp(backup.backupPath, backup.path, { recursive: true, preserveTimestamps: true });
761
+ }
762
+ }
763
+ }
764
+ async function stageProjectDirectoriesAtomically(projectRoot, paths) {
765
+ const existing = [];
766
+ for (const path of [...new Set(paths)]) {
767
+ const details = await lstat(path).catch((error) => {
768
+ if (error.code === 'ENOENT')
769
+ return null;
770
+ throw error;
771
+ });
772
+ if (details)
773
+ existing.push(path);
774
+ }
775
+ if (!existing.length)
776
+ return null;
777
+ const root = resolve(projectRoot);
778
+ const temporary = await mkdtemp(join(dirname(root), `.${basename(root)}.file-viewer-install-`));
779
+ const backups = [];
780
+ try {
781
+ for (const [index, path] of existing.entries()) {
782
+ const backupPath = join(temporary, String(index));
783
+ await rename(path, backupPath);
784
+ backups.push({ path, backupPath });
785
+ }
786
+ }
787
+ catch (error) {
788
+ for (const backup of [...backups].reverse()) {
789
+ await mkdir(dirname(backup.path), { recursive: true });
790
+ await rename(backup.backupPath, backup.path);
791
+ }
792
+ await rm(temporary, { recursive: true, force: true });
793
+ throw new Error(`Could not establish an atomic package-manager rollback area before installation: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
794
+ }
795
+ return { temporary, backups };
796
+ }
797
+ async function rollbackAtomicProjectDirectories(transaction) {
798
+ if (!transaction)
799
+ return;
800
+ for (const backup of [...transaction.backups].reverse()) {
801
+ await rm(backup.path, { recursive: true, force: true });
802
+ await mkdir(dirname(backup.path), { recursive: true });
803
+ await rename(backup.backupPath, backup.path);
804
+ }
805
+ await rm(transaction.temporary, { recursive: true, force: true });
806
+ }
807
+ async function discardAtomicProjectDirectoryBackup(transaction) {
808
+ if (transaction)
809
+ await rm(transaction.temporary, { recursive: true, force: true });
810
+ }
811
+ async function configFromArgs(args) {
812
+ const existing = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
813
+ const selectedSource = args.sourceSpecified ? installSource(args) : existing?.source;
814
+ const source = selectedSource
815
+ ? {
816
+ ...selectedSource,
817
+ ...(args.cacheDir !== undefined ? { cacheDir: args.cacheDir } : {}),
818
+ ...(args.concurrency !== undefined ? { concurrency: args.concurrency } : {})
819
+ }
820
+ : installSource(args);
821
+ return normalizeFileViewerConfig({
822
+ ...(existing ?? {}),
823
+ ...(args.framework ? { framework: args.framework } : {}),
824
+ ...(args.profile ? { profile: args.profile } : {}),
825
+ formats: args.formats.length ? args.formats : (existing?.formats ?? []),
826
+ capabilities: args.capabilities.length ? args.capabilities : (existing?.capabilities ?? []),
827
+ assetTarget: args.assetTarget ?? existing?.assetTarget,
828
+ generatedModule: args.output ?? existing?.generatedModule,
829
+ entry: args.entry ?? existing?.entry,
830
+ packageManager: args.packageManager ?? existing?.packageManager,
831
+ packageManagerVersion: args.packageManagerVersion ?? existing?.packageManagerVersion,
832
+ locale: args.locale,
833
+ source,
834
+ frameworkVersion: args.frameworkVersion ?? existing?.frameworkVersion,
835
+ assetBaseUrl: args.assetBaseUrlSpecified
836
+ ? args.assetBaseUrl
837
+ : (args.assetBaseUrl ?? existing?.assetBaseUrl)
838
+ });
839
+ }
840
+ async function detectExistingProject(args) {
841
+ const packagePath = resolve(args.projectRoot, 'package.json');
842
+ if (!existsSync(packagePath))
843
+ throw new Error(messages[args.locale].existing);
844
+ const manifest = JSON.parse(await readFile(packagePath, 'utf8'));
845
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
846
+ const declaredPackageManager = manifest.packageManager?.match(/^(pnpm|npm|yarn|bun)@(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/i);
847
+ const existingConfig = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
848
+ const versionTuple = (raw) => {
849
+ const match = String(raw ?? '').match(/(?:^|[^0-9])(\d+)\.(\d+)(?:\.(\d+))?/);
850
+ return match
851
+ ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3] ?? 0) }
852
+ : null;
853
+ };
854
+ const exactVersion = (raw) => String(raw ?? '').match(/^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/)?.[1];
855
+ const candidates = [];
856
+ if (dependencies.vue) {
857
+ const version = versionTuple(dependencies.vue);
858
+ candidates.push({
859
+ dependency: 'vue',
860
+ framework: version?.major === 2 ? (version.minor <= 6 ? 'vue2.6' : 'vue2.7') : 'vue3',
861
+ version
862
+ });
863
+ }
864
+ if (dependencies.react) {
865
+ const version = versionTuple(dependencies.react);
866
+ candidates.push({
867
+ dependency: 'react',
868
+ framework: version && version.major <= 17 ? 'react-legacy' : 'react',
869
+ version
870
+ });
871
+ }
872
+ if (dependencies.svelte)
873
+ candidates.push({
874
+ dependency: 'svelte',
875
+ framework: 'svelte',
876
+ version: versionTuple(dependencies.svelte)
877
+ });
878
+ if (dependencies.jquery)
879
+ candidates.push({
880
+ dependency: 'jquery',
881
+ framework: 'jquery',
882
+ version: versionTuple(dependencies.jquery)
883
+ });
884
+ const interactive = !args.nonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
885
+ const preferredFramework = args.framework ?? existingConfig?.framework;
886
+ if (candidates.length > 1 && !preferredFramework && !interactive) {
887
+ throw new Error(`Multiple framework runtimes were detected (${candidates.map((item) => item.dependency).join(', ')}). Pass --framework explicitly.`);
888
+ }
889
+ const selected = preferredFramework
890
+ ? candidates.find((item) => item.framework === preferredFramework)
891
+ : candidates[0];
892
+ const framework = preferredFramework ?? selected?.framework ?? 'web';
893
+ const catalog = JSON.parse(await readFile(new URL('../catalog/catalog.json', import.meta.url), 'utf8'));
894
+ const versions = Object.keys(catalog.frameworkTemplates[framework].validatedVersions);
895
+ const selectedExactVersion = selected
896
+ ? exactVersion(dependencies[selected.dependency])
897
+ : undefined;
898
+ const detectedVersion = selectedExactVersion
899
+ ? versions.find((value) => value === selectedExactVersion)
900
+ : undefined;
901
+ return {
902
+ framework,
903
+ frameworkVersion: detectedVersion ??
904
+ (selected ? undefined : catalog.frameworkTemplates[framework].defaultVersion),
905
+ detectedRuntime: selected
906
+ ? {
907
+ dependency: selected.dependency,
908
+ declaredVersion: dependencies[selected.dependency],
909
+ version: selected.version
910
+ }
911
+ : null,
912
+ packageManager: declaredPackageManager?.[1]?.toLowerCase(),
913
+ packageManagerVersion: declaredPackageManager?.[2],
914
+ candidates
915
+ };
916
+ }
917
+ async function inspectExistingProject(args) {
918
+ const existingConfig = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
919
+ const adapter = await inspectFileViewerProjectAdapter(args.projectRoot);
920
+ const detection = await detectExistingProject(args);
921
+ const framework = args.framework ?? existingConfig?.framework ?? detection.framework;
922
+ const packagePath = resolve(args.projectRoot, 'package.json');
923
+ const manifest = JSON.parse(await readFile(packagePath, 'utf8'));
924
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
925
+ const buildConfigs = adapter.configPaths;
926
+ const assetTarget = args.assetTarget ?? existingConfig?.assetTarget ?? adapter.assetTarget;
927
+ const warnings = [...adapter.warnings];
928
+ if (args.assetTargetSpecified && adapter.manualSteps.length) {
929
+ warnings.push(`Explicit --asset-target ${args.assetTarget} overrides automatic static-directory detection. Review the adapter findings: ${adapter.manualSteps.join(' ')}`);
930
+ }
931
+ if (detection.detectedRuntime && !detection.frameworkVersion) {
932
+ warnings.push(`${detection.detectedRuntime.dependency} ${detection.detectedRuntime.declaredVersion} does not exactly match a validated scaffold runtime. The existing runtime will be preserved; pass --framework-version only to record an explicitly reviewed target.`);
933
+ }
934
+ const catalog = await listFileViewerCapabilities();
935
+ const existingFileViewerPackages = Object.entries(dependencies)
936
+ .filter(([name]) => name === 'file-viewer-copy-assets' || name.startsWith('@file-viewer/'))
937
+ .sort(([left], [right]) => left.localeCompare(right))
938
+ .map(([packageName, declaredVersion]) => ({ packageName, declaredVersion }));
939
+ for (const dependency of existingFileViewerPackages) {
940
+ if (/^\d+\.\d+\.\d+$/.test(dependency.declaredVersion) &&
941
+ dependency.declaredVersion !== catalog.coreVersion) {
942
+ warnings.push(`${dependency.packageName} declares ${dependency.declaredVersion}; this CLI catalog is ${catalog.coreVersion}.`);
943
+ }
944
+ }
945
+ const expectedFullPackage = `@file-viewer/${framework}-full`;
946
+ const dependencyProfiles = [
947
+ ...(dependencies[expectedFullPackage] ? ['full'] : []),
948
+ ...['standard', 'lite', 'office', 'engineering', 'all'].filter((profile) => dependencies[`@file-viewer/preset-${profile}`])
949
+ ];
950
+ const interactive = !args.nonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
951
+ const preferredProfile = args.profile ?? existingConfig?.profile;
952
+ if (dependencyProfiles.length > 1 && !preferredProfile && !interactive) {
953
+ throw new Error(`Multiple existing File Viewer profiles were detected (${dependencyProfiles.join(', ')}). Pass --profile explicitly.`);
954
+ }
955
+ const applicationEntries = listFileViewerApplicationEntries(args.projectRoot);
956
+ const entry = args.entry ?? existingConfig?.entry;
957
+ if (applicationEntries.length === 0 && !entry && !interactive) {
958
+ throw new Error('No supported application entry was found. Pass --entry with the contained client entry that should import the generated integration.');
959
+ }
960
+ if (applicationEntries.length > 1 && !entry && !interactive) {
961
+ throw new Error(`Multiple application entries were found (${applicationEntries.join(', ')}). Pass --entry explicitly.`);
962
+ }
963
+ return {
964
+ framework,
965
+ frameworkVersion: args.frameworkVersion ?? existingConfig?.frameworkVersion ?? detection.frameworkVersion,
966
+ detectedRuntime: detection.detectedRuntime,
967
+ detectedProfiles: dependencyProfiles,
968
+ detectedProfile: preferredProfile ?? dependencyProfiles[0],
969
+ packageManager: args.packageManager ??
970
+ existingConfig?.packageManager ??
971
+ detection.packageManager ??
972
+ detectPackageManager(args.projectRoot),
973
+ packageManagerVersion: args.packageManagerVersion ??
974
+ existingConfig?.packageManagerVersion ??
975
+ detection.packageManagerVersion,
976
+ assetTarget,
977
+ source: existingConfig?.source,
978
+ assetBaseUrl: existingConfig?.assetBaseUrl,
979
+ formats: existingConfig?.formats ?? [],
980
+ capabilities: existingConfig?.capabilities ?? [],
981
+ entry,
982
+ applicationEntries,
983
+ buildConfigs,
984
+ adapter,
985
+ existingFileViewerPackages,
986
+ warnings
987
+ };
988
+ }
989
+ async function executeStepsSequentially(steps, confirmed) {
990
+ const results = [];
991
+ for (const step of steps)
992
+ results.push(await executeFileViewerPlanStep(step, { confirmed }));
993
+ return results;
994
+ }
995
+ async function installOfflineCopyAssetsCarrier(directory, projectRoot, version) {
996
+ const packages = await resolveFileViewerOfflinePackages([{ packageName: 'file-viewer-copy-assets', version }], directory, projectRoot);
997
+ if (!packages.some((item) => item.packageName === 'file-viewer-copy-assets'))
998
+ throw new Error(`Offline directory is missing file-viewer-copy-assets@${version}.`);
999
+ const temporary = await mkdtemp(join(tmpdir(), 'file-viewer-offline-carrier-'));
1000
+ await writeFile(join(temporary, 'package.json'), '{"private":true,"type":"module"}\n', 'utf8');
1001
+ const installed = spawnSync('npm', [
1002
+ 'install',
1003
+ '--ignore-scripts',
1004
+ '--offline',
1005
+ '--no-audit',
1006
+ '--no-fund',
1007
+ '--package-lock=false',
1008
+ '--save=false',
1009
+ ...packages.map((item) => `file:${item.path}`)
1010
+ ], {
1011
+ cwd: temporary,
1012
+ encoding: 'utf8',
1013
+ shell: false,
1014
+ env: { ...process.env, npm_config_audit: 'false', npm_config_fund: 'false' }
1015
+ });
1016
+ if (installed.status !== 0) {
1017
+ await rm(temporary, { recursive: true, force: true });
1018
+ throw new Error(`Could not load offline file-viewer-copy-assets@${version}: ${installed.stderr || installed.stdout}`);
1019
+ }
1020
+ return {
1021
+ entry: join(temporary, 'node_modules/file-viewer-copy-assets/dist/index.js'),
1022
+ packageJsonPath: join(temporary, 'node_modules/file-viewer-copy-assets/package.json'),
1023
+ cleanup: () => rm(temporary, { recursive: true, force: true })
1024
+ };
1025
+ }
1026
+ async function executeInstalledWebFullCopyAssets(projectRequire, args, catalogVersion) {
1027
+ let packageJsonPath;
1028
+ try {
1029
+ packageJsonPath = projectRequire.resolve('@file-viewer/web-full/package.json');
1030
+ }
1031
+ catch {
1032
+ return null;
1033
+ }
1034
+ const packageJsonDetails = await lstat(packageJsonPath);
1035
+ if (!packageJsonDetails.isFile() || packageJsonDetails.isSymbolicLink()) {
1036
+ throw new Error('Installed @file-viewer/web-full package.json is not a regular file.');
1037
+ }
1038
+ const physicalPackageJson = await realpath(packageJsonPath);
1039
+ const packageRoot = await realpath(dirname(physicalPackageJson));
1040
+ const manifest = JSON.parse(await readFile(physicalPackageJson, 'utf8'));
1041
+ if (manifest.version !== catalogVersion) {
1042
+ throw new Error(`Installed @file-viewer/web-full@${String(manifest.version)} does not match CLI catalog ${catalogVersion}.`);
1043
+ }
1044
+ const relativeBin = manifest.bin?.['file-viewer-copy-assets'];
1045
+ if (!relativeBin ||
1046
+ relativeBin.startsWith('/') ||
1047
+ relativeBin.replace(/\\/g, '/').split('/').includes('..')) {
1048
+ throw new Error('Installed @file-viewer/web-full has no safe file-viewer-copy-assets compatibility bin.');
1049
+ }
1050
+ const unresolvedEntry = resolve(packageRoot, relativeBin);
1051
+ const entryDetails = await lstat(unresolvedEntry);
1052
+ if (!entryDetails.isFile() || entryDetails.isSymbolicLink()) {
1053
+ throw new Error('Installed @file-viewer/web-full file-viewer-copy-assets bin is not a regular file.');
1054
+ }
1055
+ const entry = await realpath(unresolvedEntry);
1056
+ const entryRelation = relative(packageRoot, entry);
1057
+ if (!entryRelation || entryRelation.startsWith('..') || isAbsolute(entryRelation)) {
1058
+ throw new Error('Installed @file-viewer/web-full file-viewer-copy-assets bin escapes its package.');
1059
+ }
1060
+ const delegatedArgs = args.passthrough.includes('--json')
1061
+ ? args.passthrough
1062
+ : [...args.passthrough, '--json'];
1063
+ const delegated = spawnSync(process.execPath, [entry, ...delegatedArgs], {
1064
+ cwd: args.projectRoot,
1065
+ encoding: 'utf8',
1066
+ shell: false,
1067
+ env: process.env
1068
+ });
1069
+ if (delegated.status !== 0) {
1070
+ throw new Error(`@file-viewer/web-full copy-assets failed: ${delegated.stderr || delegated.stdout}`);
1071
+ }
1072
+ try {
1073
+ const result = JSON.parse(delegated.stdout);
1074
+ return { ...result, mode: 'copy' };
1075
+ }
1076
+ catch {
1077
+ throw new Error(`@file-viewer/web-full copy-assets returned invalid JSON: ${delegated.stdout}`);
1078
+ }
1079
+ }
1080
+ async function executeLegacyCopyAssets(args) {
1081
+ const catalogVersion = (await loadFileViewerCliCatalog()).core.version;
1082
+ if (args.passthrough.some((item) => item === '--help' || item === '-h')) {
1083
+ return {
1084
+ mode: 'help',
1085
+ usage: renderLegacyCopyAssetsHelp(args.locale, catalogVersion)
1086
+ };
1087
+ }
1088
+ if (args.passthrough.some((item) => item === '--version' || item === '-v'))
1089
+ return { mode: 'version', version: catalogVersion };
1090
+ const projectRequire = createRequire(resolve(args.projectRoot, 'package.json'));
1091
+ let entry;
1092
+ let packageJsonPath;
1093
+ let cleanup;
1094
+ try {
1095
+ entry = projectRequire.resolve('file-viewer-copy-assets');
1096
+ packageJsonPath = projectRequire.resolve('file-viewer-copy-assets/package.json');
1097
+ }
1098
+ catch {
1099
+ const embeddedWebFullResult = await executeInstalledWebFullCopyAssets(projectRequire, args, catalogVersion);
1100
+ if (embeddedWebFullResult)
1101
+ return embeddedWebFullResult;
1102
+ const config = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
1103
+ const offlineDirectory = args.offlineDirectory ??
1104
+ (config?.source?.kind === 'offline-directory' ? config.source.directory : undefined);
1105
+ const explicitRegistryRaw = args.registry ?? (config?.source?.kind === 'registry' ? config.source.registry : undefined);
1106
+ if (offlineDirectory && explicitRegistryRaw)
1107
+ throw new Error('--registry and --offline-dir are mutually exclusive.');
1108
+ if (offlineDirectory) {
1109
+ const carrier = await installOfflineCopyAssetsCarrier(offlineDirectory, args.projectRoot, catalogVersion);
1110
+ entry = carrier.entry;
1111
+ packageJsonPath = carrier.packageJsonPath;
1112
+ cleanup = carrier.cleanup;
1113
+ }
1114
+ else {
1115
+ const explicitRegistry = explicitRegistryRaw
1116
+ ? normalizeFileViewerRegistryUrl(explicitRegistryRaw)
1117
+ : undefined;
1118
+ if (!explicitRegistry) {
1119
+ throw new Error('No matching file-viewer-copy-assets payload is installed. Install a full package, or explicitly configure a registry before on-demand retrieval; automatic registry fallback is disabled.');
1120
+ }
1121
+ const manager = args.packageManager ?? detectPackageManager(args.projectRoot);
1122
+ const spec = `file-viewer-copy-assets@${catalogVersion}`;
1123
+ const carrier = createFileViewerCarrierCommand(manager, spec, args.passthrough, {
1124
+ projectRoot: args.projectRoot
1125
+ });
1126
+ const delegated = spawnSync(carrier.command, carrier.args, {
1127
+ cwd: args.projectRoot,
1128
+ encoding: 'utf8',
1129
+ shell: false,
1130
+ env: {
1131
+ ...process.env,
1132
+ ...createFileViewerRegistryEnvironment(explicitRegistry)
1133
+ }
1134
+ });
1135
+ if (delegated.status !== 0)
1136
+ throw new Error(`${carrier.command} ${carrier.args.join(' ')} failed: ${delegated.stderr || delegated.stdout}`);
1137
+ return {
1138
+ mode: 'delegated',
1139
+ packageSpec: spec,
1140
+ registry: explicitRegistry,
1141
+ ...carrier,
1142
+ stdout: delegated.stdout
1143
+ };
1144
+ }
1145
+ }
1146
+ try {
1147
+ const api = (await import(pathToFileURL(entry).href));
1148
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
1149
+ if (packageJson.version !== catalogVersion)
1150
+ throw new Error(`Installed file-viewer-copy-assets@${packageJson.version} does not match CLI catalog ${catalogVersion}.`);
1151
+ const parsed = api.parseCopyAssetsCliArguments(args.passthrough);
1152
+ if (process.env.FILE_VIEWER_SKIP_ASSET_COPY === '1' ||
1153
+ process.env.FILE_VIEWER_SKIP_ASSET_COPY === 'true')
1154
+ return { mode: 'skipped' };
1155
+ const targetDir = parsed.targetDir
1156
+ ? resolve(args.projectRoot, parsed.targetDir)
1157
+ : process.env.FILE_VIEWER_PUBLIC_DIR
1158
+ ? undefined
1159
+ : resolve(args.projectRoot) !== resolve(process.cwd())
1160
+ ? resolve(args.projectRoot, 'public/file-viewer')
1161
+ : undefined;
1162
+ return {
1163
+ mode: 'copy',
1164
+ ...(await api.copyFileViewerAssets({ ...parsed, ...(targetDir ? { targetDir } : {}) }))
1165
+ };
1166
+ }
1167
+ finally {
1168
+ await cleanup?.();
1169
+ }
1170
+ }
1171
+ async function promptQuickstart(args, existing, inspection) {
1172
+ if (args.nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY)
1173
+ return args;
1174
+ const io = createInterface({ input: process.stdin, output: process.stdout });
1175
+ try {
1176
+ const catalog = await listFileViewerCapabilities();
1177
+ const choose = async (label, values, preferred, allowBack = false) => {
1178
+ process.stdout.write(`\n${label}:\n${values.map((value, index) => ` ${index + 1}) ${value}${value === preferred ? ' *' : ''}`).join('\n')}\n`);
1179
+ while (true) {
1180
+ const answer = (await io.question(`${messages[args.locale].choose} (${messages[args.locale].cancel}${allowBack ? `, ${messages[args.locale].back}` : ''}) [${values.indexOf(preferred) + 1}]: `)).trim();
1181
+ if (!answer)
1182
+ return preferred;
1183
+ if (answer === '0')
1184
+ throw new Error(messages[args.locale].cancelled);
1185
+ if (allowBack && /^b$/i.test(answer))
1186
+ return null;
1187
+ const index = Number(answer) - 1;
1188
+ if (Number.isInteger(index) && values[index])
1189
+ return values[index];
1190
+ }
1191
+ };
1192
+ const askText = async (label, preferred = '', allowEmpty = true) => {
1193
+ while (true) {
1194
+ const suffix = preferred ? ` [${preferred}]` : '';
1195
+ const answer = (await io.question(`${label}${suffix} (${messages[args.locale].cancel}, ${messages[args.locale].back}): `)).trim();
1196
+ if (answer === '0')
1197
+ throw new Error(messages[args.locale].cancelled);
1198
+ if (/^b$/i.test(answer))
1199
+ return null;
1200
+ if (answer)
1201
+ return answer;
1202
+ if (preferred || allowEmpty)
1203
+ return preferred;
1204
+ }
1205
+ };
1206
+ const frameworks = [
1207
+ 'web',
1208
+ 'vue3',
1209
+ 'vue2.7',
1210
+ 'vue2.6',
1211
+ 'react',
1212
+ 'react-legacy',
1213
+ 'svelte',
1214
+ 'jquery'
1215
+ ];
1216
+ const profiles = ['standard', 'lite', 'office', 'engineering', 'all', 'full', 'custom'];
1217
+ const fullCatalog = JSON.parse(await readFile(new URL('../catalog/catalog.json', import.meta.url), 'utf8'));
1218
+ let framework = args.framework ?? inspection?.framework ?? 'web';
1219
+ let profile = args.profile ?? inspection?.detectedProfile ?? 'standard';
1220
+ let frameworkVersion = args.frameworkVersion ??
1221
+ inspection?.frameworkVersion ??
1222
+ fullCatalog.frameworkTemplates[framework].defaultVersion;
1223
+ let packageManager = args.packageManager ?? inspection?.packageManager ?? detectPackageManager(args.projectRoot);
1224
+ let packageManagerVersion = args.packageManagerVersion ??
1225
+ inspection?.packageManagerVersion ??
1226
+ (packageManager === 'yarn'
1227
+ ? detectYarnGeneration(args.projectRoot) === 'berry'
1228
+ ? '4.9.2'
1229
+ : '1.22.22'
1230
+ : undefined);
1231
+ let formats = args.formats.length
1232
+ ? args.formats.join(',')
1233
+ : (inspection?.formats.join(',') ?? '');
1234
+ let capabilities = args.capabilities.length
1235
+ ? args.capabilities.join(',')
1236
+ : (inspection?.capabilities.join(',') ?? '');
1237
+ let assetTarget = args.assetTarget ?? inspection?.assetTarget ?? 'public/file-viewer';
1238
+ let sourceKind = args.offlineDirectory
1239
+ ? 'offline'
1240
+ : args.registry
1241
+ ? 'registry'
1242
+ : inspection?.source?.kind === 'offline-directory'
1243
+ ? 'offline'
1244
+ : inspection?.source?.kind === 'registry'
1245
+ ? 'registry'
1246
+ : 'default';
1247
+ let registry = args.registry ??
1248
+ (inspection?.source?.kind === 'registry' ? (inspection.source.registry ?? '') : '');
1249
+ let offlineDirectory = args.offlineDirectory ??
1250
+ (inspection?.source?.kind === 'offline-directory' ? inspection.source.directory : '');
1251
+ let delivery = (args.assetBaseUrl ?? inspection?.assetBaseUrl) ? 'cdn' : 'local';
1252
+ let assetBaseUrl = args.assetBaseUrl ?? inspection?.assetBaseUrl ?? '';
1253
+ let entry = args.entry ?? inspection?.entry;
1254
+ const entries = inspection?.applicationEntries ?? [];
1255
+ let step = 0;
1256
+ while (step < 14) {
1257
+ if (step === 0) {
1258
+ const value = await choose(messages[args.locale].framework, frameworks, framework, true);
1259
+ if (value === null) {
1260
+ step = 0;
1261
+ continue;
1262
+ }
1263
+ framework = value;
1264
+ const template = fullCatalog.frameworkTemplates[framework];
1265
+ if (!Object.hasOwn(template.validatedVersions, frameworkVersion))
1266
+ frameworkVersion = template.defaultVersion;
1267
+ }
1268
+ else if (step === 1) {
1269
+ const value = await choose(messages[args.locale].profile, profiles, profile, true);
1270
+ if (value === null) {
1271
+ step -= 1;
1272
+ continue;
1273
+ }
1274
+ profile = value;
1275
+ }
1276
+ else if (step === 2) {
1277
+ const value = await choose(messages[args.locale].version, [catalog.coreVersion], catalog.coreVersion, true);
1278
+ if (value === null) {
1279
+ step -= 1;
1280
+ continue;
1281
+ }
1282
+ }
1283
+ else if (step === 3) {
1284
+ const template = fullCatalog.frameworkTemplates[framework];
1285
+ const value = await choose(`${messages[args.locale].framework} version`, Object.keys(template.validatedVersions), frameworkVersion, true);
1286
+ if (value === null) {
1287
+ step -= 1;
1288
+ continue;
1289
+ }
1290
+ frameworkVersion = value;
1291
+ }
1292
+ else if (step === 4) {
1293
+ const previousManager = packageManager;
1294
+ const value = await choose(messages[args.locale].packageManager, ['npm', 'pnpm', 'yarn', 'bun'], packageManager, true);
1295
+ if (value === null) {
1296
+ step -= 1;
1297
+ continue;
1298
+ }
1299
+ packageManager = value;
1300
+ if (packageManager !== previousManager)
1301
+ packageManagerVersion = packageManager === 'yarn' ? '4.9.2' : undefined;
1302
+ }
1303
+ else if (step === 5) {
1304
+ if (packageManager !== 'yarn') {
1305
+ step += 1;
1306
+ continue;
1307
+ }
1308
+ const value = await choose(messages[args.locale].packageManagerVersion, ['1.22.22', '4.9.2'], packageManagerVersion === '4.9.2' ? '4.9.2' : '1.22.22', true);
1309
+ if (value === null) {
1310
+ step -= 1;
1311
+ continue;
1312
+ }
1313
+ packageManagerVersion = value;
1314
+ }
1315
+ else if (step === 6) {
1316
+ const value = await askText(messages[args.locale].formats, formats);
1317
+ if (value === null) {
1318
+ step -= 1;
1319
+ continue;
1320
+ }
1321
+ formats = value === '-' ? '' : value;
1322
+ }
1323
+ else if (step === 7) {
1324
+ const value = await askText(messages[args.locale].capabilities, capabilities);
1325
+ if (value === null) {
1326
+ step -= 1;
1327
+ continue;
1328
+ }
1329
+ capabilities = value === '-' ? '' : value;
1330
+ }
1331
+ else if (step === 8) {
1332
+ const value = await askText(messages[args.locale].assetTarget, assetTarget, false);
1333
+ if (value === null) {
1334
+ step -= 1;
1335
+ continue;
1336
+ }
1337
+ assetTarget = value;
1338
+ }
1339
+ else if (step === 9) {
1340
+ const value = await choose(messages[args.locale].source, ['default', 'registry', 'offline'], sourceKind, true);
1341
+ if (value === null) {
1342
+ step -= 1;
1343
+ continue;
1344
+ }
1345
+ sourceKind = value;
1346
+ }
1347
+ else if (step === 10) {
1348
+ if (sourceKind === 'default') {
1349
+ step += 1;
1350
+ continue;
1351
+ }
1352
+ const label = sourceKind === 'registry'
1353
+ ? messages[args.locale].sourceRegistry
1354
+ : messages[args.locale].sourceOffline;
1355
+ const preferred = sourceKind === 'registry' ? registry : offlineDirectory;
1356
+ const value = await askText(label, preferred, false);
1357
+ if (value === null) {
1358
+ step -= 1;
1359
+ continue;
1360
+ }
1361
+ if (sourceKind === 'registry')
1362
+ registry = value;
1363
+ else
1364
+ offlineDirectory = value;
1365
+ }
1366
+ else if (step === 11) {
1367
+ const value = await choose(messages[args.locale].delivery, ['local', 'cdn'], delivery, true);
1368
+ if (value === null) {
1369
+ step = sourceKind === 'default' ? 9 : 10;
1370
+ continue;
1371
+ }
1372
+ delivery = value;
1373
+ }
1374
+ else if (step === 12) {
1375
+ if (delivery === 'local') {
1376
+ step += 1;
1377
+ continue;
1378
+ }
1379
+ const value = await askText(messages[args.locale].assetBaseUrl, assetBaseUrl, false);
1380
+ if (value === null) {
1381
+ step -= 1;
1382
+ continue;
1383
+ }
1384
+ assetBaseUrl = value;
1385
+ }
1386
+ else if (step === 13) {
1387
+ if (entries.length > 1) {
1388
+ const preferred = entry && entries.includes(entry) ? entry : entries[0];
1389
+ const value = await choose(messages[args.locale].entry, entries, preferred, true);
1390
+ if (value === null) {
1391
+ step = 11;
1392
+ continue;
1393
+ }
1394
+ entry = value;
1395
+ }
1396
+ else if (entries.length === 1)
1397
+ entry = entries[0];
1398
+ else if (existing) {
1399
+ const value = await askText(messages[args.locale].entry, entry ?? '', false);
1400
+ if (value === null) {
1401
+ step = 11;
1402
+ continue;
1403
+ }
1404
+ entry = value;
1405
+ }
1406
+ }
1407
+ step += 1;
1408
+ }
1409
+ args.framework = framework;
1410
+ args.profile = profile;
1411
+ args.frameworkVersion = frameworkVersion;
1412
+ args.packageManager = packageManager;
1413
+ args.packageManagerVersion = packageManagerVersion;
1414
+ args.formats = formats ? [formats] : [];
1415
+ args.capabilities = capabilities ? [capabilities] : [];
1416
+ args.assetTarget = assetTarget;
1417
+ args.assetTargetSpecified = true;
1418
+ args.sourceSpecified = true;
1419
+ args.registry = sourceKind === 'registry' ? registry : undefined;
1420
+ args.offlineDirectory = sourceKind === 'offline' ? offlineDirectory : undefined;
1421
+ args.assetBaseUrlSpecified = true;
1422
+ args.assetBaseUrl = delivery === 'cdn' ? assetBaseUrl : undefined;
1423
+ args.entry = entry;
1424
+ return args;
1425
+ }
1426
+ finally {
1427
+ io.close();
1428
+ }
1429
+ }
1430
+ async function confirmRenderedPlan(args, renderedPlan) {
1431
+ if (args.dryRun)
1432
+ return false;
1433
+ if (args.yes || args.nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY)
1434
+ return args.yes;
1435
+ process.stdout.write(`${renderedPlan}\n`);
1436
+ const io = createInterface({ input: process.stdin, output: process.stdout });
1437
+ try {
1438
+ const confirmation = await io.question(`${messages[args.locale].confirm} `);
1439
+ return /^(?:y|yes|是)$/i.test(confirmation.trim());
1440
+ }
1441
+ finally {
1442
+ io.close();
1443
+ }
1444
+ }
1445
+ const formatBytes = (value) => `${(value / 1024 / 1024).toFixed(1)} MiB`;
1446
+ const planLabels = {
1447
+ en: [
1448
+ 'Framework/profile',
1449
+ 'Pinned packages',
1450
+ 'Budget ceiling',
1451
+ 'Heavy capabilities',
1452
+ 'License notices',
1453
+ 'Missing asset owners',
1454
+ 'Generated module'
1455
+ ],
1456
+ 'zh-CN': [
1457
+ '框架/方案',
1458
+ '锁定版本包',
1459
+ '体积预算上限',
1460
+ '重型能力',
1461
+ '许可证声明',
1462
+ '缺失资产所有者',
1463
+ '生成模块'
1464
+ ],
1465
+ 'ja-JP': [
1466
+ 'フレームワーク/プロファイル',
1467
+ '固定パッケージ',
1468
+ 'サイズ上限',
1469
+ '重量機能',
1470
+ 'ライセンス通知',
1471
+ '不足アセット',
1472
+ '生成モジュール'
1473
+ ],
1474
+ 'de-DE': [
1475
+ 'Framework/Profil',
1476
+ 'Fixierte Pakete',
1477
+ 'Größenbudget',
1478
+ 'Schwere Fähigkeiten',
1479
+ 'Lizenzhinweise',
1480
+ 'Fehlende Asset-Pakete',
1481
+ 'Generiertes Modul'
1482
+ ]
1483
+ };
1484
+ const planTerms = {
1485
+ en: {
1486
+ packed: 'packed',
1487
+ unpacked: 'unpacked',
1488
+ staticAssets: 'static assets',
1489
+ fullHeavy: 'FULL IS HEAVY',
1490
+ packages: 'packages',
1491
+ measurement: 'official npm',
1492
+ none: 'none'
1493
+ },
1494
+ 'zh-CN': {
1495
+ packed: '压缩包',
1496
+ unpacked: '解压后',
1497
+ staticAssets: '静态资源',
1498
+ fullHeavy: 'FULL 方案体积较大',
1499
+ packages: '个包',
1500
+ measurement: '官方 npm 实测',
1501
+ none: '无'
1502
+ },
1503
+ 'ja-JP': {
1504
+ packed: '圧縮時',
1505
+ unpacked: '展開後',
1506
+ staticAssets: '静的アセット',
1507
+ fullHeavy: 'FULL は大容量です',
1508
+ packages: 'パッケージ',
1509
+ measurement: '公式 npm 測定',
1510
+ none: 'なし'
1511
+ },
1512
+ 'de-DE': {
1513
+ packed: 'gepackt',
1514
+ unpacked: 'entpackt',
1515
+ staticAssets: 'statische Assets',
1516
+ fullHeavy: 'FULL IST GROSS',
1517
+ packages: 'Pakete',
1518
+ measurement: 'offizielle npm-Messung',
1519
+ none: 'keine'
1520
+ }
1521
+ };
1522
+ const renderPlanSummary = (plan, locale) => {
1523
+ const labels = planLabels[locale];
1524
+ const terms = planTerms[locale];
1525
+ const lines = [
1526
+ `${labels[0]}: ${plan.framework} / ${plan.profile}`,
1527
+ `${labels[1]}: ${plan.packageSpecs.join(', ')}`
1528
+ ];
1529
+ if (plan.estimates) {
1530
+ lines.push(`${labels[2]}: ${formatBytes(plan.estimates.packedClosureBytes)} ${terms.packed}, ${formatBytes(plan.estimates.unpackedClosureBytes)} ${terms.unpacked}, ${formatBytes(plan.estimates.staticAssetBytes)} ${terms.staticAssets}.`);
1531
+ }
1532
+ if (plan.legacyFullEstimate) {
1533
+ const estimate = plan.legacyFullEstimate;
1534
+ lines.push(`${labels[2]}: ${terms.fullHeavy} — ${estimate.packageCountRange.join('–')} ${terms.packages}, ${formatBytes(estimate.packedBytesRange[0])}–${formatBytes(estimate.packedBytesRange[1])} ${terms.packed}, ${formatBytes(estimate.unpackedBytesRange[0])}–${formatBytes(estimate.unpackedBytesRange[1])} ${terms.unpacked} (${estimate.measuredAt}, ${terms.measurement}).`);
1535
+ }
1536
+ lines.push(`${labels[3]}: ${plan.heavyCapabilities.length ? `${plan.heavyCapabilities.join(', ')} (file-viewer list)` : terms.none}`);
1537
+ lines.push(`${labels[4]}: ${plan.licenseNotices.map((item) => `${item.packageName} (${item.spdx}, ${item.policy})`).join(', ') || terms.none}`);
1538
+ lines.push(`${labels[5]}: ${plan.missingAssetRendererIds.length ? plan.missingAssetRendererIds.join(', ') : terms.none}`);
1539
+ lines.push(`${labels[6]}: ${plan.generatedModule} (${plan.integrationImport})`);
1540
+ lines.push('', plan.command, ...plan.assetCommands);
1541
+ return lines.join('\n');
1542
+ };
1543
+ async function main() {
1544
+ let args = parseArgs(process.argv.slice(2));
1545
+ if (args.version) {
1546
+ print((await loadFileViewerCliCatalog()).core.version, false);
1547
+ return;
1548
+ }
1549
+ if (args.fileViewerVersion) {
1550
+ const catalogVersion = (await loadFileViewerCliCatalog()).core.version;
1551
+ if (args.fileViewerVersion !== catalogVersion) {
1552
+ throw new Error(`This installed CLI catalog is ${catalogVersion}; requested ${args.fileViewerVersion}. Run @file-viewer/cli@${args.fileViewerVersion} to use that release catalog.`);
1553
+ }
1554
+ }
1555
+ if (args.command === 'copy-assets') {
1556
+ if (args.dryRun) {
1557
+ print({ dryRun: true, mode: 'legacy-copy-assets', args: args.passthrough }, args.json);
1558
+ return;
1559
+ }
1560
+ const result = await executeLegacyCopyAssets(args);
1561
+ if (result.mode === 'help')
1562
+ print(result.usage, false);
1563
+ else if (result.mode === 'version')
1564
+ print(result.version, false);
1565
+ else
1566
+ print(result, args.json);
1567
+ return;
1568
+ }
1569
+ if (args.command === 'create' || args.command === 'add') {
1570
+ const existing = args.command === 'add';
1571
+ const projectExistedBefore = existsSync(args.projectRoot);
1572
+ const inspection = existing ? await inspectExistingProject(args) : null;
1573
+ if (inspection && !args.framework)
1574
+ args.framework = inspection.framework;
1575
+ if (inspection && !args.frameworkVersion)
1576
+ args.frameworkVersion = inspection.frameworkVersion;
1577
+ if (inspection?.detectedProfile && !args.profile)
1578
+ args.profile = inspection.detectedProfile;
1579
+ if (inspection?.packageManager && !args.packageManager)
1580
+ args.packageManager = inspection.packageManager;
1581
+ if (inspection?.packageManagerVersion && !args.packageManagerVersion)
1582
+ args.packageManagerVersion = inspection.packageManagerVersion;
1583
+ if (inspection?.assetTarget && !args.assetTarget)
1584
+ args.assetTarget = inspection.assetTarget;
1585
+ if (inspection?.entry && !args.entry)
1586
+ args.entry = inspection.entry;
1587
+ if (inspection?.assetBaseUrl && !args.assetBaseUrlSpecified)
1588
+ args.assetBaseUrl = inspection.assetBaseUrl;
1589
+ if (inspection?.source?.kind === 'registry' && !args.sourceSpecified)
1590
+ args.registry = inspection.source.registry;
1591
+ if (inspection?.source?.kind === 'offline-directory' && !args.sourceSpecified)
1592
+ args.offlineDirectory = inspection.source.directory;
1593
+ args = await promptQuickstart(args, existing, inspection);
1594
+ if (!args.assetBaseUrl && inspection?.adapter.publicDirectory && args.assetTarget) {
1595
+ args.assetBaseUrl = inferFileViewerLocalAssetBaseUrl(args.assetTarget, inspection.adapter.publicDirectory);
1596
+ }
1597
+ if (!existing && args.packageManager === 'yarn' && !args.packageManagerVersion)
1598
+ args.packageManagerVersion = '1.22.22';
1599
+ const config = await configFromArgs(args);
1600
+ const plan = await createFileViewerInstallPlan(config, {
1601
+ projectRoot: args.projectRoot,
1602
+ packageManager: args.packageManager,
1603
+ assetTarget: args.assetTarget
1604
+ });
1605
+ const renderedPlan = renderPlanSummary(plan, args.locale);
1606
+ const confirmed = await confirmRenderedPlan(args, renderedPlan);
1607
+ if (!confirmed) {
1608
+ const inspectionText = inspection
1609
+ ? [
1610
+ ...inspection.warnings,
1611
+ ...(inspection.adapter.manualSteps.length
1612
+ ? [`Manual steps:\n- ${inspection.adapter.manualSteps.join('\n- ')}`]
1613
+ : [])
1614
+ ].join('\n')
1615
+ : '';
1616
+ print(args.json
1617
+ ? { dryRun: true, inspection, config, plan }
1618
+ : `${inspectionText}${inspectionText ? '\n' : ''}${renderPlanSummary(plan, args.locale)}`, args.json);
1619
+ return;
1620
+ }
1621
+ if (inspection) {
1622
+ const adapter = args.assetTargetSpecified
1623
+ ? {
1624
+ ...inspection.adapter,
1625
+ assetTarget: config.assetTarget,
1626
+ manualSteps: [],
1627
+ safeAutomaticConfiguration: true,
1628
+ failClosed: false
1629
+ }
1630
+ : inspection.adapter;
1631
+ assertFileViewerProjectAdapterCanWrite(adapter);
1632
+ }
1633
+ const scaffoldPreview = existing
1634
+ ? null
1635
+ : await scaffoldFileViewerQuickstart(args.projectRoot, config, { force: args.force });
1636
+ const initializedPreview = await initializeFileViewerProject(args.projectRoot, config, {
1637
+ configFile: args.configFile,
1638
+ force: args.force
1639
+ });
1640
+ const generatedPreview = await generateFileViewerIntegrationModule(args.projectRoot, config, {
1641
+ output: args.output,
1642
+ force: args.force
1643
+ });
1644
+ if (existing)
1645
+ await patchFileViewerApplicationEntry(args.projectRoot, config.generatedModule, {
1646
+ entry: config.entry
1647
+ });
1648
+ const rollbackFiles = await snapshotProjectFiles([
1649
+ ...(scaffoldPreview?.files.map((file) => file.path) ?? []),
1650
+ initializedPreview.configPath,
1651
+ generatedPreview.outputPath,
1652
+ ...(config.entry ? [resolve(args.projectRoot, config.entry)] : []),
1653
+ ...[
1654
+ 'package.json',
1655
+ 'package-lock.json',
1656
+ 'npm-shrinkwrap.json',
1657
+ 'pnpm-lock.yaml',
1658
+ 'yarn.lock',
1659
+ 'bun.lock',
1660
+ 'bun.lockb',
1661
+ '.pnp.cjs',
1662
+ '.pnp.loader.mjs',
1663
+ '.pnp.data.json',
1664
+ '.yarnrc',
1665
+ '.yarnrc.yml',
1666
+ '.yarn/install-state.gz'
1667
+ ].map((path) => resolve(args.projectRoot, path))
1668
+ ]);
1669
+ const packageManagerArtifacts = [
1670
+ 'node_modules',
1671
+ 'package-lock.json',
1672
+ 'npm-shrinkwrap.json',
1673
+ 'pnpm-lock.yaml',
1674
+ 'yarn.lock',
1675
+ 'bun.lock',
1676
+ 'bun.lockb',
1677
+ '.pnp.cjs',
1678
+ '.pnp.loader.mjs',
1679
+ '.pnp.data.json',
1680
+ '.yarn',
1681
+ '.yarn/unplugged',
1682
+ config.assetTarget
1683
+ ].map((path) => resolve(args.projectRoot, path));
1684
+ const absentArtifacts = packageManagerArtifacts.filter((path) => !existsSync(path));
1685
+ const assetDirectoryBackup = await snapshotProjectDirectories([
1686
+ resolve(args.projectRoot, config.assetTarget)
1687
+ ]);
1688
+ let packageManagerDirectoryTransaction = null;
1689
+ try {
1690
+ const scaffold = existing
1691
+ ? null
1692
+ : await scaffoldFileViewerQuickstart(args.projectRoot, config, {
1693
+ write: true,
1694
+ force: args.force
1695
+ });
1696
+ const managedDependencies = await reconcileFileViewerManagedDependencies(args.projectRoot, config, plan, { write: true });
1697
+ const appliedConfig = managedDependencies.config;
1698
+ packageManagerDirectoryTransaction = await stageProjectDirectoriesAtomically(args.projectRoot, ['node_modules', '.yarn/unplugged'].map((path) => resolve(args.projectRoot, path)));
1699
+ const results = await executeStepsSequentially(plan.steps, true);
1700
+ const removedAssetOwners = config.profile === 'full'
1701
+ ? []
1702
+ : await reconcileFileViewerAssetOwners(args.projectRoot, plan);
1703
+ const initialized = await initializeFileViewerProject(args.projectRoot, appliedConfig, {
1704
+ configFile: args.configFile,
1705
+ write: true,
1706
+ force: args.force
1707
+ });
1708
+ const generated = await generateFileViewerIntegrationModule(args.projectRoot, appliedConfig, {
1709
+ write: true,
1710
+ output: args.output,
1711
+ force: args.force
1712
+ });
1713
+ const entryIntegration = await patchFileViewerApplicationEntry(args.projectRoot, appliedConfig.generatedModule, { write: true, entry: appliedConfig.entry });
1714
+ await discardAtomicProjectDirectoryBackup(packageManagerDirectoryTransaction);
1715
+ packageManagerDirectoryTransaction = null;
1716
+ print({
1717
+ dryRun: false,
1718
+ detectedExistingProject: existing,
1719
+ inspection,
1720
+ scaffoldPreview,
1721
+ scaffold,
1722
+ initialized,
1723
+ managedDependencies,
1724
+ plan,
1725
+ results,
1726
+ removedAssetOwners,
1727
+ generated,
1728
+ entryIntegration
1729
+ }, args.json);
1730
+ }
1731
+ catch (error) {
1732
+ // A brand-new target is wholly CLI-owned. Remove a failed scaffold so a
1733
+ // package-manager or asset error never leaves a misleading half-project.
1734
+ // Existing directories are never recursively removed.
1735
+ if (!existing && !projectExistedBefore) {
1736
+ await rm(args.projectRoot, { recursive: true, force: true });
1737
+ }
1738
+ else {
1739
+ await rollbackAtomicProjectDirectories(packageManagerDirectoryTransaction);
1740
+ await restoreProjectFiles(args.projectRoot, rollbackFiles, absentArtifacts);
1741
+ await restoreProjectDirectories(assetDirectoryBackup.backups);
1742
+ }
1743
+ throw error;
1744
+ }
1745
+ finally {
1746
+ await rm(assetDirectoryBackup.temporary, { recursive: true, force: true });
1747
+ }
1748
+ return;
1749
+ }
1750
+ if (args.command === 'list' || args.command === 'capabilities') {
1751
+ const capabilities = await listFileViewerCapabilities();
1752
+ print(args.json ? capabilities : renderFileViewerCapabilityList(capabilities), args.json);
1753
+ return;
1754
+ }
1755
+ if (args.command === 'plan') {
1756
+ const plan = await createFileViewerInstallPlan(await configFromArgs(args), {
1757
+ projectRoot: args.projectRoot,
1758
+ packageManager: args.packageManager,
1759
+ assetTarget: args.assetTarget
1760
+ });
1761
+ print(args.json ? plan : renderPlanSummary(plan, args.locale), args.json);
1762
+ return;
1763
+ }
1764
+ if (args.command === 'init') {
1765
+ const result = await initializeFileViewerProject(args.projectRoot, await configFromArgs(args), {
1766
+ configFile: args.configFile,
1767
+ write: args.write && !args.dryRun,
1768
+ force: args.force
1769
+ });
1770
+ print(result, args.json);
1771
+ return;
1772
+ }
1773
+ if (args.command === 'config-add' ||
1774
+ args.command === 'config-remove' ||
1775
+ args.command === 'select' ||
1776
+ args.command === 'remove') {
1777
+ if (!args.token)
1778
+ throw new Error(`${args.command} requires a format or capability token.`);
1779
+ const result = await updateFileViewerProjectSelection(args.projectRoot, args.token, args.command === 'config-remove' || args.command === 'remove' ? 'remove' : 'add', { configFile: args.configFile, write: args.write && !args.dryRun });
1780
+ print({ dryRun: !result.written, ...result }, args.json);
1781
+ return;
1782
+ }
1783
+ if (args.command === 'generate') {
1784
+ const result = await generateFileViewerIntegrationModule(args.projectRoot, await configFromArgs(args), {
1785
+ write: args.write && !args.dryRun,
1786
+ output: args.output,
1787
+ force: args.force
1788
+ });
1789
+ print(args.json
1790
+ ? result
1791
+ : args.write && !args.dryRun
1792
+ ? `${result.written ? 'Wrote' : 'Kept'} ${result.outputPath}.\nAdd this to your application entry: ${result.importStatement}`
1793
+ : result.content, args.json);
1794
+ return;
1795
+ }
1796
+ if (args.command === 'install' || args.command === 'assets') {
1797
+ if (args.command === 'assets') {
1798
+ const configured = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
1799
+ const modularSelectionRequested = Boolean(args.framework || args.profile || args.formats.length || args.capabilities.length);
1800
+ if (!configured && !modularSelectionRequested) {
1801
+ if (args.dryRun) {
1802
+ print({ dryRun: true, mode: 'legacy-copy-assets', args: args.passthrough }, args.json);
1803
+ return;
1804
+ }
1805
+ if (args.positionals[0])
1806
+ args.passthrough.unshift(args.positionals[0]);
1807
+ else if (args.assetTarget)
1808
+ args.passthrough.unshift(args.assetTarget);
1809
+ const legacyResult = await executeLegacyCopyAssets(args);
1810
+ if (legacyResult.mode === 'help')
1811
+ print(legacyResult.usage, false);
1812
+ else if (legacyResult.mode === 'version')
1813
+ print(legacyResult.version, false);
1814
+ else
1815
+ print(legacyResult, args.json);
1816
+ return;
1817
+ }
1818
+ if (args.passthrough.length || args.positionals.length)
1819
+ throw new Error('Legacy copy-assets flags cannot be mixed with a configured modular asset plan.');
1820
+ }
1821
+ const config = await configFromArgs(args);
1822
+ const plan = await createFileViewerInstallPlan(config, {
1823
+ projectRoot: args.projectRoot,
1824
+ packageManager: args.packageManager,
1825
+ assetTarget: args.assetTarget
1826
+ });
1827
+ const confirmed = !args.dryRun && (args.command === 'install' ? args.yes : args.write);
1828
+ const steps = args.command === 'install'
1829
+ ? plan.steps
1830
+ : plan.steps.filter((candidate) => candidate.kind === 'assets');
1831
+ const transactionFiles = confirmed
1832
+ ? await snapshotProjectFiles([
1833
+ 'package.json',
1834
+ 'package-lock.json',
1835
+ 'npm-shrinkwrap.json',
1836
+ 'pnpm-lock.yaml',
1837
+ 'yarn.lock',
1838
+ 'bun.lock',
1839
+ 'bun.lockb',
1840
+ '.pnp.cjs',
1841
+ '.pnp.loader.mjs',
1842
+ '.pnp.data.json',
1843
+ '.yarnrc',
1844
+ '.yarnrc.yml',
1845
+ '.yarn/install-state.gz',
1846
+ args.configFile,
1847
+ config.generatedModule
1848
+ ].map((path) => resolve(args.projectRoot, path)))
1849
+ : [];
1850
+ const absentArtifacts = confirmed
1851
+ ? [
1852
+ 'node_modules',
1853
+ 'package-lock.json',
1854
+ 'npm-shrinkwrap.json',
1855
+ 'pnpm-lock.yaml',
1856
+ 'yarn.lock',
1857
+ 'bun.lock',
1858
+ 'bun.lockb',
1859
+ '.pnp.cjs',
1860
+ '.pnp.loader.mjs',
1861
+ '.pnp.data.json',
1862
+ '.yarn',
1863
+ '.yarn/unplugged',
1864
+ config.generatedModule
1865
+ ]
1866
+ .map((path) => resolve(args.projectRoot, path))
1867
+ .filter((path) => !existsSync(path))
1868
+ : [];
1869
+ const assetDirectoryBackup = confirmed
1870
+ ? await snapshotProjectDirectories([resolve(args.projectRoot, config.assetTarget)])
1871
+ : null;
1872
+ let packageManagerDirectoryTransaction = null;
1873
+ try {
1874
+ const managedDependencies = confirmed && args.command === 'install'
1875
+ ? await reconcileFileViewerManagedDependencies(args.projectRoot, config, plan, {
1876
+ write: true
1877
+ })
1878
+ : null;
1879
+ const appliedConfig = managedDependencies?.config ?? config;
1880
+ if (confirmed && args.command === 'install')
1881
+ packageManagerDirectoryTransaction = await stageProjectDirectoriesAtomically(args.projectRoot, ['node_modules', '.yarn/unplugged'].map((path) => resolve(args.projectRoot, path)));
1882
+ const results = await executeStepsSequentially(steps, confirmed);
1883
+ const removedAssetOwners = confirmed && config.profile !== 'full'
1884
+ ? await reconcileFileViewerAssetOwners(args.projectRoot, plan)
1885
+ : [];
1886
+ const initialized = confirmed && args.command === 'install'
1887
+ ? await initializeFileViewerProject(args.projectRoot, appliedConfig, {
1888
+ configFile: args.configFile,
1889
+ write: true,
1890
+ force: args.force
1891
+ })
1892
+ : null;
1893
+ const generated = await generateFileViewerIntegrationModule(args.projectRoot, appliedConfig, {
1894
+ write: confirmed && args.command === 'install',
1895
+ output: args.output,
1896
+ force: args.force
1897
+ });
1898
+ await discardAtomicProjectDirectoryBackup(packageManagerDirectoryTransaction);
1899
+ packageManagerDirectoryTransaction = null;
1900
+ print({
1901
+ dryRun: !confirmed,
1902
+ plan,
1903
+ results,
1904
+ removedAssetOwners,
1905
+ managedDependencies,
1906
+ initialized,
1907
+ generated
1908
+ }, args.json);
1909
+ }
1910
+ catch (error) {
1911
+ if (confirmed) {
1912
+ await rollbackAtomicProjectDirectories(packageManagerDirectoryTransaction);
1913
+ await restoreProjectFiles(args.projectRoot, transactionFiles, absentArtifacts);
1914
+ if (assetDirectoryBackup)
1915
+ await restoreProjectDirectories(assetDirectoryBackup.backups);
1916
+ }
1917
+ throw error;
1918
+ }
1919
+ finally {
1920
+ if (assetDirectoryBackup)
1921
+ await rm(assetDirectoryBackup.temporary, { recursive: true, force: true });
1922
+ }
1923
+ return;
1924
+ }
1925
+ if (args.command === 'prepare' || args.command === 'cache') {
1926
+ if (args.concurrency !== undefined &&
1927
+ (!Number.isInteger(args.concurrency) || args.concurrency < 1 || args.concurrency > 8)) {
1928
+ throw new Error('prepare --concurrency must be an integer from 1 to 8. Install commands continue to support 1 to 32.');
1929
+ }
1930
+ const existing = await readFileViewerProjectConfig(args.projectRoot, args.configFile, true);
1931
+ const registryRaw = args.registry ??
1932
+ (existing?.source?.kind === 'registry' ? existing.source.registry : undefined);
1933
+ if (!registryRaw)
1934
+ throw new Error('prepare requires an explicit --registry URL or a configured registry source.');
1935
+ const registry = normalizeFileViewerRegistryUrl(registryRaw);
1936
+ const planConfig = normalizeFileViewerConfig({
1937
+ ...(existing ?? {}),
1938
+ ...(args.framework ? { framework: args.framework } : {}),
1939
+ ...(args.profile ? { profile: args.profile } : {}),
1940
+ formats: args.formats.length ? args.formats : (existing?.formats ?? []),
1941
+ capabilities: args.capabilities.length ? args.capabilities : (existing?.capabilities ?? []),
1942
+ frameworkVersion: args.frameworkVersion ?? existing?.frameworkVersion,
1943
+ assetBaseUrl: args.assetBaseUrl ?? existing?.assetBaseUrl,
1944
+ source: { kind: 'registry', registry }
1945
+ });
1946
+ const installPlan = await createFileViewerInstallPlan(planConfig, {
1947
+ projectRoot: args.projectRoot,
1948
+ packageManager: args.packageManager,
1949
+ assetTarget: args.assetTarget
1950
+ });
1951
+ const catalogVersion = (await loadFileViewerCliCatalog()).core.version;
1952
+ const requiredPackages = [
1953
+ ...installPlan.requiredPackages,
1954
+ { packageName: '@file-viewer/cli', version: catalogVersion },
1955
+ { packageName: 'create-file-viewer', version: catalogVersion },
1956
+ { packageName: 'file-viewer-cli', version: catalogVersion }
1957
+ ].filter((item, index, values) => values.findIndex((candidate) => candidate.packageName === item.packageName) === index);
1958
+ const plan = { ...installPlan, requiredPackages };
1959
+ const directory = args.offlineDirectory ?? `.file-viewer/offline/${catalogVersion}`;
1960
+ if (!args.yes || args.dryRun) {
1961
+ print(args.json
1962
+ ? { dryRun: true, directory: resolve(args.projectRoot, directory), registry, plan }
1963
+ : `${renderPlanSummary(plan, args.locale)}\n\nOffline directory: ${resolve(args.projectRoot, directory)}\nUse --yes to download atomically.`, args.json);
1964
+ return;
1965
+ }
1966
+ const result = await prepareFileViewerOfflineDirectory(plan.requiredPackages, {
1967
+ projectRoot: args.projectRoot,
1968
+ directory,
1969
+ registry,
1970
+ concurrency: args.concurrency
1971
+ });
1972
+ print(result, args.json);
1973
+ return;
1974
+ }
1975
+ if (args.command === 'doctor' || args.command === 'verify') {
1976
+ const result = await doctorFileViewerProject(args.projectRoot, args.configFile, args.packageManager);
1977
+ print(result, args.json);
1978
+ if (args.command === 'verify' && !result.ok)
1979
+ process.exitCode = 1;
1980
+ return;
1981
+ }
1982
+ throw new Error(`Unknown command ${args.command}.\n\n${localizedUsage[args.locale]}`);
1983
+ }
1984
+ main().catch((error) => {
1985
+ if (Object.values(messages).some((message) => message.cancelled === error.message)) {
1986
+ process.stdout.write(`${error.message}\n`);
1987
+ process.exitCode = 0;
1988
+ return;
1989
+ }
1990
+ process.stderr.write(`[file-viewer] ${error.message}\n`);
1991
+ process.exitCode = 1;
1992
+ });