From 468369c3744b30f7a3cd083e8406d27bea4f92a1 Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:31:08 -0400 Subject: [PATCH 1/8] Finish movez rename and fix broken CLI flows - Rename contextbridge/ to movez/ and the entry script to movez, matching composer.json bin, box.json, the spec and the release workflow - export writes real .cbz bundles (optionally encrypted); import, unpack and package handle encrypted and invalid bundles without crashing - --project now filters sessions on export, transfer, list and sync:push - Sync client uses Guzzle (illuminate/http was never installed), sends { sessions, count }, handles empty pulls, reads MOVEZ_TOKEN / MOVEZ_SERVER_URL; sync:pull accepts --from-path/--to-path - Encryptor no longer creates a key on decrypt and validates the key file - Reject unsafe session ids / project names before building file paths - Codex: read and write the dated rollout format; Cline: read per-task history files; flatten block content in all parsers; skip malformed lines - Windsurf reports "not supported yet" instead of returning Cursor sessions - ClaudeCodeWriter: keep POSIX paths, no duplicate desktop registry entries - Remove Laravel Zero boilerplate, add strict_types, ext-* requirements - Tests: stop touching the real Cursor DB, test the real ClaudeCodeWriter, add coverage for sync, encryption, bundles, parsers and commands Co-Authored-By: Claude Opus 5.5 --- contextbridge/app/Commands/DoctorCommand.php | 73 -------- contextbridge/app/Commands/ExportCommand.php | 65 ------- contextbridge/app/Commands/InspireCommand.php | 48 ----- .../app/Commands/Sync/SyncPullCommand.php | 70 -------- .../app/Commands/Sync/SyncPushCommand.php | 71 -------- contextbridge/app/Parsers/ClineParser.php | 87 --------- contextbridge/app/Parsers/CodexParser.php | 92 ---------- contextbridge/app/Parsers/WindsurfParser.php | 12 -- contextbridge/app/Services/Packager.php | 94 ---------- contextbridge/app/Services/SyncClient.php | 70 -------- contextbridge/app/Writers/CodexWriter.php | 45 ----- contextbridge/app/Writers/WindsurfWriter.php | 12 -- contextbridge/composer.bat | 2 - .../tests/Feature/InspireCommandTest.php | 5 - contextbridge/tests/Unit/ExampleTest.php | 5 - .../Unit/Writers/ClaudeCodeWriterTest.php | 78 -------- {contextbridge => movez}/.editorconfig | 0 {contextbridge => movez}/.gitattributes | 0 {contextbridge => movez}/.gitignore | 0 {contextbridge => movez}/README.md | 0 movez/app/Commands/DoctorCommand.php | 74 ++++++++ movez/app/Commands/ExportCommand.php | 80 +++++++++ .../app/Commands/ImportCommand.php | 74 ++++---- .../app/Commands/ListCommand.php | 10 +- .../app/Commands/PackageCommand.php | 24 +-- .../Commands/Sync/ResolvesSyncCredentials.php | 49 +++++ movez/app/Commands/Sync/SyncPullCommand.php | 68 +++++++ movez/app/Commands/Sync/SyncPushCommand.php | 58 ++++++ .../app/Commands/TransferCommand.php | 33 ++-- .../app/Commands/UnpackCommand.php | 28 +-- .../app/Contracts/ParserInterface.php | 0 .../app/Contracts/WriterInterface.php | 0 .../app/DTOs/FileDiffDTO.php | 0 .../app/DTOs/ProjectConfigDTO.php | 0 .../app/DTOs/SessionDTO.php | 25 ++- {contextbridge => movez}/app/DTOs/TurnDTO.php | 7 +- .../app/Parsers/ClaudeCodeParser.php | 73 ++++---- movez/app/Parsers/ClineParser.php | 119 ++++++++++++ movez/app/Parsers/CodexParser.php | 170 ++++++++++++++++++ .../app/Parsers/ContinueParser.php | 5 +- .../app/Parsers/CopilotCliParser.php | 26 +-- .../app/Parsers/CursorParser.php | 18 +- .../app/Parsers/JsonlParser.php | 18 +- .../app/Parsers/SqliteParser.php | 0 movez/app/Parsers/WindsurfParser.php | 48 +++++ .../app/Providers/AppServiceProvider.php | 0 .../app/Services/Encryptor.php | 44 ++++- movez/app/Services/Packager.php | 114 ++++++++++++ .../app/Services/PathMapper.php | 0 movez/app/Services/SyncClient.php | 102 +++++++++++ .../app/Services/ToolDetector.php | 13 +- .../app/Support/BundleSchema.php | 20 ++- movez/app/Support/ContentFlattener.php | 35 ++++ .../app/Support/PlatformPaths.php | 0 movez/app/Support/ProjectFilter.php | 43 +++++ movez/app/Support/SafePath.php | 43 +++++ .../app/Writers/AbstractWriter.php | 0 .../app/Writers/ClaudeCodeWriter.php | 71 +++++--- movez/app/Writers/CodexWriter.php | 78 ++++++++ .../app/Writers/CopilotCliWriter.php | 6 +- .../app/Writers/CursorWriter.php | 9 +- movez/app/Writers/WindsurfWriter.php | 27 +++ {contextbridge => movez}/bootstrap/app.php | 2 + .../bootstrap/providers.php | 2 + {contextbridge => movez}/box.bat | 0 {contextbridge => movez}/box.json | 2 + movez/composer.bat | 3 + {contextbridge => movez}/composer.json | 10 +- {contextbridge => movez}/composer.lock | 2 +- {contextbridge => movez}/config/app.php | 7 +- {contextbridge => movez}/config/commands.php | 2 + {contextbridge => movez}/config/movez.php | 40 ++++- contextbridge/contextbridge => movez/movez | 2 + {contextbridge => movez}/phpunit.xml.dist | 0 .../Commands/ExportImportCommandTest.php | 110 ++++++++++++ .../Feature/Writers/CursorRoundTripTest.php | 5 + {contextbridge => movez}/tests/Pest.php | 2 +- {contextbridge => movez}/tests/TestCase.php | 2 + .../tests/Unit/DTOs/FileDiffDTOTest.php | 0 .../tests/Unit/DTOs/ProjectConfigDTOTest.php | 0 .../tests/Unit/DTOs/SessionDTOTest.php | 0 .../tests/Unit/DTOs/TurnDTOTest.php | 0 .../Unit/Parsers/ClaudeCodeParserTest.php | 0 .../tests/Unit/Parsers/CursorParserTest.php | 0 movez/tests/Unit/Parsers/OtherParsersTest.php | 125 +++++++++++++ .../tests/Unit/Services/EncryptorTest.php | 18 ++ .../tests/Unit/Services/PackagerTest.php | 34 ++++ .../tests/Unit/Services/PathMapperTest.php | 0 movez/tests/Unit/Services/SyncClientTest.php | 80 +++++++++ .../tests/Unit/Support/BundleSchemaTest.php | 16 +- .../tests/Unit/Support/PlatformPathsTest.php | 18 +- .../tests/Unit/Support/SafetyHelpersTest.php | 51 ++++++ .../Unit/Writers/ClaudeCodeWriterTest.php | 123 +++++++++++++ .../tests/Unit/Writers/CursorWriterTest.php | 4 +- 94 files changed, 2033 insertions(+), 1068 deletions(-) delete mode 100644 contextbridge/app/Commands/DoctorCommand.php delete mode 100644 contextbridge/app/Commands/ExportCommand.php delete mode 100644 contextbridge/app/Commands/InspireCommand.php delete mode 100644 contextbridge/app/Commands/Sync/SyncPullCommand.php delete mode 100644 contextbridge/app/Commands/Sync/SyncPushCommand.php delete mode 100644 contextbridge/app/Parsers/ClineParser.php delete mode 100644 contextbridge/app/Parsers/CodexParser.php delete mode 100644 contextbridge/app/Parsers/WindsurfParser.php delete mode 100644 contextbridge/app/Services/Packager.php delete mode 100644 contextbridge/app/Services/SyncClient.php delete mode 100644 contextbridge/app/Writers/CodexWriter.php delete mode 100644 contextbridge/app/Writers/WindsurfWriter.php delete mode 100644 contextbridge/composer.bat delete mode 100644 contextbridge/tests/Feature/InspireCommandTest.php delete mode 100644 contextbridge/tests/Unit/ExampleTest.php delete mode 100644 contextbridge/tests/Unit/Writers/ClaudeCodeWriterTest.php rename {contextbridge => movez}/.editorconfig (100%) rename {contextbridge => movez}/.gitattributes (100%) rename {contextbridge => movez}/.gitignore (100%) rename {contextbridge => movez}/README.md (100%) create mode 100644 movez/app/Commands/DoctorCommand.php create mode 100644 movez/app/Commands/ExportCommand.php rename {contextbridge => movez}/app/Commands/ImportCommand.php (55%) rename {contextbridge => movez}/app/Commands/ListCommand.php (85%) rename {contextbridge => movez}/app/Commands/PackageCommand.php (53%) create mode 100644 movez/app/Commands/Sync/ResolvesSyncCredentials.php create mode 100644 movez/app/Commands/Sync/SyncPullCommand.php create mode 100644 movez/app/Commands/Sync/SyncPushCommand.php rename {contextbridge => movez}/app/Commands/TransferCommand.php (58%) rename {contextbridge => movez}/app/Commands/UnpackCommand.php (57%) rename {contextbridge => movez}/app/Contracts/ParserInterface.php (100%) rename {contextbridge => movez}/app/Contracts/WriterInterface.php (100%) rename {contextbridge => movez}/app/DTOs/FileDiffDTO.php (100%) rename {contextbridge => movez}/app/DTOs/ProjectConfigDTO.php (100%) rename {contextbridge => movez}/app/DTOs/SessionDTO.php (59%) rename {contextbridge => movez}/app/DTOs/TurnDTO.php (86%) rename {contextbridge => movez}/app/Parsers/ClaudeCodeParser.php (79%) create mode 100644 movez/app/Parsers/ClineParser.php create mode 100644 movez/app/Parsers/CodexParser.php rename {contextbridge => movez}/app/Parsers/ContinueParser.php (93%) rename {contextbridge => movez}/app/Parsers/CopilotCliParser.php (71%) rename {contextbridge => movez}/app/Parsers/CursorParser.php (93%) rename {contextbridge => movez}/app/Parsers/JsonlParser.php (71%) rename {contextbridge => movez}/app/Parsers/SqliteParser.php (100%) create mode 100644 movez/app/Parsers/WindsurfParser.php rename {contextbridge => movez}/app/Providers/AppServiceProvider.php (100%) rename {contextbridge => movez}/app/Services/Encryptor.php (53%) create mode 100644 movez/app/Services/Packager.php rename {contextbridge => movez}/app/Services/PathMapper.php (100%) create mode 100644 movez/app/Services/SyncClient.php rename {contextbridge => movez}/app/Services/ToolDetector.php (86%) rename {contextbridge => movez}/app/Support/BundleSchema.php (64%) create mode 100644 movez/app/Support/ContentFlattener.php rename {contextbridge => movez}/app/Support/PlatformPaths.php (100%) create mode 100644 movez/app/Support/ProjectFilter.php create mode 100644 movez/app/Support/SafePath.php rename {contextbridge => movez}/app/Writers/AbstractWriter.php (100%) rename {contextbridge => movez}/app/Writers/ClaudeCodeWriter.php (79%) create mode 100644 movez/app/Writers/CodexWriter.php rename {contextbridge => movez}/app/Writers/CopilotCliWriter.php (87%) rename {contextbridge => movez}/app/Writers/CursorWriter.php (99%) create mode 100644 movez/app/Writers/WindsurfWriter.php rename {contextbridge => movez}/bootstrap/app.php (81%) rename {contextbridge => movez}/bootstrap/providers.php (71%) rename {contextbridge => movez}/box.bat (100%) rename {contextbridge => movez}/box.json (87%) create mode 100644 movez/composer.bat rename {contextbridge => movez}/composer.json (86%) rename {contextbridge => movez}/composer.lock (99%) rename {contextbridge => movez}/config/app.php (94%) rename {contextbridge => movez}/config/commands.php (99%) rename {contextbridge => movez}/config/movez.php (62%) rename contextbridge/contextbridge => movez/movez (98%) rename {contextbridge => movez}/phpunit.xml.dist (100%) create mode 100644 movez/tests/Feature/Commands/ExportImportCommandTest.php rename {contextbridge => movez}/tests/Feature/Writers/CursorRoundTripTest.php (88%) rename {contextbridge => movez}/tests/Pest.php (95%) rename {contextbridge => movez}/tests/TestCase.php (83%) rename {contextbridge => movez}/tests/Unit/DTOs/FileDiffDTOTest.php (100%) rename {contextbridge => movez}/tests/Unit/DTOs/ProjectConfigDTOTest.php (100%) rename {contextbridge => movez}/tests/Unit/DTOs/SessionDTOTest.php (100%) rename {contextbridge => movez}/tests/Unit/DTOs/TurnDTOTest.php (100%) rename {contextbridge => movez}/tests/Unit/Parsers/ClaudeCodeParserTest.php (100%) rename {contextbridge => movez}/tests/Unit/Parsers/CursorParserTest.php (100%) create mode 100644 movez/tests/Unit/Parsers/OtherParsersTest.php rename {contextbridge => movez}/tests/Unit/Services/EncryptorTest.php (73%) rename {contextbridge => movez}/tests/Unit/Services/PackagerTest.php (67%) rename {contextbridge => movez}/tests/Unit/Services/PathMapperTest.php (100%) create mode 100644 movez/tests/Unit/Services/SyncClientTest.php rename {contextbridge => movez}/tests/Unit/Support/BundleSchemaTest.php (79%) rename {contextbridge => movez}/tests/Unit/Support/PlatformPathsTest.php (80%) create mode 100644 movez/tests/Unit/Support/SafetyHelpersTest.php create mode 100644 movez/tests/Unit/Writers/ClaudeCodeWriterTest.php rename {contextbridge => movez}/tests/Unit/Writers/CursorWriterTest.php (95%) diff --git a/contextbridge/app/Commands/DoctorCommand.php b/contextbridge/app/Commands/DoctorCommand.php deleted file mode 100644 index 4a62d82..0000000 --- a/contextbridge/app/Commands/DoctorCommand.php +++ /dev/null @@ -1,73 +0,0 @@ -info('MoveZ Doctor'); - $this->line(''); - - $passed = true; - - // PHP version - $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - $phpOk = PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 2; - $this->checkResult($phpOk, "PHP >= 8.2 (found {$phpVersion})"); - $passed = $passed && $phpOk; - - // OpenSSL extension - $sslOk = extension_loaded('openssl'); - $this->checkResult($sslOk, 'OpenSSL extension loaded'); - $passed = $passed && $sslOk; - - // PDO SQLite - $sqliteOk = extension_loaded('pdo_sqlite'); - $this->checkResult($sqliteOk, 'PDO SQLite driver loaded'); - $passed = $passed && $sqliteOk; - - // ZipArchive - $zipOk = class_exists('ZipArchive'); - $this->checkResult($zipOk, 'ZipArchive class available'); - $passed = $passed && $zipOk; - - // Key path readable/writable - $keyPath = config('movez.key_path'); - $keyDirOk = is_dir(dirname((string) $keyPath)) || is_writable(dirname(dirname((string) $keyPath))); - $this->checkResult($keyDirOk, 'Encryption key directory accessible'); - $passed = $passed && $keyDirOk; - - // Detected tools - $this->line(''); - $detected = $detector->detect(); - if (!empty($detected)) { - $this->info('Detected AI tools: ' . implode(', ', $detected)); - } else { - $this->warn('No AI tools detected on this machine'); - } - - $this->line(''); - if ($passed) { - $this->info('All checks passed!'); - } else { - $this->error('Some checks failed. See above for details.'); - } - - return $passed ? self::SUCCESS : self::FAILURE; - } - - private function checkResult(bool $ok, string $label): void - { - $icon = $ok ? '✔' : '✘'; - $this->line(" {$icon} {$label}"); - } -} diff --git a/contextbridge/app/Commands/ExportCommand.php b/contextbridge/app/Commands/ExportCommand.php deleted file mode 100644 index f7bbcf9..0000000 --- a/contextbridge/app/Commands/ExportCommand.php +++ /dev/null @@ -1,65 +0,0 @@ -option('tool'); - $outputPath = $this->option('output') ?? ('movez-export-' . date('Ymd-His') . '.json'); - $projectPath = $this->option('project') ?? getcwd(); - $encrypt = (bool) $this->option('encrypt'); - - if ($toolName === 'auto') { - $detected = $detector->detect(); - if (empty($detected)) { - $this->error('No AI coding tools detected on this machine.'); - return self::FAILURE; - } - $toolName = $detected[0]; - $this->info("Auto-detected tool: {$toolName}"); - } - - try { - $parser = $detector->getParser($toolName); - $sessions = $parser->parse((string) $projectPath); - } catch (\InvalidArgumentException $e) { - $this->error($e->getMessage()); - return self::FAILURE; - } - - if ($sessions->isEmpty()) { - $this->warn('No sessions found for the given tool and project.'); - return self::SUCCESS; - } - - $bundle = json_encode( - $sessions->map(fn($s) => $s->toArray())->values()->all(), - JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE - ); - - if ($encrypt) { - $bundle = $encryptor->encrypt((string) $bundle); - $outputPath = str_replace('.json', '.enc.json', $outputPath); - } - - file_put_contents($outputPath, $bundle); - - $this->info("Exported {$sessions->count()} session(s) to: {$outputPath}"); - return self::SUCCESS; - } -} diff --git a/contextbridge/app/Commands/InspireCommand.php b/contextbridge/app/Commands/InspireCommand.php deleted file mode 100644 index 934fe72..0000000 --- a/contextbridge/app/Commands/InspireCommand.php +++ /dev/null @@ -1,48 +0,0 @@ - -
Laravel Zero
- - Simplicity is the ultimate sophistication. - - - HTML); - } - - /** - * Define the command's schedule. - */ - public function schedule(Schedule $schedule): void - { - // $schedule->command(static::class)->everyMinute(); - } -} diff --git a/contextbridge/app/Commands/Sync/SyncPullCommand.php b/contextbridge/app/Commands/Sync/SyncPullCommand.php deleted file mode 100644 index ee0c473..0000000 --- a/contextbridge/app/Commands/Sync/SyncPullCommand.php +++ /dev/null @@ -1,70 +0,0 @@ -resolveToken(); - $baseUrl = $this->option('server') ?? env('CB_SERVER_URL', ''); - $toolName = $this->option('tool'); - $projectPath = $this->option('project') ?? getcwd(); - - if (!$token) { - $this->error('No token found. Use --token or store in ~/.movez/token'); - return self::FAILURE; - } - - if (!$baseUrl) { - $this->error('No server URL. Use --server or set CB_SERVER_URL env var'); - return self::FAILURE; - } - - $client = new SyncClient($baseUrl, $encryptor); - $sessions = $client->pull($token); - - if ($toolName) { - $writer = $detector->getWriter($toolName); - $writer->write($sessions, (string) $projectPath); - $this->info("Pulled and imported {$sessions->count()} session(s) into {$toolName}"); - } else { - // Just output as JSON - $this->line(json_encode( - $sessions->map(fn($s) => $s->toArray())->all(), - JSON_PRETTY_PRINT - )); - } - - return self::SUCCESS; - } - - private function resolveToken(): ?string - { - $token = $this->option('token'); - if ($token) { - return $token; - } - - $tokenPath = config('movez.token_path'); - if ($tokenPath && file_exists($tokenPath)) { - return trim((string) file_get_contents($tokenPath)); - } - - return null; - } -} diff --git a/contextbridge/app/Commands/Sync/SyncPushCommand.php b/contextbridge/app/Commands/Sync/SyncPushCommand.php deleted file mode 100644 index 6ea4458..0000000 --- a/contextbridge/app/Commands/Sync/SyncPushCommand.php +++ /dev/null @@ -1,71 +0,0 @@ -resolveToken(); - $baseUrl = $this->option('server') ?? env('CB_SERVER_URL', ''); - $toolName = $this->option('tool'); - $projectPath = $this->option('project') ?? getcwd(); - - if (!$token) { - $this->error('No token found. Use --token or store in ~/.movez/token'); - return self::FAILURE; - } - - if (!$baseUrl) { - $this->error('No server URL. Use --server or set CB_SERVER_URL env var'); - return self::FAILURE; - } - - if ($toolName === 'auto') { - $detected = $detector->detect(); - if (empty($detected)) { - $this->error('No AI coding tools detected'); - return self::FAILURE; - } - $toolName = $detected[0]; - } - - $sessions = $detector->getParser($toolName)->parse((string) $projectPath); - - $client = new SyncClient($baseUrl, $encryptor); - $client->push($sessions, $token); - - $this->info("Pushed {$sessions->count()} session(s) to {$baseUrl}"); - return self::SUCCESS; - } - - private function resolveToken(): ?string - { - $token = $this->option('token'); - if ($token) { - return $token; - } - - $tokenPath = config('movez.token_path'); - if ($tokenPath && file_exists($tokenPath)) { - return trim((string) file_get_contents($tokenPath)); - } - - return null; - } -} diff --git a/contextbridge/app/Parsers/ClineParser.php b/contextbridge/app/Parsers/ClineParser.php deleted file mode 100644 index e9f23a3..0000000 --- a/contextbridge/app/Parsers/ClineParser.php +++ /dev/null @@ -1,87 +0,0 @@ - */ - public function parseMetadata(string $projectPath): Collection - { - return $this->parse($projectPath); - } - - public function parse(string $projectPath): Collection - { - $os = PlatformPaths::configKey(); - $pattern = config("movez.tools.cline.storage.{$os}", ''); - $dirs = PlatformPaths::globExpand((string) $pattern); - - if (empty($dirs)) { - return collect(); - } - - $sessions = collect(); - - foreach ($dirs as $dir) { - $files = glob($dir . '/*.json') ?: []; - - foreach ($files as $file) { - $raw = file_get_contents($file); - $data = json_decode((string) $raw, true); - - if (!is_array($data)) { - continue; - } - - $turns = collect($data['conversation'] ?? $data['messages'] ?? []) - ->map(fn(array $msg) => new TurnDTO( - role: $msg['role'] ?? 'user', - content: $msg['content'] ?? '', - timestamp: Carbon::parse($msg['timestamp'] ?? 'now'), - )); - - $sessions->push(new SessionDTO( - id: $data['id'] ?? pathinfo($file, PATHINFO_FILENAME), - title: $data['task'] ?? basename($file), - sourceTool: $this->toolName(), - sourceMachineSha: substr(hash('sha256', (string) gethostname()), 0, 16), - createdAt: Carbon::parse($data['ts'] ?? 'now'), - lastActiveAt: Carbon::parse($data['ts'] ?? 'now'), - turns: $turns, - )); - } - } - - return $sessions; - } -} diff --git a/contextbridge/app/Parsers/CodexParser.php b/contextbridge/app/Parsers/CodexParser.php deleted file mode 100644 index 9a9913f..0000000 --- a/contextbridge/app/Parsers/CodexParser.php +++ /dev/null @@ -1,92 +0,0 @@ -getStoragePath($projectPath); - return is_dir($storagePath) && !empty(glob($storagePath . '/*.jsonl')); - } - - /** @return Collection */ - public function parseMetadata(string $projectPath): Collection - { - return $this->parse($projectPath); - } - - public function parse(string $projectPath): Collection - { - $storagePath = $this->getStoragePath($projectPath); - - if (!is_dir($storagePath)) { - return collect(); - } - - $files = glob($storagePath . '/*.jsonl') ?: []; - $sessions = collect(); - - foreach ($files as $file) { - $lines = $this->readJsonlFile($file); - if (empty($lines)) { - continue; - } - - $sessionId = pathinfo($file, PATHINFO_FILENAME); - $turns = collect(); - $createdAt = null; - $lastAt = null; - - foreach ($lines as $line) { - if (!is_array($line) || !isset($line['role'])) { - continue; - } - - $ts = Carbon::parse($line['timestamp'] ?? 'now'); - if ($createdAt === null) { - $createdAt = $ts; - } - $lastAt = $ts; - - $turns->push(new TurnDTO( - role: $line['role'], - content: $line['content'] ?? '', - timestamp: $ts, - )); - } - - $sessions->push(new SessionDTO( - id: $sessionId, - title: $lines[0]['title'] ?? basename($file), - sourceTool: $this->toolName(), - sourceMachineSha: substr(hash('sha256', (string) gethostname()), 0, 16), - createdAt: $createdAt ?? Carbon::now(), - lastActiveAt: $lastAt ?? Carbon::now(), - turns: $turns, - )); - } - - return $sessions; - } -} diff --git a/contextbridge/app/Parsers/WindsurfParser.php b/contextbridge/app/Parsers/WindsurfParser.php deleted file mode 100644 index 7cc3839..0000000 --- a/contextbridge/app/Parsers/WindsurfParser.php +++ /dev/null @@ -1,12 +0,0 @@ - $sessions - */ - public function pack( - Collection $sessions, - string $outputPath, - ?ProjectConfigDTO $config = null, - string $sourceTool = 'unknown', - ): void { - $machineSha = $this->machineSha(); - - $bundleData = [ - 'version' => '1.0', - 'source_tool' => $sourceTool, - 'exported_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ISO8601), - 'sessions' => $sessions->map(fn(SessionDTO $s) => $s->toArray())->values()->all(), - ]; - - BundleSchema::validate($bundleData); - - $manifest = BundleSchema::makeManifest($sourceTool, $machineSha); - - $zip = new ZipArchive(); - if ($zip->open($outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - throw new RuntimeException("Cannot create archive: {$outputPath}"); - } - - $zip->addFromString('bundle.json', json_encode($bundleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - $zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - - if ($config !== null) { - $zip->addFromString('config.json', json_encode($config->toArray(), JSON_PRETTY_PRINT)); - } - - $zip->close(); - } - - /** - * Unpack a .cbz archive and return the sessions it contains. - * - * @return Collection - * @throws RuntimeException on corrupted archive - */ - public function unpack(string $cbzPath): Collection - { - if (!file_exists($cbzPath)) { - throw new RuntimeException("Archive not found: {$cbzPath}"); - } - - $zip = new ZipArchive(); - if ($zip->open($cbzPath) !== true) { - throw new RuntimeException("Cannot open archive: {$cbzPath}"); - } - - $bundleJson = $zip->getFromName('bundle.json'); - $zip->close(); - - if ($bundleJson === false) { - throw new RuntimeException('Archive is missing bundle.json'); - } - - $data = json_decode($bundleJson, true); - - if (!is_array($data)) { - throw new RuntimeException('bundle.json contains invalid JSON'); - } - - BundleSchema::validate($data); - - return collect($data['sessions'])->map(fn(array $s) => SessionDTO::fromArray($s)); - } - - private function machineSha(): string - { - $hostname = (string) gethostname(); - return substr(hash('sha256', $hostname), 0, 16); - } -} diff --git a/contextbridge/app/Services/SyncClient.php b/contextbridge/app/Services/SyncClient.php deleted file mode 100644 index 6397940..0000000 --- a/contextbridge/app/Services/SyncClient.php +++ /dev/null @@ -1,70 +0,0 @@ - $sessions - */ - public function push(Collection $sessions, string $token): void - { - $payload = json_encode($sessions->map(fn(SessionDTO $s) => $s->toArray())->all()); - $encrypted = $this->encryptor->encrypt((string) $payload); - - $response = Http::withToken($token) - ->post("{$this->baseUrl}/api/sync/push", ['data' => $encrypted]); - - $this->assertSuccess($response, [401 => 'Unauthorized', 500 => 'Server error']); - } - - /** - * Pull sessions from the sync server (decrypts automatically). - * - * @return Collection - */ - public function pull(string $token): Collection - { - $response = Http::withToken($token) - ->get("{$this->baseUrl}/api/sync/pull"); - - $this->assertSuccess($response, [401 => 'Unauthorized', 500 => 'Server error']); - - $decrypted = $this->encryptor->decrypt($response->json('data')); - $decoded = json_decode($decrypted, true); - - if (!is_array($decoded)) { - throw new RuntimeException('Sync server returned invalid payload'); - } - - return collect($decoded)->map(fn(array $s) => SessionDTO::fromArray($s)); - } - - private function assertSuccess(\Illuminate\Http\Client\Response $response, array $errorMap): void - { - $status = $response->status(); - - foreach ($errorMap as $code => $message) { - if ($status === $code) { - throw new RuntimeException($message); - } - } - - if (!$response->successful()) { - throw new RuntimeException("Sync server error: HTTP {$status}"); - } - } -} diff --git a/contextbridge/app/Writers/CodexWriter.php b/contextbridge/app/Writers/CodexWriter.php deleted file mode 100644 index 1e0973d..0000000 --- a/contextbridge/app/Writers/CodexWriter.php +++ /dev/null @@ -1,45 +0,0 @@ - $sessions */ - public function write(Collection $sessions, string $projectPath): void - { - $storagePath = $this->getStoragePath(); - if (!is_dir($storagePath)) { - mkdir($storagePath, 0755, true); - } - - foreach ($sessions as $session) { - $lines = $session->turns->map(fn($turn) => json_encode([ - 'role' => $turn->role, - 'content' => $turn->content, - 'timestamp' => $turn->timestamp->toIso8601String(), - 'title' => $session->title, - ], JSON_UNESCAPED_UNICODE)); - - $path = $storagePath . DIRECTORY_SEPARATOR . $session->id . '.jsonl'; - file_put_contents($path, implode("\n", $lines->all()) . "\n"); - } - } - - private function getStoragePath(): string - { - $os = PlatformPaths::configKey(); - $cfg = config('movez.tools.codex.storage.' . $os, ''); - return PlatformPaths::expand((string) $cfg); - } -} diff --git a/contextbridge/app/Writers/WindsurfWriter.php b/contextbridge/app/Writers/WindsurfWriter.php deleted file mode 100644 index 9788e9c..0000000 --- a/contextbridge/app/Writers/WindsurfWriter.php +++ /dev/null @@ -1,12 +0,0 @@ -artisan('inspire')->assertExitCode(0); -}); diff --git a/contextbridge/tests/Unit/ExampleTest.php b/contextbridge/tests/Unit/ExampleTest.php deleted file mode 100644 index 61cd84c..0000000 --- a/contextbridge/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/contextbridge/tests/Unit/Writers/ClaudeCodeWriterTest.php b/contextbridge/tests/Unit/Writers/ClaudeCodeWriterTest.php deleted file mode 100644 index 55a9a88..0000000 --- a/contextbridge/tests/Unit/Writers/ClaudeCodeWriterTest.php +++ /dev/null @@ -1,78 +0,0 @@ -fakePath; - } - - // Expose protected method for testing - public function write(\Illuminate\Support\Collection $sessions, string $projectPath): void - { - if (!is_dir($this->fakePath)) { - mkdir($this->fakePath, 0755, true); - } - - foreach ($sessions as $session) { - $lines = $session->turns->map(function ($turn) use ($session) { - return json_encode([ - 'role' => $turn->role, - 'content' => $turn->content, - 'timestamp' => $turn->timestamp->toIso8601String(), - 'files_referenced' => $turn->filesReferenced, - 'file_diffs' => $turn->fileDiffs - ->map(fn($d) => ['file' => $d->file, 'diff' => $d->diff]) - ->values() - ->all(), - 'reasoning_trace' => $turn->reasoningTrace, - 'tool_calls' => $turn->toolCalls, - 'session_title' => $session->title, - ], JSON_UNESCAPED_UNICODE); - }); - - $path = $this->fakePath . DIRECTORY_SEPARATOR . $session->id . '.jsonl'; - file_put_contents($path, implode("\n", $lines->all()) . "\n"); - } - } - }; - - $session = new SessionDTO( - id: 'sess-xyz', - title: 'ClaudeCode Test', - sourceTool: 'claude-code', - sourceMachineSha: 'abc', - createdAt: Carbon::now(), - lastActiveAt: Carbon::now(), - turns: collect([ - new TurnDTO('user', 'Q', Carbon::now()), - new TurnDTO('assistant', 'A', Carbon::now()), - ]), - ); - - $writer->write(collect([$session]), '/any'); - - expect(file_exists($dir . '/sess-xyz.jsonl'))->toBeTrue(); - - $lines = file($dir . '/sess-xyz.jsonl', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - expect($lines)->toHaveCount(2); - - $first = json_decode($lines[0], true); - expect($first['role'])->toBe('user') - ->and($first['content'])->toBe('Q'); - }); -}); - -it('toolName returns claude-code', function (): void { - expect((new ClaudeCodeWriter())->toolName())->toBe('claude-code'); -}); diff --git a/contextbridge/.editorconfig b/movez/.editorconfig similarity index 100% rename from contextbridge/.editorconfig rename to movez/.editorconfig diff --git a/contextbridge/.gitattributes b/movez/.gitattributes similarity index 100% rename from contextbridge/.gitattributes rename to movez/.gitattributes diff --git a/contextbridge/.gitignore b/movez/.gitignore similarity index 100% rename from contextbridge/.gitignore rename to movez/.gitignore diff --git a/contextbridge/README.md b/movez/README.md similarity index 100% rename from contextbridge/README.md rename to movez/README.md diff --git a/movez/app/Commands/DoctorCommand.php b/movez/app/Commands/DoctorCommand.php new file mode 100644 index 0000000..3c000b4 --- /dev/null +++ b/movez/app/Commands/DoctorCommand.php @@ -0,0 +1,74 @@ +info('MoveZ Doctor'); + $this->line(''); + + $checks = [ + 'PHP >= 8.2 (found ' . PHP_VERSION . ')' => version_compare(PHP_VERSION, '8.2.0', '>='), + 'OpenSSL extension loaded' => extension_loaded('openssl'), + 'PDO SQLite driver loaded' => extension_loaded('pdo_sqlite'), + 'ZipArchive class available' => class_exists('ZipArchive'), + ]; + + $keyPath = $encryptor->keyPath(); + $keyDir = dirname($keyPath); + $checks['Encryption key directory accessible'] = is_dir($keyDir) ? is_writable($keyDir) : is_writable(dirname($keyDir)); + + $passed = true; + foreach ($checks as $label => $ok) { + $this->checkResult($ok, $label); + $passed = $passed && $ok; + } + + $this->line(''); + $this->line($encryptor->hasKey() + ? " Encryption key: {$keyPath}" + : " Encryption key: not created yet (it is generated on first encrypted export or sync:push)"); + + // Detected tools + $this->line(''); + $detected = $detector->detect(includeUnsupported: true); + $supported = array_values(array_filter($detected, fn(string $t) => $detector->isSupported($t))); + $unsupported = array_values(array_diff($detected, $supported)); + + if (!empty($supported)) { + $this->info('Detected AI tools: ' . implode(', ', $supported)); + } else { + $this->warn('No supported AI tools detected on this machine'); + } + + foreach ($unsupported as $tool) { + $this->warn("Detected {$tool}, but it is not supported yet"); + } + + $this->line(''); + if ($passed) { + $this->info('All checks passed!'); + } else { + $this->error('Some checks failed. See above for details.'); + } + + return $passed ? self::SUCCESS : self::FAILURE; + } + + private function checkResult(bool $ok, string $label): void + { + $icon = $ok ? '✔' : '✘'; + $this->line(" {$icon} {$label}"); + } +} diff --git a/movez/app/Commands/ExportCommand.php b/movez/app/Commands/ExportCommand.php new file mode 100644 index 0000000..6940cc9 --- /dev/null +++ b/movez/app/Commands/ExportCommand.php @@ -0,0 +1,80 @@ +option('tool'); + $outputPath = (string) ($this->option('output') ?: ('movez-export-' . date('Ymd-His') . '.cbz')); + $project = $this->option('project'); + $encrypt = (bool) $this->option('encrypt'); + + if ($toolName === 'auto') { + $detected = $detector->detect(); + if (empty($detected)) { + $this->error('No supported AI coding tools detected on this machine.'); + return self::FAILURE; + } + $toolName = $detected[0]; + $this->info("Auto-detected tool: {$toolName}"); + } + + try { + $sessions = $detector->getParser($toolName)->parse((string) ($project ?? getcwd())); + $sessions = ProjectFilter::apply($sessions, $project); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + return self::FAILURE; + } + + if ($sessions->isEmpty()) { + $this->warn('No sessions found for the given tool and project.'); + return self::SUCCESS; + } + + try { + if (strtolower(pathinfo($outputPath, PATHINFO_EXTENSION)) === 'json') { + $json = (string) json_encode( + $sessions->map(fn($s) => $s->toArray())->values()->all(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE + ); + + if ($encrypt) { + $json = $encryptor->encrypt($json); + $outputPath = (string) preg_replace('/\.json$/i', '.enc.json', $outputPath); + } + + file_put_contents($outputPath, $json); + } else { + $packager->pack($sessions, $outputPath, null, $toolName, $encrypt ? $encryptor : null); + } + } catch (\Throwable $e) { + $this->error('Export failed: ' . $e->getMessage()); + return self::FAILURE; + } + + $this->info("Exported {$sessions->count()} session(s) to: {$outputPath}"); + if ($encrypt) { + $this->line("Encrypted with the key at {$encryptor->keyPath()} — copy it to any machine that needs to import this bundle."); + } + + return self::SUCCESS; + } +} diff --git a/contextbridge/app/Commands/ImportCommand.php b/movez/app/Commands/ImportCommand.php similarity index 55% rename from contextbridge/app/Commands/ImportCommand.php rename to movez/app/Commands/ImportCommand.php index 3a306dd..4ba8cd9 100644 --- a/contextbridge/app/Commands/ImportCommand.php +++ b/movez/app/Commands/ImportCommand.php @@ -14,9 +14,9 @@ class ImportCommand extends Command { protected $signature = 'import {--input= : Path to the bundle file (.cbz or .json)} - {--tool= : Target tool (cursor|windsurf|claude-code|codex|copilot-cli)} + {--tool= : Target tool (cursor|claude-code|codex|copilot-cli)} {--project= : Target project path} - {--encrypted : Input file is AES-256-GCM encrypted} + {--encrypted : Input .json file is AES-256-GCM encrypted (.cbz bundles are detected automatically)} {--from-path= : Source project path (for path remapping)} {--to-path= : Target project path (for path remapping)}'; @@ -27,7 +27,6 @@ public function handle(ToolDetector $detector, Packager $packager, Encryptor $en $inputPath = $this->option('input'); $toolName = $this->option('tool'); $projectPath = $this->option('project') ?? getcwd(); - $isEncrypted = (bool) $this->option('encrypted'); $fromPath = $this->option('from-path'); $toPath = $this->option('to-path'); @@ -41,35 +40,21 @@ public function handle(ToolDetector $detector, Packager $packager, Encryptor $en return self::FAILURE; } - // .cbz/.zip archives — use Packager::unpack() - $ext = strtolower(pathinfo($inputPath, PATHINFO_EXTENSION)); - if (in_array($ext, ['cbz', 'zip'], true)) { - try { - $sessions = $packager->unpack($inputPath); - } catch (\RuntimeException $e) { - $this->error('Failed to unpack bundle: ' . $e->getMessage()); - return self::FAILURE; - } - } else { - // Plain JSON export (possibly encrypted) - $raw = (string) file_get_contents($inputPath); - - if ($isEncrypted) { - try { - $raw = $encryptor->decrypt($raw); - } catch (\RuntimeException $e) { - $this->error('Decryption failed: ' . $e->getMessage()); - return self::FAILURE; - } - } - - $data = json_decode($raw, true); - if (!is_array($data)) { - $this->error('Invalid bundle JSON'); - return self::FAILURE; - } - - $sessions = collect($data)->map(fn(array $s) => SessionDTO::fromArray($s)); + try { + $writer = $detector->getWriter($toolName); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage()); + return self::FAILURE; + } + + try { + $ext = strtolower(pathinfo($inputPath, PATHINFO_EXTENSION)); + $sessions = in_array($ext, ['cbz', 'zip'], true) + ? $packager->unpack($inputPath, $encryptor) + : $this->readJsonBundle($inputPath, $encryptor); + } catch (\Throwable $e) { + $this->error('Cannot read bundle: ' . $e->getMessage()); + return self::FAILURE; } if ($fromPath && $toPath) { @@ -78,14 +63,33 @@ public function handle(ToolDetector $detector, Packager $packager, Encryptor $en } try { - $writer = $detector->getWriter($toolName); $writer->write($sessions, (string) $projectPath); - } catch (\InvalidArgumentException $e) { - $this->error($e->getMessage()); + } catch (\Throwable $e) { + $this->error('Import failed: ' . $e->getMessage()); return self::FAILURE; } $this->info("Imported {$sessions->count()} session(s) to {$toolName}"); return self::SUCCESS; } + + /** @return \Illuminate\Support\Collection */ + private function readJsonBundle(string $path, Encryptor $encryptor): \Illuminate\Support\Collection + { + $raw = (string) file_get_contents($path); + + if ($this->option('encrypted') || str_ends_with(strtolower($path), '.enc.json')) { + $raw = $encryptor->decrypt($raw); + } + + $data = json_decode($raw, true); + if (!is_array($data)) { + throw new \RuntimeException('Invalid bundle JSON'); + } + + // Accept both a plain session list and a full bundle object + $list = array_key_exists('sessions', $data) && is_array($data['sessions']) ? $data['sessions'] : $data; + + return collect($list)->map(fn(array $s) => SessionDTO::fromArray($s)); + } } diff --git a/contextbridge/app/Commands/ListCommand.php b/movez/app/Commands/ListCommand.php similarity index 85% rename from contextbridge/app/Commands/ListCommand.php rename to movez/app/Commands/ListCommand.php index 803b042..0de57b9 100644 --- a/contextbridge/app/Commands/ListCommand.php +++ b/movez/app/Commands/ListCommand.php @@ -4,13 +4,14 @@ namespace App\Commands; use App\Services\ToolDetector; +use App\Support\ProjectFilter; use LaravelZero\Framework\Commands\Command; class ListCommand extends Command { protected $signature = 'list-sessions {--tool= : Filter by specific tool} - {--project= : Project path} + {--project= : Only list sessions belonging to this project path} {--json : Output as JSON}'; protected $description = 'List all AI coding sessions on this machine'; @@ -18,7 +19,8 @@ class ListCommand extends Command public function handle(ToolDetector $detector): int { $filterTool = $this->option('tool'); - $projectPath = $this->option('project') ?? getcwd(); + $project = $this->option('project'); + $projectPath = (string) ($project ?? getcwd()); $asJson = (bool) $this->option('json'); $tools = $filterTool ? [$filterTool] : $detector->detect(); @@ -37,7 +39,7 @@ public function handle(ToolDetector $detector): int foreach ($tools as $toolName) { try { $parser = $detector->getParser($toolName); - $sessions = $parser->parseMetadata((string) $projectPath); + $sessions = ProjectFilter::apply($parser->parseMetadata($projectPath), $project); $allSessions = $allSessions->merge($sessions); } catch (\Throwable $e) { // In JSON mode write warnings to stderr so they don't corrupt JSON stdout @@ -62,7 +64,7 @@ public function handle(ToolDetector $detector): int $s->title, $s->sourceTool, $s->lastActiveAt->toDateTimeString(), - $s->turns->count(), + $s->turnCount(), ])->all(); $this->table( diff --git a/contextbridge/app/Commands/PackageCommand.php b/movez/app/Commands/PackageCommand.php similarity index 53% rename from contextbridge/app/Commands/PackageCommand.php rename to movez/app/Commands/PackageCommand.php index ef2adfd..3b697d9 100644 --- a/contextbridge/app/Commands/PackageCommand.php +++ b/movez/app/Commands/PackageCommand.php @@ -11,37 +11,41 @@ class PackageCommand extends Command { protected $signature = 'package - {--input= : Path to bundle JSON file} + {--input= : Path to a session list JSON file (from export --output=*.json)} {--output= : Path to output .cbz archive} {--encrypt : Encrypt bundle.json inside the archive} {--tool=unknown : Source tool name for manifest}'; - protected $description = 'Package a bundle JSON into a portable .cbz archive'; + protected $description = 'Package a session list JSON into a portable .cbz archive'; public function handle(Packager $packager, Encryptor $encryptor): int { $inputPath = $this->option('input'); - $outputPath = $this->option('output') ?? ('movez-' . date('Ymd-His') . '.cbz'); - $toolName = $this->option('tool') ?? 'unknown'; + $outputPath = (string) ($this->option('output') ?: ('movez-' . date('Ymd-His') . '.cbz')); + $toolName = (string) ($this->option('tool') ?? 'unknown'); + $encrypt = (bool) $this->option('encrypt'); if (!$inputPath || !file_exists($inputPath)) { $this->error('Input file not found: ' . ($inputPath ?? '(not specified)')); return self::FAILURE; } - $raw = (string) file_get_contents($inputPath); - $data = json_decode($raw, true); + $data = json_decode((string) file_get_contents($inputPath), true); if (!is_array($data)) { $this->error('Input is not valid JSON'); return self::FAILURE; } - $sessions = collect($data)->map(fn(array $s) => SessionDTO::fromArray($s)); - - $packager->pack($sessions, $outputPath, null, $toolName); + try { + $sessions = collect($data)->map(fn(array $s) => SessionDTO::fromArray($s)); + $packager->pack($sessions, $outputPath, null, $toolName, $encrypt ? $encryptor : null); + } catch (\Throwable $e) { + $this->error('Packaging failed: ' . $e->getMessage()); + return self::FAILURE; + } - $this->info("Packaged {$sessions->count()} session(s) into: {$outputPath}"); + $this->info("Packaged {$sessions->count()} session(s) into: {$outputPath}" . ($encrypt ? ' (encrypted)' : '')); return self::SUCCESS; } } diff --git a/movez/app/Commands/Sync/ResolvesSyncCredentials.php b/movez/app/Commands/Sync/ResolvesSyncCredentials.php new file mode 100644 index 0000000..5334196 --- /dev/null +++ b/movez/app/Commands/Sync/ResolvesSyncCredentials.php @@ -0,0 +1,49 @@ +option('token') ?: config('movez.token'); + if (is_string($token) && $token !== '') { + return $token; + } + + $tokenPath = config('movez.token_path'); + if (is_string($tokenPath) && file_exists($tokenPath)) { + $fromFile = trim((string) file_get_contents($tokenPath)); + return $fromFile !== '' ? $fromFile : null; + } + + return null; + } + + private function resolveServer(): ?string + { + $server = $this->option('server') ?: config('movez.server_url'); + return is_string($server) && $server !== '' ? rtrim($server, '/') : null; + } + + private function missingCredentials(?string $token, ?string $server): bool + { + if (!$token) { + $this->error('No token found. Use --token, set MOVEZ_TOKEN, or store it in ~/.movez/token'); + return true; + } + + if (!$server) { + $this->error('No server URL. Use --server or set the MOVEZ_SERVER_URL env var'); + return true; + } + + return false; + } +} diff --git a/movez/app/Commands/Sync/SyncPullCommand.php b/movez/app/Commands/Sync/SyncPullCommand.php new file mode 100644 index 0000000..79184db --- /dev/null +++ b/movez/app/Commands/Sync/SyncPullCommand.php @@ -0,0 +1,68 @@ +resolveToken(); + $baseUrl = $this->resolveServer(); + $toolName = $this->option('tool'); + $projectPath = (string) ($this->option('project') ?? getcwd()); + $fromPath = $this->option('from-path'); + $toPath = $this->option('to-path'); + + if ($this->missingCredentials($token, $baseUrl)) { + return self::FAILURE; + } + + try { + $sessions = (new SyncClient((string) $baseUrl, $encryptor))->pull((string) $token); + + if ($fromPath && $toPath) { + $sessions = $mapper->remap($fromPath, $toPath, $sessions); + } + + if (!$toolName) { + $this->line((string) json_encode( + $sessions->map(fn($s) => $s->toArray())->values()->all(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE + )); + return self::SUCCESS; + } + + if ($sessions->isEmpty()) { + $this->warn('The sync server has no sessions for this account yet.'); + return self::SUCCESS; + } + + $detector->getWriter($toolName)->write($sessions, $projectPath); + } catch (\Throwable $e) { + $this->error('Pull failed: ' . $e->getMessage()); + return self::FAILURE; + } + + $this->info("Pulled and imported {$sessions->count()} session(s) into {$toolName}"); + return self::SUCCESS; + } +} diff --git a/movez/app/Commands/Sync/SyncPushCommand.php b/movez/app/Commands/Sync/SyncPushCommand.php new file mode 100644 index 0000000..642597a --- /dev/null +++ b/movez/app/Commands/Sync/SyncPushCommand.php @@ -0,0 +1,58 @@ +resolveToken(); + $baseUrl = $this->resolveServer(); + $toolName = (string) $this->option('tool'); + $project = $this->option('project'); + + if ($this->missingCredentials($token, $baseUrl)) { + return self::FAILURE; + } + + if ($toolName === 'auto') { + $detected = $detector->detect(); + if (empty($detected)) { + $this->error('No supported AI coding tools detected'); + return self::FAILURE; + } + $toolName = $detected[0]; + } + + try { + $sessions = $detector->getParser($toolName)->parse((string) ($project ?? getcwd())); + $sessions = ProjectFilter::apply($sessions, $project); + + (new SyncClient((string) $baseUrl, $encryptor))->push($sessions, (string) $token); + } catch (\Throwable $e) { + $this->error('Push failed: ' . $e->getMessage()); + return self::FAILURE; + } + + $this->info("Pushed {$sessions->count()} session(s) from {$toolName} to {$baseUrl}"); + $this->line("Pull on another machine needs the same key: copy {$encryptor->keyPath()} there."); + return self::SUCCESS; + } +} diff --git a/contextbridge/app/Commands/TransferCommand.php b/movez/app/Commands/TransferCommand.php similarity index 58% rename from contextbridge/app/Commands/TransferCommand.php rename to movez/app/Commands/TransferCommand.php index 91c399a..9f5f39d 100644 --- a/contextbridge/app/Commands/TransferCommand.php +++ b/movez/app/Commands/TransferCommand.php @@ -5,6 +5,7 @@ use App\Services\PathMapper; use App\Services\ToolDetector; +use App\Support\ProjectFilter; use LaravelZero\Framework\Commands\Command; class TransferCommand extends Command @@ -12,7 +13,7 @@ class TransferCommand extends Command protected $signature = 'transfer {--from= : Source tool} {--to= : Target tool} - {--project= : Project path} + {--project= : Project path (filters source sessions and is the target project)} {--from-path= : Source project path for remapping} {--to-path= : Target project path for remapping}'; @@ -22,7 +23,8 @@ public function handle(ToolDetector $detector, PathMapper $mapper): int { $fromTool = $this->option('from'); $toTool = $this->option('to'); - $projectPath = $this->option('project') ?? getcwd(); + $project = $this->option('project'); + $projectPath = (string) ($project ?? getcwd()); $fromPath = $this->option('from-path'); $toPath = $this->option('to-path'); @@ -32,25 +34,30 @@ public function handle(ToolDetector $detector, PathMapper $mapper): int } try { - $parser = $detector->getParser($fromTool); - $writer = $detector->getWriter($toTool); + $parser = $detector->getParser($fromTool); + $writer = $detector->getWriter($toTool); } catch (\InvalidArgumentException $e) { $this->error($e->getMessage()); return self::FAILURE; } - $sessions = $parser->parse((string) $projectPath); + try { + $sessions = ProjectFilter::apply($parser->parse($projectPath), $project); - if ($sessions->isEmpty()) { - $this->warn("No sessions found in {$fromTool}"); - return self::SUCCESS; - } + if ($sessions->isEmpty()) { + $this->warn("No sessions found in {$fromTool}"); + return self::SUCCESS; + } - if ($fromPath && $toPath) { - $sessions = $mapper->remap($fromPath, $toPath, $sessions); - } + if ($fromPath && $toPath) { + $sessions = $mapper->remap($fromPath, $toPath, $sessions); + } - $writer->write($sessions, (string) $projectPath); + $writer->write($sessions, $projectPath); + } catch (\Throwable $e) { + $this->error('Transfer failed: ' . $e->getMessage()); + return self::FAILURE; + } $this->info("Transferred {$sessions->count()} session(s) from {$fromTool} to {$toTool}"); return self::SUCCESS; diff --git a/contextbridge/app/Commands/UnpackCommand.php b/movez/app/Commands/UnpackCommand.php similarity index 57% rename from contextbridge/app/Commands/UnpackCommand.php rename to movez/app/Commands/UnpackCommand.php index 41ea853..47ac190 100644 --- a/contextbridge/app/Commands/UnpackCommand.php +++ b/movez/app/Commands/UnpackCommand.php @@ -3,7 +3,9 @@ namespace App\Commands; +use App\Services\Encryptor; use App\Services\Packager; +use App\Support\SafePath; use LaravelZero\Framework\Commands\Command; class UnpackCommand extends Command @@ -14,10 +16,10 @@ class UnpackCommand extends Command protected $description = 'Unpack a .cbz archive into individual session JSON files'; - public function handle(Packager $packager): int + public function handle(Packager $packager, Encryptor $encryptor): int { $inputPath = $this->option('input'); - $outputPath = $this->option('output') ?? './movez-sessions'; + $outputPath = (string) ($this->option('output') ?: './movez-sessions'); if (!$inputPath || !file_exists($inputPath)) { $this->error('Input archive not found: ' . ($inputPath ?? '(not specified)')); @@ -25,19 +27,19 @@ public function handle(Packager $packager): int } try { - $sessions = $packager->unpack($inputPath); - } catch (\RuntimeException $e) { - $this->error('Unpack failed: ' . $e->getMessage()); - return self::FAILURE; - } + $sessions = $packager->unpack($inputPath, $encryptor); - if (!is_dir($outputPath)) { - mkdir($outputPath, 0755, true); - } + if (!is_dir($outputPath)) { + mkdir($outputPath, 0755, true); + } - foreach ($sessions as $session) { - $file = $outputPath . DIRECTORY_SEPARATOR . $session->id . '.json'; - file_put_contents($file, json_encode($session->toArray(), JSON_PRETTY_PRINT)); + foreach ($sessions as $session) { + $file = $outputPath . DIRECTORY_SEPARATOR . SafePath::id($session->id) . '.json'; + file_put_contents($file, json_encode($session->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + } catch (\Throwable $e) { + $this->error('Unpack failed: ' . $e->getMessage()); + return self::FAILURE; } $this->info("Unpacked {$sessions->count()} session(s) into: {$outputPath}"); diff --git a/contextbridge/app/Contracts/ParserInterface.php b/movez/app/Contracts/ParserInterface.php similarity index 100% rename from contextbridge/app/Contracts/ParserInterface.php rename to movez/app/Contracts/ParserInterface.php diff --git a/contextbridge/app/Contracts/WriterInterface.php b/movez/app/Contracts/WriterInterface.php similarity index 100% rename from contextbridge/app/Contracts/WriterInterface.php rename to movez/app/Contracts/WriterInterface.php diff --git a/contextbridge/app/DTOs/FileDiffDTO.php b/movez/app/DTOs/FileDiffDTO.php similarity index 100% rename from contextbridge/app/DTOs/FileDiffDTO.php rename to movez/app/DTOs/FileDiffDTO.php diff --git a/contextbridge/app/DTOs/ProjectConfigDTO.php b/movez/app/DTOs/ProjectConfigDTO.php similarity index 100% rename from contextbridge/app/DTOs/ProjectConfigDTO.php rename to movez/app/DTOs/ProjectConfigDTO.php diff --git a/contextbridge/app/DTOs/SessionDTO.php b/movez/app/DTOs/SessionDTO.php similarity index 59% rename from contextbridge/app/DTOs/SessionDTO.php rename to movez/app/DTOs/SessionDTO.php index 8bfdddc..a8015b1 100644 --- a/contextbridge/app/DTOs/SessionDTO.php +++ b/movez/app/DTOs/SessionDTO.php @@ -18,19 +18,21 @@ public function __construct( /** @var Collection */ public Collection $turns, public string $project = '', + /** Known turn count when turns are not loaded (metadata-only listings) */ + public ?int $turnCount = null, ) {} public static function fromArray(array $data): self { return new self( - id: $data['id'], - title: $data['title'] ?? 'Untitled', - sourceTool: $data['source_tool'], - sourceMachineSha: $data['source_machine_id'], - createdAt: Carbon::parse($data['created_at']), - lastActiveAt: Carbon::parse($data['last_active_at']), - turns: collect($data['turns'])->map(fn($t) => TurnDTO::fromArray($t)), - project: $data['project'] ?? '', + id: (string) $data['id'], + title: (string) ($data['title'] ?? 'Untitled'), + sourceTool: (string) ($data['source_tool'] ?? 'unknown'), + sourceMachineSha: (string) ($data['source_machine_id'] ?? $data['machine_sha'] ?? ''), + createdAt: Carbon::parse($data['created_at'] ?? 'now'), + lastActiveAt: Carbon::parse($data['last_active_at'] ?? $data['created_at'] ?? 'now'), + turns: collect($data['turns'] ?? [])->map(fn($t) => TurnDTO::fromArray($t)), + project: (string) ($data['project'] ?? ''), ); } @@ -44,8 +46,13 @@ public function toArray(): array 'source_machine_id' => $this->sourceMachineSha, 'created_at' => $this->createdAt->toIso8601String(), 'last_active_at' => $this->lastActiveAt->toIso8601String(), - 'turn_count' => $this->turns->count(), + 'turn_count' => $this->turnCount(), 'turns' => $this->turns->map(fn($t) => $t->toArray())->all(), ]; } + + public function turnCount(): int + { + return $this->turnCount ?? $this->turns->count(); + } } diff --git a/contextbridge/app/DTOs/TurnDTO.php b/movez/app/DTOs/TurnDTO.php similarity index 86% rename from contextbridge/app/DTOs/TurnDTO.php rename to movez/app/DTOs/TurnDTO.php index d299efc..646cff7 100644 --- a/contextbridge/app/DTOs/TurnDTO.php +++ b/movez/app/DTOs/TurnDTO.php @@ -4,6 +4,7 @@ namespace App\DTOs; use Illuminate\Support\Collection; +use App\Support\ContentFlattener; use Carbon\Carbon; final readonly class TurnDTO @@ -22,9 +23,9 @@ public function __construct( public static function fromArray(array $data): self { return new self( - role: $data['role'], - content: $data['content'], - timestamp: Carbon::parse($data['timestamp']), + role: (string) ($data['role'] ?? 'user'), + content: ContentFlattener::flatten($data['content'] ?? ''), + timestamp: Carbon::parse($data['timestamp'] ?? 'now'), filesReferenced: $data['files_referenced'] ?? [], fileDiffs: collect($data['file_diffs'] ?? []) ->map(fn($d) => new FileDiffDTO($d['file'], $d['diff'])), diff --git a/contextbridge/app/Parsers/ClaudeCodeParser.php b/movez/app/Parsers/ClaudeCodeParser.php similarity index 79% rename from contextbridge/app/Parsers/ClaudeCodeParser.php rename to movez/app/Parsers/ClaudeCodeParser.php index c87d7cb..e5e342e 100644 --- a/contextbridge/app/Parsers/ClaudeCodeParser.php +++ b/movez/app/Parsers/ClaudeCodeParser.php @@ -4,9 +4,9 @@ namespace App\Parsers; use App\Contracts\ParserInterface; -use App\DTOs\FileDiffDTO; use App\DTOs\SessionDTO; use App\DTOs\TurnDTO; +use App\Support\ContentFlattener; use App\Support\PlatformPaths; use Carbon\Carbon; use Illuminate\Support\Collection; @@ -63,6 +63,7 @@ public function parseMetadata(string $projectPath): Collection } $title = null; + $cwd = null; $createdAt = null; $turnCount = 0; $headerDone = false; @@ -95,20 +96,9 @@ public function parseMetadata(string $projectPath): Collection if ($createdAt === null && str_contains($line, '"type":"user"')) { $d = json_decode($line, true); $createdAt = Carbon::parse($d['timestamp'] ?? 'now'); + $cwd = is_string($d['cwd'] ?? null) ? $d['cwd'] : null; if ($title === null) { - $content = $d['message']['content'] ?? ''; - if (is_array($content)) { - $text = ''; - foreach ($content as $block) { - if (isset($block['text'])) { - $text = $block['text']; - break; - } - } - $title = $text; - } else { - $title = (string) $content; - } + $title = ContentFlattener::flatten($d['message']['content'] ?? ''); } if ($title !== null && $sourceTool !== $this->toolName()) { $headerDone = true; // have everything we need @@ -119,9 +109,8 @@ public function parseMetadata(string $projectPath): Collection fclose($fh); $sessionId = pathinfo($file, PATHINFO_FILENAME); - $encodedDir = basename(dirname($file)); - $project = $this->decodeProjectName($encodedDir); - $lastAt = Carbon::createFromTimestamp(filemtime($file)); + $project = $this->projectName($cwd, basename(dirname($file))); + $lastAt = Carbon::createFromTimestamp((int) filemtime($file)); $sessions->push(new SessionDTO( id: $sessionId, @@ -132,6 +121,7 @@ public function parseMetadata(string $projectPath): Collection lastActiveAt: $lastAt, turns: collect(), project: $project, + turnCount: $turnCount, )); } @@ -166,6 +156,7 @@ public function parse(string $projectPath): Collection $lastAt = null; $firstUserContent = null; + $cwd = null; foreach ($lines as $line) { if (!is_array($line)) { @@ -178,21 +169,17 @@ public function parse(string $projectPath): Collection continue; } - $msg = $line['message'] ?? []; - $role = $msg['role'] ?? $type; - $rawContent = $msg['content'] ?? ''; - - // Assistant content is an array of blocks; flatten to text - if (is_array($rawContent)) { - $parts = []; - foreach ($rawContent as $block) { - if (isset($block['text'])) { - $parts[] = $block['text']; - } elseif (isset($block['thinking'])) { - // skip thinking blocks - } - } - $rawContent = implode("\n", $parts); + if ($cwd === null && is_string($line['cwd'] ?? null)) { + $cwd = $line['cwd']; + } + + $msg = is_array($line['message'] ?? null) ? $line['message'] : []; + $role = $msg['role'] ?? $type; + + // Content is a string or an array of blocks; thinking/tool blocks are dropped + $rawContent = ContentFlattener::flatten($msg['content'] ?? ''); + if (trim($rawContent) === '') { + continue; // e.g. tool_result-only entries } $ts = Carbon::parse($line['timestamp'] ?? 'now'); @@ -217,9 +204,7 @@ public function parse(string $projectPath): Collection )); } - // Derive project name from parent directory (e.g. "D--Flutter-Aurora" → "Aurora") - $encodedDir = basename(dirname($file)); - $project = $this->decodeProjectName($encodedDir); + $project = $this->projectName($cwd, basename(dirname($file))); // Use first user message as title (truncated to 80 chars) $title = $firstUserContent !== null @@ -241,6 +226,19 @@ public function parse(string $projectPath): Collection return $sessions; } + /** + * Prefer the real working directory recorded in the transcript; fall back + * to decoding the storage directory name. + */ + private function projectName(?string $cwd, string $encodedDir): string + { + if ($cwd !== null && $cwd !== '') { + return basename(str_replace('\\', '/', rtrim($cwd, '/\\'))); + } + + return $this->decodeProjectName($encodedDir); + } + private function decodeProjectName(string $encoded): string { // Claude Code encodes paths as "D--Flutter-Aurora" (drive + double-dash + path segments) @@ -248,9 +246,4 @@ private function decodeProjectName(string $encoded): string $parts = explode('-', $stripped); return end($parts) ?: $encoded; } - - private function machineSha(): string - { - return substr(hash('sha256', (string) gethostname()), 0, 16); - } } diff --git a/movez/app/Parsers/ClineParser.php b/movez/app/Parsers/ClineParser.php new file mode 100644 index 0000000..512cbe9 --- /dev/null +++ b/movez/app/Parsers/ClineParser.php @@ -0,0 +1,119 @@ +taskRoots(); + return $dirs[0] ?? PlatformPaths::expand($this->storagePattern()); + } + + public function detect(string $projectPath): bool + { + return !empty($this->taskRoots()); + } + + /** @return Collection */ + public function parseMetadata(string $projectPath): Collection + { + return $this->parse($projectPath); + } + + /** @return Collection */ + public function parse(string $projectPath): Collection + { + $sessions = collect(); + + // Cline stores one directory per task: + // tasks//api_conversation_history.json + foreach ($this->taskRoots() as $root) { + foreach (glob($root . '/*/api_conversation_history.json') ?: [] as $file) { + $session = $this->parseTask($file); + if ($session !== null) { + $sessions->push($session); + } + } + } + + return $sessions; + } + + private function parseTask(string $file): ?SessionDTO + { + $data = json_decode((string) file_get_contents($file), true); + if (!is_array($data)) { + return null; + } + + $taskId = basename(dirname($file)); + $messages = isset($data[0]) ? $data : ($data['conversation'] ?? $data['messages'] ?? []); + $fallback = is_numeric($taskId) + ? Carbon::createFromTimestampMs((int) $taskId) + : Carbon::createFromTimestamp((int) filemtime($file)); + + $turns = collect($messages) + ->filter(fn($msg) => is_array($msg)) + ->map(fn(array $msg) => new TurnDTO( + role: ($msg['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user', + content: ContentFlattener::flatten($msg['content'] ?? ''), + timestamp: isset($msg['ts']) ? Carbon::createFromTimestampMs((int) $msg['ts']) : $fallback, + )) + ->filter(fn(TurnDTO $t) => trim($t->content) !== '') + ->values(); + + if ($turns->isEmpty()) { + return null; + } + + $firstUser = $turns->first(fn(TurnDTO $t) => $t->role === 'user'); + $title = $firstUser !== null ? $this->titleFrom($firstUser->content) : $taskId; + + return new SessionDTO( + id: $taskId, + title: $title, + sourceTool: $this->toolName(), + sourceMachineSha: substr(hash('sha256', (string) gethostname()), 0, 16), + createdAt: $turns->first()->timestamp, + lastActiveAt: Carbon::createFromTimestamp((int) filemtime($file)), + turns: $turns, + ); + } + + private function titleFrom(string $content): string + { + // Cline wraps the user's task in ... + if (preg_match('/\s*(.*?)\s*<\/task>/s', $content, $m)) { + $content = $m[1]; + } + + return mb_substr(trim($content), 0, 80) ?: 'Untitled'; + } + + /** @return string[] */ + private function taskRoots(): array + { + return PlatformPaths::globExpand($this->storagePattern()); + } + + private function storagePattern(): string + { + $os = PlatformPaths::configKey(); + return (string) config("movez.tools.cline.storage.{$os}", ''); + } +} diff --git a/movez/app/Parsers/CodexParser.php b/movez/app/Parsers/CodexParser.php new file mode 100644 index 0000000..60e4633 --- /dev/null +++ b/movez/app/Parsers/CodexParser.php @@ -0,0 +1,170 @@ +-.jsonl with + * {"type":"session_meta"|"response_item","payload":{...}} records + * - Older: records are the response items themselves ({"type":"message","role":...}) + * - Legacy: flat {"role","content","timestamp"} lines (MoveZ ≤ 1.0 writer output) + */ +class CodexParser extends JsonlParser implements ParserInterface +{ + public function toolName(): string + { + return 'codex'; + } + + public function getStoragePath(string $projectPath): string + { + $os = PlatformPaths::configKey(); + $cfg = config("movez.tools.codex.storage.{$os}", ''); + return PlatformPaths::expand((string) $cfg); + } + + public function detect(string $projectPath): bool + { + return !empty($this->sessionFiles($this->getStoragePath($projectPath))); + } + + /** @return Collection */ + public function parseMetadata(string $projectPath): Collection + { + return $this->parse($projectPath); + } + + /** @return Collection */ + public function parse(string $projectPath): Collection + { + $sessions = collect(); + + foreach ($this->sessionFiles($this->getStoragePath($projectPath)) as $file) { + $session = $this->parseFile($file); + if ($session !== null) { + $sessions->push($session); + } + } + + return $sessions; + } + + private function parseFile(string $file): ?SessionDTO + { + $lines = $this->readJsonlFile($file); + if (empty($lines)) { + return null; + } + + $sessionId = $this->idFromFilename($file); + $title = null; + $cwd = null; + $turns = collect(); + + foreach ($lines as $line) { + $ts = Carbon::parse(is_string($line['timestamp'] ?? null) ? $line['timestamp'] : '@' . filemtime($file)); + $type = $line['type'] ?? null; + $payload = is_array($line['payload'] ?? null) ? $line['payload'] : $line; + + if ($type === 'session_meta') { + $sessionId = is_string($payload['id'] ?? null) ? $payload['id'] : $sessionId; + $cwd = is_string($payload['cwd'] ?? null) ? $payload['cwd'] : $cwd; + $title = is_string($payload['title'] ?? null) ? $payload['title'] : $title; + continue; + } + + // Old header line: {"id": ..., "timestamp": ..., "instructions": ...} + if ($type === null && !isset($line['role']) && isset($line['id']) && is_string($line['id'])) { + $sessionId = $line['id']; + continue; + } + + $role = $payload['role'] ?? null; + if (!in_array($role, ['user', 'assistant'], true)) { + continue; + } + if (isset($payload['type']) && $payload['type'] !== 'message') { + continue; + } + + $content = trim(ContentFlattener::flatten($payload['content'] ?? '')); + if ($content === '' || $this->isEnvironmentContext($content)) { + continue; + } + + if ($title === null && is_string($line['title'] ?? null)) { + $title = $line['title']; + } + + $turns->push(new TurnDTO(role: $role, content: $content, timestamp: $ts)); + } + + if ($turns->isEmpty()) { + return null; + } + + $firstUser = $turns->first(fn(TurnDTO $t) => $t->role === 'user'); + + return new SessionDTO( + id: $sessionId, + title: mb_substr(trim($title ?? $firstUser?->content ?? basename($file)), 0, 80), + sourceTool: $this->toolName(), + sourceMachineSha: $this->machineSha(), + createdAt: $turns->first()->timestamp, + lastActiveAt: $turns->last()->timestamp, + turns: $turns, + project: $cwd !== null ? basename(str_replace('\\', '/', rtrim($cwd, '/\\'))) : '', + ); + } + + /** Codex injects environment/instructions blocks as user messages — not real turns. */ + private function isEnvironmentContext(string $content): bool + { + return str_starts_with($content, '') + || str_starts_with($content, ''); + } + + private function idFromFilename(string $file): string + { + $name = pathinfo($file, PATHINFO_FILENAME); + + // rollout-2025-05-07T17-24-21- → + if (preg_match('/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i', $name, $m)) { + return $m[1]; + } + + return $name; + } + + /** @return string[] */ + private function sessionFiles(string $root): array + { + if (!is_dir($root)) { + return []; + } + + $files = []; + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)); + foreach ($it as $file) { + if ($file->isFile() && strtolower($file->getExtension()) === 'jsonl') { + $files[] = $file->getPathname(); + } + } + sort($files); + + return $files; + } +} diff --git a/contextbridge/app/Parsers/ContinueParser.php b/movez/app/Parsers/ContinueParser.php similarity index 93% rename from contextbridge/app/Parsers/ContinueParser.php rename to movez/app/Parsers/ContinueParser.php index 53dd97f..a6fa3f9 100644 --- a/contextbridge/app/Parsers/ContinueParser.php +++ b/movez/app/Parsers/ContinueParser.php @@ -6,6 +6,7 @@ use App\Contracts\ParserInterface; use App\DTOs\SessionDTO; use App\DTOs\TurnDTO; +use App\Support\ContentFlattener; use App\Support\PlatformPaths; use Carbon\Carbon; use Illuminate\Support\Collection; @@ -59,14 +60,14 @@ public function parse(string $projectPath): Collection $turns = collect($turnRows)->map( fn(array $t) => new TurnDTO( role: $t['role'] ?? 'user', - content: $t['content'] ?? '', + content: ContentFlattener::flatten($t['content'] ?? ''), timestamp: Carbon::parse($t['created_at'] ?? 'now'), ) ); return new SessionDTO( id: (string) $row['id'], - title: $row['title'] ?? 'Untitled', + title: (string) ($row['title'] ?? 'Untitled'), sourceTool: $this->toolName(), sourceMachineSha: substr(hash('sha256', (string) gethostname()), 0, 16), createdAt: Carbon::parse($row['created_at'] ?? 'now'), diff --git a/contextbridge/app/Parsers/CopilotCliParser.php b/movez/app/Parsers/CopilotCliParser.php similarity index 71% rename from contextbridge/app/Parsers/CopilotCliParser.php rename to movez/app/Parsers/CopilotCliParser.php index 4928c42..146e1f9 100644 --- a/contextbridge/app/Parsers/CopilotCliParser.php +++ b/movez/app/Parsers/CopilotCliParser.php @@ -6,10 +6,10 @@ use App\Contracts\ParserInterface; use App\DTOs\SessionDTO; use App\DTOs\TurnDTO; +use App\Support\ContentFlattener; use App\Support\PlatformPaths; use Carbon\Carbon; use Illuminate\Support\Collection; -use RuntimeException; class CopilotCliParser implements ParserInterface { @@ -53,29 +53,35 @@ public function parse(string $projectPath): Collection $data = json_decode((string) $raw, true); if (!is_array($data)) { - throw new RuntimeException("Invalid JSON in: {$file}"); + continue; // skip unreadable files rather than aborting the whole parse } // Copilot-cli stores an array of sessions per file $records = isset($data[0]) ? $data : [$data]; foreach ($records as $record) { - $turns = collect($record['messages'] ?? [])->map( - fn(array $msg) => new TurnDTO( - role: $msg['role'] ?? 'user', - content: $msg['content'] ?? '', + if (!is_array($record)) { + continue; + } + + $turns = collect($record['messages'] ?? []) + ->filter(fn($msg) => is_array($msg)) + ->map(fn(array $msg) => new TurnDTO( + role: ($msg['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user', + content: ContentFlattener::flatten($msg['content'] ?? ''), timestamp: Carbon::parse($msg['timestamp'] ?? 'now'), - ) - ); + )) + ->values(); $sessions->push(new SessionDTO( - id: $record['id'] ?? uniqid('copilot_', true), - title: $record['title'] ?? basename($file), + id: (string) ($record['id'] ?? pathinfo($file, PATHINFO_FILENAME)), + title: (string) ($record['title'] ?? basename($file)), sourceTool: $this->toolName(), sourceMachineSha: substr(hash('sha256', (string) gethostname()), 0, 16), createdAt: Carbon::parse($record['created_at'] ?? 'now'), lastActiveAt: Carbon::parse($record['updated_at'] ?? 'now'), turns: $turns, + project: (string) ($record['project'] ?? ''), )); } } diff --git a/contextbridge/app/Parsers/CursorParser.php b/movez/app/Parsers/CursorParser.php similarity index 93% rename from contextbridge/app/Parsers/CursorParser.php rename to movez/app/Parsers/CursorParser.php index cda8125..406a72c 100644 --- a/contextbridge/app/Parsers/CursorParser.php +++ b/movez/app/Parsers/CursorParser.php @@ -81,9 +81,9 @@ private function parseTranscript(string $file, string $root): ?SessionDTO return null; } - $turns = collect(); - $createdAt = null; - $lastAt = null; + $turns = collect(); + // Transcripts don't embed per-line timestamps; use the file's mtime + $mtime = Carbon::createFromTimestamp((int) filemtime($file)); foreach ($raw as $line) { $entry = json_decode($line, true); @@ -96,16 +96,10 @@ private function parseTranscript(string $file, string $root): ?SessionDTO continue; } - $ts = Carbon::now(); // transcripts don't embed timestamps per line - if ($createdAt === null) { - $createdAt = $ts; - } - $lastAt = $ts; - $turns->push(new TurnDTO( role: $entry['role'] === 'assistant' ? 'assistant' : 'user', content: $content, - timestamp: $ts, + timestamp: $mtime, )); } @@ -122,8 +116,8 @@ private function parseTranscript(string $file, string $root): ?SessionDTO title: $title, sourceTool: $this->toolName(), sourceMachineSha: $this->machineSha(), - createdAt: $createdAt ?? Carbon::now(), - lastActiveAt: $lastAt ?? Carbon::now(), + createdAt: $mtime, + lastActiveAt: $mtime, turns: $turns, project: $this->decodeProjectName($encodedProj), ); diff --git a/contextbridge/app/Parsers/JsonlParser.php b/movez/app/Parsers/JsonlParser.php similarity index 71% rename from contextbridge/app/Parsers/JsonlParser.php rename to movez/app/Parsers/JsonlParser.php index efd4742..7a08955 100644 --- a/contextbridge/app/Parsers/JsonlParser.php +++ b/movez/app/Parsers/JsonlParser.php @@ -9,6 +9,8 @@ abstract class JsonlParser { /** * Read a JSONL file and return an array of decoded objects. + * Streams line by line (large sessions can exceed 60 MB) and skips + * malformed lines — e.g. a half-written last line of a live session. * * @return array> */ @@ -23,21 +25,16 @@ protected function readJsonlFile(string $path): array throw new RuntimeException("Cannot open JSONL file: {$path}"); } - $result = []; - $lineNumber = 0; + $result = []; while (($line = fgets($fh)) !== false) { - $lineNumber++; $line = trim($line); if ($line === '') { continue; } $decoded = json_decode($line, true); - if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { - fclose($fh); - throw new RuntimeException( - "Invalid JSON on line {$lineNumber} of {$path}: " . json_last_error_msg() - ); + if (!is_array($decoded)) { + continue; } $result[] = $decoded; } @@ -45,4 +42,9 @@ protected function readJsonlFile(string $path): array fclose($fh); return $result; } + + protected function machineSha(): string + { + return substr(hash('sha256', (string) gethostname()), 0, 16); + } } diff --git a/contextbridge/app/Parsers/SqliteParser.php b/movez/app/Parsers/SqliteParser.php similarity index 100% rename from contextbridge/app/Parsers/SqliteParser.php rename to movez/app/Parsers/SqliteParser.php diff --git a/movez/app/Parsers/WindsurfParser.php b/movez/app/Parsers/WindsurfParser.php new file mode 100644 index 0000000..a551eb2 --- /dev/null +++ b/movez/app/Parsers/WindsurfParser.php @@ -0,0 +1,48 @@ +getStoragePath($projectPath)); + } + + public function parseMetadata(string $projectPath): Collection + { + return $this->parse($projectPath); + } + + public function parse(string $projectPath): Collection + { + throw new RuntimeException(self::UNSUPPORTED_MESSAGE); + } +} diff --git a/contextbridge/app/Providers/AppServiceProvider.php b/movez/app/Providers/AppServiceProvider.php similarity index 100% rename from contextbridge/app/Providers/AppServiceProvider.php rename to movez/app/Providers/AppServiceProvider.php diff --git a/contextbridge/app/Services/Encryptor.php b/movez/app/Services/Encryptor.php similarity index 53% rename from contextbridge/app/Services/Encryptor.php rename to movez/app/Services/Encryptor.php index 5e5ae9d..7b6c4a7 100644 --- a/contextbridge/app/Services/Encryptor.php +++ b/movez/app/Services/Encryptor.php @@ -9,19 +9,31 @@ final class Encryptor { public function __construct(private readonly string $keyPath) {} + public function keyPath(): string + { + return $this->keyPath; + } + + public function hasKey(): bool + { + return file_exists($this->keyPath); + } + public function getOrCreateKey(): string { - if (!file_exists($this->keyPath)) { + if (!$this->hasKey()) { $dir = dirname($this->keyPath); if (!is_dir($dir)) { mkdir($dir, 0700, true); } - $key = bin2hex(random_bytes(32)); // 256-bit - file_put_contents($this->keyPath, $key); + + // Restrict permissions before the secret is written + touch($this->keyPath); chmod($this->keyPath, 0600); + file_put_contents($this->keyPath, bin2hex(random_bytes(32))); // 256-bit } - return hex2bin(trim((string) file_get_contents($this->keyPath))); + return $this->readKey(); } public function encrypt(string $plaintext): string @@ -43,8 +55,16 @@ public function encrypt(string $plaintext): string public function decrypt(string $encoded): string { - $key = $this->getOrCreateKey(); - $raw = base64_decode($encoded, true); + // Never generate a key here: a fresh key can't decrypt anything and + // would hide the real problem (key not copied from the source machine). + if (!$this->hasKey()) { + throw new RuntimeException( + "No encryption key found at {$this->keyPath}. Copy ~/.movez/key from the machine that encrypted the data." + ); + } + + $key = $this->readKey(); + $raw = base64_decode(trim($encoded), true); if ($raw === false || strlen($raw) < 29) { throw new RuntimeException('Invalid ciphertext'); @@ -62,4 +82,16 @@ public function decrypt(string $encoded): string return $pt; } + + private function readKey(): string + { + $hex = trim((string) file_get_contents($this->keyPath)); + $key = strlen($hex) === 64 && ctype_xdigit($hex) ? hex2bin($hex) : false; + + if ($key === false) { + throw new RuntimeException("Encryption key at {$this->keyPath} is corrupted (expected 64 hex characters)"); + } + + return $key; + } } diff --git a/movez/app/Services/Packager.php b/movez/app/Services/Packager.php new file mode 100644 index 0000000..1aa8912 --- /dev/null +++ b/movez/app/Services/Packager.php @@ -0,0 +1,114 @@ + $sessions + */ + public function pack( + Collection $sessions, + string $outputPath, + ?ProjectConfigDTO $config = null, + string $sourceTool = 'unknown', + ?Encryptor $encryptor = null, + ): void { + $bundleData = [ + 'version' => BundleSchema::VERSION, + 'source_tool' => $sourceTool, + 'exported_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM), + 'sessions' => $sessions->map(fn(SessionDTO $s) => $s->toArray())->values()->all(), + ]; + + BundleSchema::validate($bundleData); + + $bundleJson = (string) json_encode($bundleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + if ($encryptor !== null) { + $bundleJson = $encryptor->encrypt($bundleJson); + } + + $manifest = BundleSchema::makeManifest($sourceTool, $this->machineSha(), $sessions->count(), $encryptor !== null); + + $zip = new ZipArchive(); + if ($zip->open($outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException("Cannot create archive: {$outputPath}"); + } + + $zip->addFromString('bundle.json', $bundleJson); + $zip->addFromString('manifest.json', (string) json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + if ($config !== null) { + $zip->addFromString('config.json', (string) json_encode($config->toArray(), JSON_PRETTY_PRINT)); + } + + $zip->close(); + } + + /** + * Unpack a .cbz archive and return the sessions it contains. + * + * @return Collection + * @throws RuntimeException on a missing, corrupted, invalid or undecryptable archive + */ + public function unpack(string $cbzPath, ?Encryptor $encryptor = null): Collection + { + if (!file_exists($cbzPath)) { + throw new RuntimeException("Archive not found: {$cbzPath}"); + } + + $zip = new ZipArchive(); + if ($zip->open($cbzPath) !== true) { + throw new RuntimeException("Cannot open archive: {$cbzPath}"); + } + + $bundleJson = $zip->getFromName('bundle.json'); + $manifestJson = $zip->getFromName('manifest.json'); + $zip->close(); + + if ($bundleJson === false) { + throw new RuntimeException('Archive is missing bundle.json'); + } + + $manifest = is_string($manifestJson) ? json_decode($manifestJson, true) : null; + $encrypted = is_array($manifest) && ($manifest['encrypted'] ?? false) === true; + + if ($encrypted) { + if ($encryptor === null) { + throw new RuntimeException('Archive is encrypted — an encryption key is required'); + } + $bundleJson = $encryptor->decrypt($bundleJson); + } + + $data = json_decode($bundleJson, true); + + if (!is_array($data)) { + throw new RuntimeException('bundle.json contains invalid JSON'); + } + + try { + BundleSchema::validate($data); + + return collect($data['sessions'])->map(fn(array $s) => SessionDTO::fromArray($s)); + } catch (InvalidArgumentException|\TypeError|\ErrorException $e) { + throw new RuntimeException('Invalid bundle: ' . $e->getMessage(), 0, $e); + } + } + + private function machineSha(): string + { + return substr(hash('sha256', (string) gethostname()), 0, 16); + } +} diff --git a/contextbridge/app/Services/PathMapper.php b/movez/app/Services/PathMapper.php similarity index 100% rename from contextbridge/app/Services/PathMapper.php rename to movez/app/Services/PathMapper.php diff --git a/movez/app/Services/SyncClient.php b/movez/app/Services/SyncClient.php new file mode 100644 index 0000000..b2dbf35 --- /dev/null +++ b/movez/app/Services/SyncClient.php @@ -0,0 +1,102 @@ +http = $http ?? new Client(['timeout' => 60, 'http_errors' => false]); + } + + /** + * Push sessions to the sync server (encrypted client-side). + * + * @param Collection $sessions + */ + public function push(Collection $sessions, string $token): void + { + $payload = json_encode($sessions->map(fn(SessionDTO $s) => $s->toArray())->values()->all()); + $encrypted = $this->encryptor->encrypt((string) $payload); + + $response = $this->request('POST', '/api/sync/push', $token, [ + 'json' => ['sessions' => $encrypted, 'count' => $sessions->count()], + ]); + + $this->assertSuccess($response); + } + + /** + * Pull sessions from the sync server (decrypts automatically). + * Returns an empty collection when nothing has been pushed yet. + * + * @return Collection + */ + public function pull(string $token): Collection + { + $response = $this->request('GET', '/api/sync/pull', $token); + $this->assertSuccess($response); + + $body = json_decode((string) $response->getBody(), true); + if (!is_array($body)) { + throw new RuntimeException('Sync server returned invalid JSON'); + } + + $blob = $body['sessions'] ?? null; + if ($blob === null || $blob === '') { + return collect(); + } + + if (!is_string($blob)) { + throw new RuntimeException('Sync server returned an invalid payload'); + } + + $decoded = json_decode($this->encryptor->decrypt($blob), true); + if (!is_array($decoded)) { + throw new RuntimeException('Decrypted sync payload is not valid JSON'); + } + + return collect($decoded)->map(fn(array $s) => SessionDTO::fromArray($s)); + } + + /** @param array $options */ + private function request(string $method, string $path, string $token, array $options = []): ResponseInterface + { + $options['headers'] = [ + 'Authorization' => "Bearer {$token}", + 'Accept' => 'application/json', + ]; + + try { + return $this->http->request($method, rtrim($this->baseUrl, '/') . $path, $options); + } catch (GuzzleException $e) { + throw new RuntimeException("Cannot reach sync server: {$e->getMessage()}", 0, $e); + } + } + + private function assertSuccess(ResponseInterface $response): void + { + $status = $response->getStatusCode(); + + match (true) { + $status === 401 => throw new RuntimeException('Unauthorized — check your API token'), + $status === 422 => throw new RuntimeException('Sync server rejected the payload (HTTP 422)'), + $status < 200 || $status >= 300 => throw new RuntimeException("Sync server error: HTTP {$status}"), + default => null, + }; + } +} diff --git a/contextbridge/app/Services/ToolDetector.php b/movez/app/Services/ToolDetector.php similarity index 86% rename from contextbridge/app/Services/ToolDetector.php rename to movez/app/Services/ToolDetector.php index 2de1e06..83fc75b 100644 --- a/contextbridge/app/Services/ToolDetector.php +++ b/movez/app/Services/ToolDetector.php @@ -42,16 +42,22 @@ final class ToolDetector /** * Return names of all AI tools whose storage directories exist on this machine. + * Tools marked 'supported' => false in config are skipped unless requested, + * so auto-detection never picks a tool MoveZ can't read. * * @return string[] */ - public function detect(): array + public function detect(bool $includeUnsupported = false): array { $detected = []; $os = PlatformPaths::configKey(); $tools = config('movez.tools', []); foreach ($tools as $name => $cfg) { + if (!$includeUnsupported && ($cfg['supported'] ?? true) === false) { + continue; + } + $storagePath = $cfg['storage'][$os] ?? null; if ($storagePath === null) { continue; @@ -93,6 +99,11 @@ public function getWriter(string $toolName): WriterInterface return new $class(); } + public function isSupported(string $toolName): bool + { + return (config("movez.tools.{$toolName}.supported") ?? true) !== false; + } + /** @return string[] */ public function supportedTools(): array { diff --git a/contextbridge/app/Support/BundleSchema.php b/movez/app/Support/BundleSchema.php similarity index 64% rename from contextbridge/app/Support/BundleSchema.php rename to movez/app/Support/BundleSchema.php index 5e8da90..1e076f8 100644 --- a/contextbridge/app/Support/BundleSchema.php +++ b/movez/app/Support/BundleSchema.php @@ -7,7 +7,7 @@ final class BundleSchema { - private const VERSION = '1.0'; + public const VERSION = '1.0'; private const REQUIRED_KEYS = ['version', 'sessions', 'source_tool', 'exported_at']; @@ -36,13 +36,19 @@ public static function validate(array $data): bool /** * Build a manifest array for a new bundle. */ - public static function makeManifest(string $sourceTool, string $machineSha): array - { + public static function makeManifest( + string $sourceTool, + string $machineSha, + int $sessionCount = 0, + bool $encrypted = false, + ): array { return [ - 'version' => self::VERSION, - 'source_tool' => $sourceTool, - 'source_machine_sha' => $machineSha, - 'exported_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ISO8601), + 'version' => self::VERSION, + 'source_tool' => $sourceTool, + 'machine_sha' => $machineSha, + 'exported_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM), + 'session_count' => $sessionCount, + 'encrypted' => $encrypted, ]; } } diff --git a/movez/app/Support/ContentFlattener.php b/movez/app/Support/ContentFlattener.php new file mode 100644 index 0000000..6777427 --- /dev/null +++ b/movez/app/Support/ContentFlattener.php @@ -0,0 +1,35 @@ + $sessions + * @return Collection + */ + public static function apply(Collection $sessions, ?string $projectPath): Collection + { + if ($projectPath === null || trim($projectPath) === '') { + return $sessions; + } + + $needle = self::normalise(basename(str_replace('\\', '/', rtrim($projectPath, '/\\')))); + if ($needle === '') { + return $sessions; + } + + return $sessions->filter(function (SessionDTO $session) use ($needle): bool { + $project = self::normalise($session->project); + + return $project !== '' && ($project === $needle || str_ends_with($project, '-' . $needle)); + })->values(); + } + + private static function normalise(string $name): string + { + return trim((string) preg_replace('/[^a-z0-9]+/', '-', strtolower($name)), '-'); + } +} diff --git a/movez/app/Support/SafePath.php b/movez/app/Support/SafePath.php new file mode 100644 index 0000000..5ccaf8d --- /dev/null +++ b/movez/app/Support/SafePath.php @@ -0,0 +1,43 @@ +normalisePath($projectPath); foreach ($sessions as $session) { + $sessionId = SafePath::id($session->id); + // If the session belongs to a different project, resolve its own directory. // e.g. projectPath = c:\work\personal\flutter\movez, session->project = RatWatch // → write to c--work-personal-flutter-ratwatch/ so it appears in that project. $sessionProjectPath = $projectPath; if ($session->project && strcasecmp(basename($projectPath), $session->project) !== 0) { - $sessionProjectPath = dirname($projectPath) . DIRECTORY_SEPARATOR . $session->project; + $sessionProjectPath = dirname($projectPath) . DIRECTORY_SEPARATOR . SafePath::segment($session->project); $sessionProjectPath = $this->normalisePath($sessionProjectPath); } @@ -108,7 +111,7 @@ public function write(Collection $sessions, string $projectPath): void 'uuid' => $uuid, 'timestamp' => $turn->timestamp->toIso8601ZuluString(), 'userType' => 'external', - 'entrypoint' => 'movez-import', + 'entrypoint' => 'movez-import:' . $session->sourceTool, 'cwd' => $sessionProjectPath, 'sessionId' => $session->id, 'version' => $this->getClaudeCodeVersion(), @@ -118,7 +121,7 @@ public function write(Collection $sessions, string $projectPath): void $prevUuid = $uuid; } - $filePath = $sessionDir . DIRECTORY_SEPARATOR . $session->id . '.jsonl'; + $filePath = $sessionDir . DIRECTORY_SEPARATOR . $sessionId . '.jsonl'; file_put_contents($filePath, implode("\n", $lines) . "\n"); // Register with Claude desktop app so it appears in the app's history @@ -127,12 +130,17 @@ public function write(Collection $sessions, string $projectPath): void } /** - * Normalise a path to match Claude desktop app format: + * Normalise a Windows path to match Claude desktop app format: * - Use backslashes: C:/Work/... → C:\Work\... * - Uppercase drive letter: c:\Work\... → C:\Work\... + * POSIX paths (macOS/Linux) are returned unchanged. */ private function normalisePath(string $path): string { + if (!preg_match('#^[A-Za-z]:[\\\\/]#', $path)) { + return $path; + } + $path = str_replace('/', '\\', $path); return preg_replace_callback('/^([a-z])(:\\\\)/', fn($m) => strtoupper($m[1]) . $m[2], $path) ?? $path; } @@ -161,7 +169,8 @@ private function createDesktopRegistryEntry(SessionDTO $session, string $session return; } - $localId = 'local_' . $this->uuid4(); + // Re-importing the same session must update its entry, not add a duplicate + $localId = $this->findRegistryEntry($registryDir, $session->id) ?? ('local_' . $this->uuid4()); $createdMs = $session->createdAt->getTimestamp() * 1000; $lastTurn = $session->turns->last(); $lastMs = $lastTurn ? ($lastTurn->timestamp->getTimestamp() * 1000) : $createdMs; @@ -173,7 +182,7 @@ private function createDesktopRegistryEntry(SessionDTO $session, string $session 'originCwd' => $sessionProjectPath, 'createdAt' => $createdMs, 'lastActivityAt' => $lastMs, - 'model' => 'claude-opus-4-6', + 'model' => (string) config('movez.tools.claude-code.default_model', 'claude-opus-4-6'), 'isArchived' => false, 'title' => $session->title ?: 'Imported session', 'permissionMode' => 'acceptEdits', @@ -186,11 +195,23 @@ private function createDesktopRegistryEntry(SessionDTO $session, string $session ); } - private function getDesktopRegistryDir(): ?string + /** Return the existing local_ id registered for a CLI session id, if any. */ + private function findRegistryEntry(string $registryDir, string $cliSessionId): ?string { - $appData = getenv('APPDATA') ?: (getenv('USERPROFILE') . '/AppData/Roaming'); - $base = $appData . '/Claude/claude-code-sessions'; - if (!is_dir($base)) { + foreach (glob($registryDir . DIRECTORY_SEPARATOR . 'local_*.json') ?: [] as $file) { + $meta = json_decode((string) file_get_contents($file), true); + if (is_array($meta) && ($meta['cliSessionId'] ?? null) === $cliSessionId) { + return pathinfo($file, PATHINFO_FILENAME); + } + } + + return null; + } + + protected function getDesktopRegistryDir(): ?string + { + $base = $this->configPath('desktop_registry'); + if ($base === '' || !is_dir($base)) { return null; } $machineDirs = glob($base . '/*', GLOB_ONLYDIR); @@ -206,24 +227,34 @@ private function getDesktopRegistryDir(): ?string private function getClaudeCodeVersion(): string { - // Try to detect the installed Claude Code version from the Roaming directory - $roaming = getenv('APPDATA') ?: (getenv('USERPROFILE') . '/AppData/Roaming'); - $versionDir = $roaming . '/Claude/claude-code'; - if (is_dir($versionDir)) { - $entries = scandir($versionDir, SCANDIR_SORT_DESCENDING); - foreach ($entries as $entry) { + // Detect the newest installed Claude Code version from the desktop app's directory + $versionDir = $this->configPath('desktop_versions'); + $versions = []; + if ($versionDir !== '' && is_dir($versionDir)) { + foreach (scandir($versionDir) ?: [] as $entry) { if (preg_match('/^\d+\.\d+\.\d+$/', $entry) && is_dir($versionDir . '/' . $entry)) { - return $entry; + $versions[] = $entry; } } } - return '2.1.78'; + + if (!empty($versions)) { + usort($versions, 'version_compare'); + return end($versions); + } + + return (string) config('movez.tools.claude-code.default_version', '2.1.78'); + } + + protected function getStoragePath(): string + { + return $this->configPath('storage'); } - private function getStoragePath(): string + private function configPath(string $key): string { $os = PlatformPaths::configKey(); - $cfg = config('movez.tools.claude-code.storage.' . $os, ''); + $cfg = config("movez.tools.claude-code.{$key}.{$os}", ''); return PlatformPaths::expand((string) $cfg); } diff --git a/movez/app/Writers/CodexWriter.php b/movez/app/Writers/CodexWriter.php new file mode 100644 index 0000000..7691f1b --- /dev/null +++ b/movez/app/Writers/CodexWriter.php @@ -0,0 +1,78 @@ +-.jsonl + */ +class CodexWriter extends AbstractWriter implements WriterInterface +{ + public function toolName(): string + { + return 'codex'; + } + + /** @param Collection $sessions */ + public function write(Collection $sessions, string $projectPath): void + { + $storagePath = $this->getStoragePath(); + + foreach ($sessions as $session) { + $id = SafePath::id($session->id); + $created = $session->createdAt->copy()->utc(); + $dir = $storagePath . DIRECTORY_SEPARATOR . $created->format('Y') + . DIRECTORY_SEPARATOR . $created->format('m') + . DIRECTORY_SEPARATOR . $created->format('d'); + + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $lines = []; + $lines[] = json_encode([ + 'timestamp' => $created->toIso8601ZuluString(), + 'type' => 'session_meta', + 'payload' => [ + 'id' => $session->id, + 'timestamp' => $created->toIso8601ZuluString(), + 'cwd' => $projectPath, + 'originator' => 'movez-import:' . $session->sourceTool, + 'title' => $session->title, + ], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + foreach ($session->turns as $turn) { + $lines[] = json_encode([ + 'timestamp' => $turn->timestamp->copy()->utc()->toIso8601ZuluString(), + 'type' => 'response_item', + 'payload' => [ + 'type' => 'message', + 'role' => $turn->role, + 'content' => [[ + 'type' => $turn->role === 'user' ? 'input_text' : 'output_text', + 'text' => $turn->content, + ]], + ], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $file = $dir . DIRECTORY_SEPARATOR . 'rollout-' . $created->format('Y-m-d\TH-i-s') . '-' . $id . '.jsonl'; + file_put_contents($file, implode("\n", $lines) . "\n"); + } + } + + protected function getStoragePath(): string + { + $os = PlatformPaths::configKey(); + $cfg = config('movez.tools.codex.storage.' . $os, ''); + return PlatformPaths::expand((string) $cfg); + } +} diff --git a/contextbridge/app/Writers/CopilotCliWriter.php b/movez/app/Writers/CopilotCliWriter.php similarity index 87% rename from contextbridge/app/Writers/CopilotCliWriter.php rename to movez/app/Writers/CopilotCliWriter.php index ecf2a82..f8f0c40 100644 --- a/contextbridge/app/Writers/CopilotCliWriter.php +++ b/movez/app/Writers/CopilotCliWriter.php @@ -6,6 +6,7 @@ use App\Contracts\WriterInterface; use App\DTOs\SessionDTO; use App\Support\PlatformPaths; +use App\Support\SafePath; use Illuminate\Support\Collection; class CopilotCliWriter extends AbstractWriter implements WriterInterface @@ -27,6 +28,7 @@ public function write(Collection $sessions, string $projectPath): void $record = [ 'id' => $session->id, 'title' => $session->title, + 'project' => $session->project, 'created_at' => $session->createdAt->toIso8601String(), 'updated_at' => $session->lastActiveAt->toIso8601String(), 'messages' => $session->turns->map(fn($turn) => [ @@ -36,12 +38,12 @@ public function write(Collection $sessions, string $projectPath): void ])->values()->all(), ]; - $path = $storagePath . DIRECTORY_SEPARATOR . $session->id . '.json'; + $path = $storagePath . DIRECTORY_SEPARATOR . SafePath::id($session->id) . '.json'; file_put_contents($path, json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } - private function getStoragePath(): string + protected function getStoragePath(): string { $os = PlatformPaths::configKey(); $cfg = config('movez.tools.copilot-cli.storage.' . $os, ''); diff --git a/contextbridge/app/Writers/CursorWriter.php b/movez/app/Writers/CursorWriter.php similarity index 99% rename from contextbridge/app/Writers/CursorWriter.php rename to movez/app/Writers/CursorWriter.php index ea2264f..dc03e25 100644 --- a/contextbridge/app/Writers/CursorWriter.php +++ b/movez/app/Writers/CursorWriter.php @@ -7,6 +7,7 @@ use App\DTOs\SessionDTO; use App\DTOs\TurnDTO; use App\Support\PlatformPaths; +use App\Support\SafePath; use Illuminate\Support\Collection; use PDO; @@ -26,18 +27,20 @@ public function write(Collection $sessions, string $projectPath): void $sessionsByProject = []; foreach ($sessions as $session) { + $sessionId = SafePath::id($session->id); + // Route to the correct project directory. // If the session has a project name that differs from the projectPath basename, // resolve it as a sibling directory (same parent). $sessionProjectPath = $projectPath; if ($session->project && strcasecmp(basename($projectPath), $session->project) !== 0) { - $sessionProjectPath = dirname($projectPath) . DIRECTORY_SEPARATOR . $session->project; + $sessionProjectPath = dirname($projectPath) . DIRECTORY_SEPARATOR . SafePath::segment($session->project); } $encoded = $this->encodeProjectPath($sessionProjectPath); $sessionDir = $storagePath . DIRECTORY_SEPARATOR . $encoded . DIRECTORY_SEPARATOR . 'agent-transcripts' - . DIRECTORY_SEPARATOR . $session->id; + . DIRECTORY_SEPARATOR . $sessionId; if (!is_dir($sessionDir)) { mkdir($sessionDir, 0755, true); @@ -72,7 +75,7 @@ public function write(Collection $sessions, string $projectPath): void } } - $filePath = $sessionDir . DIRECTORY_SEPARATOR . $session->id . '.jsonl'; + $filePath = $sessionDir . DIRECTORY_SEPARATOR . $sessionId . '.jsonl'; file_put_contents($filePath, implode("\n", $lines) . "\n"); // ── Layer 2: cursorDiskKV in global state.vscdb ─────────────────── diff --git a/movez/app/Writers/WindsurfWriter.php b/movez/app/Writers/WindsurfWriter.php new file mode 100644 index 0000000..9dbd9e0 --- /dev/null +++ b/movez/app/Writers/WindsurfWriter.php @@ -0,0 +1,27 @@ +create(); diff --git a/contextbridge/bootstrap/providers.php b/movez/bootstrap/providers.php similarity index 71% rename from contextbridge/bootstrap/providers.php rename to movez/bootstrap/providers.php index 38b258d..84c7d4d 100644 --- a/contextbridge/bootstrap/providers.php +++ b/movez/bootstrap/providers.php @@ -1,5 +1,7 @@ [ - App\Providers\AppServiceProvider::class, - ], + // AppServiceProvider is registered in bootstrap/providers.php + 'providers' => [], ]; diff --git a/contextbridge/config/commands.php b/movez/config/commands.php similarity index 99% rename from contextbridge/config/commands.php rename to movez/config/commands.php index 8c45042..c9fc9d0 100644 --- a/contextbridge/config/commands.php +++ b/movez/config/commands.php @@ -1,5 +1,7 @@ env('CB_KEY_PATH', ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE'] ?? '') . '/.movez/key'), + 'key_path' => env('MOVEZ_KEY_PATH', $home . '/.movez/key'), + + 'token_path' => env('MOVEZ_TOKEN_PATH', $home . '/.movez/token'), + + // Sync defaults; overridable per command with --server / --token. + // Prefer MOVEZ_TOKEN over --token: arguments are visible in process lists. + 'server_url' => env('MOVEZ_SERVER_URL'), - 'token_path' => env('CB_TOKEN_PATH', ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE'] ?? '') . '/.movez/token'), + 'token' => env('MOVEZ_TOKEN'), 'tools' => [ 'cursor' => [ @@ -34,9 +44,9 @@ 'Linux' => '~/.config/Windsurf/User/workspaceStorage', 'Windows' => '%APPDATA%/Windsurf/User/workspaceStorage', ], - 'db_file' => 'state.vscdb', - 'db_key' => 'workbench.panel.aichat.view.aichat.chatdata', - 'format' => 'sqlite', + 'format' => 'protobuf', + // Cascade conversations are encrypted — read/write not supported yet + 'supported' => false, ], 'claude-code' => [ 'storage' => [ @@ -44,13 +54,27 @@ 'Linux' => '~/.claude/projects', 'Windows' => '%USERPROFILE%/.claude/projects', ], + // Claude desktop app session registry — imported sessions must be + // registered here to show up in the desktop app's history. + 'desktop_registry' => [ + 'Darwin' => '~/Library/Application Support/Claude/claude-code-sessions', + 'Linux' => '~/.config/Claude/claude-code-sessions', + 'Windows' => '%APPDATA%/Claude/claude-code-sessions', + ], + 'desktop_versions' => [ + 'Darwin' => '~/Library/Application Support/Claude/claude-code', + 'Linux' => '~/.config/Claude/claude-code', + 'Windows' => '%APPDATA%/Claude/claude-code', + ], + 'default_model' => env('MOVEZ_CLAUDE_MODEL', 'claude-opus-4-6'), + 'default_version' => '2.1.78', 'format' => 'jsonl', ], 'codex' => [ 'storage' => [ 'Darwin' => '~/.codex/sessions', 'Linux' => '~/.codex/sessions', - 'Windows' => '%APPDATA%/codex/sessions', + 'Windows' => '%USERPROFILE%/.codex/sessions', ], 'format' => 'jsonl', ], @@ -58,7 +82,7 @@ 'storage' => [ 'Darwin' => '~/.copilot/sessions', 'Linux' => '~/.copilot/sessions', - 'Windows' => '%APPDATA%/copilot/sessions', + 'Windows' => '%USERPROFILE%/.copilot/sessions', ], 'format' => 'json', ], @@ -74,7 +98,7 @@ 'storage' => [ 'Darwin' => '~/.continue', 'Linux' => '~/.continue', - 'Windows' => '%APPDATA%/continue', + 'Windows' => '%USERPROFILE%/.continue', ], 'db_file' => 'sessions.db', 'format' => 'sqlite', diff --git a/contextbridge/contextbridge b/movez/movez similarity index 98% rename from contextbridge/contextbridge rename to movez/movez index 33aa752..a2d124f 100644 --- a/contextbridge/contextbridge +++ b/movez/movez @@ -1,6 +1,8 @@ #!/usr/bin/env php 'user', 'cwd' => $cwd, 'message' => ['role' => 'user', 'content' => $text], 'timestamp' => '2026-01-15T10:00:00Z']), + json_encode(['type' => 'assistant', 'cwd' => $cwd, 'message' => ['role' => 'assistant', 'content' => [['type' => 'text', 'text' => 'ok']]], 'timestamp' => '2026-01-15T10:01:00Z']), + ]) . "\n"); +} + +beforeEach(function (): void { + $this->os = PlatformPaths::configKey(); +}); + +it('export writes a .cbz bundle that import can read back', function (): void { + withTempDir(function (string $dir): void { + seedClaudeSession($dir . '/claude', 'sess-a', '/work/alpha', 'Alpha task'); + config([ + "movez.tools.claude-code.storage.{$this->os}" => $dir . '/claude', + "movez.tools.codex.storage.{$this->os}" => $dir . '/codex', + ]); + + $this->artisan('export', ['--tool' => 'claude-code', '--output' => $dir . '/out.cbz']) + ->expectsOutputToContain('Exported 1 session(s)') + ->assertExitCode(0); + + $zip = new ZipArchive(); + expect($zip->open($dir . '/out.cbz'))->toBeTrue(); + expect($zip->locateName('bundle.json'))->not->toBeFalse() + ->and($zip->locateName('manifest.json'))->not->toBeFalse(); + $zip->close(); + + $this->artisan('import', ['--input' => $dir . '/out.cbz', '--tool' => 'codex', '--project' => '/work/alpha']) + ->expectsOutputToContain('Imported 1 session(s) to codex') + ->assertExitCode(0); + + expect(glob($dir . '/codex/*/*/*/rollout-*-sess-a.jsonl'))->toHaveCount(1); + }); +}); + +it('export --project only includes sessions for that project', function (): void { + withTempDir(function (string $dir): void { + seedClaudeSession($dir . '/claude', 'sess-a', '/work/alpha', 'Alpha task'); + seedClaudeSession($dir . '/claude', 'sess-b', '/work/beta', 'Beta task'); + config(["movez.tools.claude-code.storage.{$this->os}" => $dir . '/claude']); + + $this->artisan('export', ['--tool' => 'claude-code', '--project' => '/work/beta', '--output' => $dir . '/beta.json']) + ->expectsOutputToContain('Exported 1 session(s)') + ->assertExitCode(0); + + $data = json_decode((string) file_get_contents($dir . '/beta.json'), true); + expect(array_column($data, 'id'))->toBe(['sess-b']); + }); +}); + +it('encrypted export round-trips with the same key', function (): void { + withTempDir(function (string $dir): void { + seedClaudeSession($dir . '/claude', 'sess-a', '/work/alpha', 'Secret task'); + config([ + 'movez.key_path' => $dir . '/key', + "movez.tools.claude-code.storage.{$this->os}" => $dir . '/claude', + "movez.tools.copilot-cli.storage.{$this->os}" => $dir . '/copilot', + ]); + app()->forgetInstance(\App\Services\Encryptor::class); + + $this->artisan('export', ['--tool' => 'claude-code', '--output' => $dir . '/secret.cbz', '--encrypt' => true]) + ->assertExitCode(0); + + $zip = new ZipArchive(); + $zip->open($dir . '/secret.cbz'); + expect((string) $zip->getFromName('bundle.json'))->not->toContain('Secret task'); + $zip->close(); + + $this->artisan('import', ['--input' => $dir . '/secret.cbz', '--tool' => 'copilot-cli']) + ->assertExitCode(0); + + expect(file_exists($dir . '/copilot/sess-a.json'))->toBeTrue(); + }); +}); + +it('import fails cleanly on an invalid bundle', function (): void { + withTempDir(function (string $dir): void { + $zip = new ZipArchive(); + $zip->open($dir . '/bad.cbz', ZipArchive::CREATE); + $zip->addFromString('bundle.json', '{"version":"1.0"}'); + $zip->close(); + + $this->artisan('import', ['--input' => $dir . '/bad.cbz', '--tool' => 'codex']) + ->expectsOutputToContain('Cannot read bundle') + ->assertExitCode(1); + }); +}); + +it('windsurf is reported as unsupported rather than silently empty', function (): void { + $this->artisan('export', ['--tool' => 'windsurf', '--output' => sys_get_temp_dir() . '/never.cbz']) + ->expectsOutputToContain('not supported yet') + ->assertExitCode(1); +}); + +it('inspire boilerplate command is gone', function (): void { + expect(array_keys(\Illuminate\Support\Facades\Artisan::all()))->not->toContain('inspire'); +}); diff --git a/contextbridge/tests/Feature/Writers/CursorRoundTripTest.php b/movez/tests/Feature/Writers/CursorRoundTripTest.php similarity index 88% rename from contextbridge/tests/Feature/Writers/CursorRoundTripTest.php rename to movez/tests/Feature/Writers/CursorRoundTripTest.php index 7500da3..2f00a76 100644 --- a/contextbridge/tests/Feature/Writers/CursorRoundTripTest.php +++ b/movez/tests/Feature/Writers/CursorRoundTripTest.php @@ -29,6 +29,11 @@ protected function getStoragePath(): string { return $this->fakePath; } + + // Never touch the real Cursor databases in tests + protected function writeToGlobalDb(\App\DTOs\SessionDTO $session): void {} + + protected function writeToWorkspaceDbs(array $sessionsByProject): void {} }; $writer->write(collect([$session]), $dir); diff --git a/contextbridge/tests/Pest.php b/movez/tests/Pest.php similarity index 95% rename from contextbridge/tests/Pest.php rename to movez/tests/Pest.php index 1810baf..3bff293 100644 --- a/contextbridge/tests/Pest.php +++ b/movez/tests/Pest.php @@ -9,7 +9,7 @@ */ function withTempDir(\Closure $callback): void { - $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cb_test_' . uniqid('', true); + $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'movez_test_' . uniqid('', true); mkdir($dir, 0755, true); try { diff --git a/contextbridge/tests/TestCase.php b/movez/tests/TestCase.php similarity index 83% rename from contextbridge/tests/TestCase.php rename to movez/tests/TestCase.php index a3eb9fc..0f2d903 100644 --- a/contextbridge/tests/TestCase.php +++ b/movez/tests/TestCase.php @@ -1,5 +1,7 @@ 'user', 'message' => ['role' => 'user', 'content' => 'first'], 'timestamp' => '2026-01-15T10:00:00Z']), + '{"type":"assistant","message":{"role":"assist', // half-written line of a live session + json_encode(['type' => 'assistant', 'message' => ['role' => 'assistant', 'content' => [['type' => 'text', 'text' => 'reply']]], 'timestamp' => '2026-01-15T10:01:00Z']), + ])); + + config([storageKey('claude-code') => $dir]); + $sessions = (new ClaudeCodeParser())->parse('/any'); + + expect($sessions)->toHaveCount(1) + ->and($sessions->first()->turns->pluck('content')->all())->toBe(['first', 'reply']); + }); +}); + +it('Codex parser reads current rollout files in dated subdirectories', function (): void { + withTempDir(function (string $dir): void { + $day = $dir . '/2026/01/15'; + mkdir($day, 0755, true); + file_put_contents($day . '/rollout-2026-01-15T10-00-00-7f3e1c2a-0000-4000-8000-000000000001.jsonl', implode("\n", [ + json_encode(['timestamp' => '2026-01-15T10:00:00Z', 'type' => 'session_meta', 'payload' => ['id' => '7f3e1c2a-0000-4000-8000-000000000001', 'cwd' => '/home/dev/shop']]), + json_encode(['timestamp' => '2026-01-15T10:00:01Z', 'type' => 'response_item', 'payload' => ['type' => 'message', 'role' => 'user', 'content' => [['type' => 'input_text', 'text' => 'cwd']]]]), + json_encode(['timestamp' => '2026-01-15T10:00:02Z', 'type' => 'response_item', 'payload' => ['type' => 'message', 'role' => 'user', 'content' => [['type' => 'input_text', 'text' => 'Add a cart']]]]), + json_encode(['timestamp' => '2026-01-15T10:00:03Z', 'type' => 'response_item', 'payload' => ['type' => 'reasoning', 'summary' => []]]), + json_encode(['timestamp' => '2026-01-15T10:00:04Z', 'type' => 'response_item', 'payload' => ['type' => 'message', 'role' => 'assistant', 'content' => [['type' => 'output_text', 'text' => 'Done']]]]), + ])); + + config([storageKey('codex') => $dir]); + $session = (new CodexParser())->parse('/any')->first(); + + expect($session)->toBeInstanceOf(SessionDTO::class) + ->and($session->id)->toBe('7f3e1c2a-0000-4000-8000-000000000001') + ->and($session->project)->toBe('shop') + ->and($session->title)->toBe('Add a cart') + ->and($session->turns->pluck('content')->all())->toBe(['Add a cart', 'Done']); + }); +}); + +it('Codex writer output round-trips through the Codex parser', function (): void { + withTempDir(function (string $dir): void { + config([storageKey('codex') => $dir]); + + $session = new SessionDTO( + id: 'imported-1', title: 'From Cursor', sourceTool: 'cursor', sourceMachineSha: 'x', + createdAt: Carbon::parse('2026-02-01T09:00:00Z'), lastActiveAt: Carbon::parse('2026-02-01T09:05:00Z'), + turns: collect([ + new TurnDTO('user', 'Question', Carbon::parse('2026-02-01T09:00:00Z')), + new TurnDTO('assistant', 'Answer', Carbon::parse('2026-02-01T09:01:00Z')), + ]), + ); + + (new CodexWriter())->write(collect([$session]), '/home/dev/app'); + + expect(glob($dir . '/2026/02/01/rollout-*-imported-1.jsonl'))->toHaveCount(1); + + $parsed = (new CodexParser())->parse('/any')->first(); + expect($parsed->id)->toBe('imported-1') + ->and($parsed->title)->toBe('From Cursor') + ->and($parsed->project)->toBe('app') + ->and($parsed->turns)->toHaveCount(2); + }); +}); + +it('Cline parser reads per-task api_conversation_history.json files', function (): void { + withTempDir(function (string $dir): void { + $tasks = $dir . '/ext-1.0/data/tasks'; + mkdir($tasks . '/1736935200000', 0755, true); + file_put_contents($tasks . '/1736935200000/api_conversation_history.json', json_encode([ + ['role' => 'user', 'content' => [['type' => 'text', 'text' => "\nFix the login bug\n"]]], + ['role' => 'assistant', 'content' => [['type' => 'text', 'text' => 'Looking at auth.ts']]], + ])); + + config([storageKey('cline') => $dir . '/ext-*/data/tasks']); + $session = (new ClineParser())->parse('/any')->first(); + + expect($session->id)->toBe('1736935200000') + ->and($session->title)->toBe('Fix the login bug') + ->and($session->turns)->toHaveCount(2) + ->and($session->turns->last()->content)->toBe('Looking at auth.ts'); + }); +}); + +it('Copilot CLI parser flattens block content and skips unreadable files', function (): void { + withTempDir(function (string $dir): void { + file_put_contents($dir . '/broken.json', '{not json'); + file_put_contents($dir . '/good.json', json_encode([ + 'id' => 'cp-1', 'title' => 'Copilot chat', 'created_at' => '2026-01-15T10:00:00Z', + 'messages' => [ + ['role' => 'user', 'content' => [['type' => 'text', 'text' => 'hi']], 'timestamp' => '2026-01-15T10:00:00Z'], + ['role' => 'assistant', 'content' => 'hello', 'timestamp' => '2026-01-15T10:00:01Z'], + ], + ])); + + config([storageKey('copilot-cli') => $dir]); + $sessions = (new CopilotCliParser())->parse('/any'); + + expect($sessions)->toHaveCount(1) + ->and($sessions->first()->turns->pluck('content')->all())->toBe(['hi', 'hello']); + }); +}); + +it('Windsurf parser fails explicitly instead of returning Cursor sessions', function (): void { + expect(fn() => (new WindsurfParser())->parse('/any')) + ->toThrow(RuntimeException::class, 'not supported yet'); +}); diff --git a/contextbridge/tests/Unit/Services/EncryptorTest.php b/movez/tests/Unit/Services/EncryptorTest.php similarity index 73% rename from contextbridge/tests/Unit/Services/EncryptorTest.php rename to movez/tests/Unit/Services/EncryptorTest.php index d4639cd..058a424 100644 --- a/contextbridge/tests/Unit/Services/EncryptorTest.php +++ b/movez/tests/Unit/Services/EncryptorTest.php @@ -63,3 +63,21 @@ expect(fn() => $enc2->decrypt($ct))->toThrow(RuntimeException::class); }); }); + +it('decrypt never generates a key and explains how to fix it', function (): void { + withTempDir(function (string $dir): void { + $source = new Encryptor($dir . '/source-key'); + $ct = $source->encrypt('payload'); + + $other = new Encryptor($dir . '/missing/key'); + expect(fn() => $other->decrypt($ct))->toThrow(RuntimeException::class, 'No encryption key found') + ->and(file_exists($dir . '/missing/key'))->toBeFalse(); + }); +}); + +it('rejects a corrupted key file', function (): void { + withTempDir(function (string $dir): void { + file_put_contents($dir . '/key', 'not-hex'); + expect(fn() => (new Encryptor($dir . '/key'))->encrypt('x'))->toThrow(RuntimeException::class, 'corrupted'); + }); +}); diff --git a/contextbridge/tests/Unit/Services/PackagerTest.php b/movez/tests/Unit/Services/PackagerTest.php similarity index 67% rename from contextbridge/tests/Unit/Services/PackagerTest.php rename to movez/tests/Unit/Services/PackagerTest.php index a706e48..758560e 100644 --- a/contextbridge/tests/Unit/Services/PackagerTest.php +++ b/movez/tests/Unit/Services/PackagerTest.php @@ -95,3 +95,37 @@ function makePackageSession(string $id = 'sess1'): SessionDTO expect(fn() => $packager->unpack($badPath))->toThrow(RuntimeException::class); }); }); + +it('encrypted bundles round-trip and record encryption in the manifest', function (): void { + withTempDir(function (string $dir): void { + $packager = new Packager(); + $encryptor = new \App\Services\Encryptor($dir . '/key'); + $cbz = $dir . '/secret.cbz'; + + $packager->pack(collect([makePackageSession('enc-1')]), $cbz, null, 'cursor', $encryptor); + + $zip = new ZipArchive(); + $zip->open($cbz); + $manifest = json_decode((string) $zip->getFromName('manifest.json'), true); + $raw = (string) $zip->getFromName('bundle.json'); + $zip->close(); + + expect($manifest['encrypted'])->toBeTrue() + ->and($manifest['session_count'])->toBe(1) + ->and($raw)->not->toContain('enc-1') + ->and($packager->unpack($cbz, $encryptor)->first()->id)->toBe('enc-1') + ->and(fn() => $packager->unpack($cbz))->toThrow(RuntimeException::class, 'encrypted'); + }); +}); + +it('unpack reports schema problems as RuntimeException', function (): void { + withTempDir(function (string $dir): void { + $cbz = $dir . '/bad-schema.cbz'; + $zip = new ZipArchive(); + $zip->open($cbz, ZipArchive::CREATE); + $zip->addFromString('bundle.json', '{"version":"1.0"}'); + $zip->close(); + + expect(fn() => (new Packager())->unpack($cbz))->toThrow(RuntimeException::class, 'Invalid bundle'); + }); +}); diff --git a/contextbridge/tests/Unit/Services/PathMapperTest.php b/movez/tests/Unit/Services/PathMapperTest.php similarity index 100% rename from contextbridge/tests/Unit/Services/PathMapperTest.php rename to movez/tests/Unit/Services/PathMapperTest.php diff --git a/movez/tests/Unit/Services/SyncClientTest.php b/movez/tests/Unit/Services/SyncClientTest.php new file mode 100644 index 0000000..6676099 --- /dev/null +++ b/movez/tests/Unit/Services/SyncClientTest.php @@ -0,0 +1,80 @@ +> $history */ +function syncClientWith(Encryptor $enc, array $responses, array &$history = []): SyncClient +{ + $stack = HandlerStack::create(new MockHandler($responses)); + $stack->push(Middleware::history($history)); + + return new SyncClient('https://sync.test/', $enc, new Client(['handler' => $stack, 'http_errors' => false])); +} + +function syncSession(): SessionDTO +{ + return new SessionDTO( + id: 's1', title: 'Synced', sourceTool: 'cursor', sourceMachineSha: 'abc', + createdAt: Carbon::parse('2026-01-15'), lastActiveAt: Carbon::parse('2026-01-15'), + turns: collect([new TurnDTO('user', 'hello', Carbon::parse('2026-01-15'))]), + ); +} + +it('push sends an encrypted sessions blob with a bearer token', function (): void { + withTempDir(function (string $dir): void { + $enc = new Encryptor($dir . '/key'); + $history = []; + $client = syncClientWith($enc, [new Response(200, [], '{"status":"ok"}')], $history); + + $client->push(collect([syncSession()]), 'tok123'); + + $request = $history[0]['request']; + $body = json_decode((string) $request->getBody(), true); + + expect((string) $request->getUri())->toBe('https://sync.test/api/sync/push') + ->and($request->getHeaderLine('Authorization'))->toBe('Bearer tok123') + ->and($body['count'])->toBe(1) + ->and($body['sessions'])->not->toContain('hello') + ->and(json_decode($enc->decrypt($body['sessions']), true)[0]['id'])->toBe('s1'); + }); +}); + +it('pull decrypts the sessions returned by the server', function (): void { + withTempDir(function (string $dir): void { + $enc = new Encryptor($dir . '/key'); + $blob = $enc->encrypt((string) json_encode([syncSession()->toArray()])); + + $sessions = syncClientWith($enc, [new Response(200, [], (string) json_encode(['sessions' => $blob, 'count' => 1]))]) + ->pull('tok'); + + expect($sessions)->toHaveCount(1) + ->and($sessions->first()->title)->toBe('Synced'); + }); +}); + +it('pull returns an empty collection when nothing has been pushed', function (): void { + withTempDir(function (string $dir): void { + $sessions = syncClientWith(new Encryptor($dir . '/key'), [new Response(200, [], '{"sessions":null,"count":0}')]) + ->pull('tok'); + + expect($sessions)->toBeEmpty(); + }); +}); + +it('throws a readable error on 401', function (): void { + withTempDir(function (string $dir): void { + $client = syncClientWith(new Encryptor($dir . '/key'), [new Response(401)]); + + expect(fn() => $client->pull('bad'))->toThrow(RuntimeException::class, 'Unauthorized'); + }); +}); diff --git a/contextbridge/tests/Unit/Support/BundleSchemaTest.php b/movez/tests/Unit/Support/BundleSchemaTest.php similarity index 79% rename from contextbridge/tests/Unit/Support/BundleSchemaTest.php rename to movez/tests/Unit/Support/BundleSchemaTest.php index eedf6e5..0e20f29 100644 --- a/contextbridge/tests/Unit/Support/BundleSchemaTest.php +++ b/movez/tests/Unit/Support/BundleSchemaTest.php @@ -46,12 +46,12 @@ ]))->toThrow(InvalidArgumentException::class, 'exported_at'); }); -it('makeManifest includes required fields', function (): void { - $manifest = BundleSchema::makeManifest('claude-code', 'deadbeef01234567'); - - expect($manifest)->toHaveKey('version') - ->toHaveKey('source_tool') - ->toHaveKey('source_machine_sha') - ->toHaveKey('exported_at') - ->and($manifest['source_tool'])->toBe('claude-code'); +it('makeManifest includes the spec fields', function (): void { + $manifest = BundleSchema::makeManifest('claude-code', 'deadbeef01234567', 3); + + expect($manifest)->toHaveKeys(['version', 'source_tool', 'machine_sha', 'exported_at', 'session_count', 'encrypted']) + ->and($manifest['source_tool'])->toBe('claude-code') + ->and($manifest['machine_sha'])->toBe('deadbeef01234567') + ->and($manifest['session_count'])->toBe(3) + ->and($manifest['encrypted'])->toBeFalse(); }); diff --git a/contextbridge/tests/Unit/Support/PlatformPathsTest.php b/movez/tests/Unit/Support/PlatformPathsTest.php similarity index 80% rename from contextbridge/tests/Unit/Support/PlatformPathsTest.php rename to movez/tests/Unit/Support/PlatformPathsTest.php index 305d8cf..6966f69 100644 --- a/contextbridge/tests/Unit/Support/PlatformPathsTest.php +++ b/movez/tests/Unit/Support/PlatformPathsTest.php @@ -4,41 +4,29 @@ use App\Support\PlatformPaths; it('expand resolves tilde to HOME on non-Windows', function (): void { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('Tilde expansion is not used on Windows'); - } - $home = getenv('HOME') ?: '/root'; $result = PlatformPaths::expand('~/.config/tool'); expect($result)->toBe($home . '/.config/tool'); -}); +})->skip(PHP_OS_FAMILY === 'Windows', 'Tilde expansion is not used on Windows'); it('expand resolves %APPDATA% on Windows', function (): void { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('APPDATA expansion is Windows-only'); - } - $appData = getenv('APPDATA') ?: 'C:/Users/test/AppData/Roaming'; putenv("APPDATA={$appData}"); $result = PlatformPaths::expand('%APPDATA%/Tool'); expect($result)->toBe("{$appData}/Tool"); -}); +})->skip(PHP_OS_FAMILY !== 'Windows', 'APPDATA expansion is Windows-only'); it('expand resolves %USERPROFILE% on Windows', function (): void { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('USERPROFILE expansion is Windows-only'); - } - $profile = getenv('USERPROFILE') ?: 'C:/Users/test'; putenv("USERPROFILE={$profile}"); $result = PlatformPaths::expand('%USERPROFILE%/.vscode'); expect($result)->toBe("{$profile}/.vscode"); -}); +})->skip(PHP_OS_FAMILY !== 'Windows', 'USERPROFILE expansion is Windows-only'); it('globExpand returns an empty array for non-existent patterns', function (): void { $result = PlatformPaths::globExpand('/nonexistent_path_xyz123/*/data'); diff --git a/movez/tests/Unit/Support/SafetyHelpersTest.php b/movez/tests/Unit/Support/SafetyHelpersTest.php new file mode 100644 index 0000000..35b9b55 --- /dev/null +++ b/movez/tests/Unit/Support/SafetyHelpersTest.php @@ -0,0 +1,51 @@ +toBe($id); +})->with(['abc-123', '7f3e1c2a-0000-4000-8000-000000000001', 'rollout_1.v2']); + +it('SafePath::id rejects traversal and separators', function (string $id): void { + expect(fn() => SafePath::id($id))->toThrow(InvalidArgumentException::class); +})->with(['', '.', '..', '../x', 'a/b', 'a\\b', 'C:evil', "x\0y"]); + +it('SafePath::segment reduces a project name to its last segment', function (): void { + expect(SafePath::segment('My Project'))->toBe('My Project') + ->and(SafePath::segment('../../etc'))->toBe('etc') + ->and(fn() => SafePath::segment('..'))->toThrow(InvalidArgumentException::class); +}); + +it('ContentFlattener handles strings, block arrays and junk', function (): void { + expect(ContentFlattener::flatten('plain'))->toBe('plain') + ->and(ContentFlattener::flatten([ + ['type' => 'text', 'text' => 'one'], + ['type' => 'thinking', 'thinking' => 'hidden'], + ['type' => 'tool_use', 'input' => []], + 'two', + ]))->toBe("one\ntwo") + ->and(ContentFlattener::flatten(null))->toBe('') + ->and(ContentFlattener::flatten(42))->toBe('42'); +}); + +it('ProjectFilter keeps only sessions for the given project', function (): void { + $make = fn(string $id, string $project) => new SessionDTO( + id: $id, title: $id, sourceTool: 'cursor', sourceMachineSha: 'x', + createdAt: Carbon::now(), lastActiveAt: Carbon::now(), turns: collect(), project: $project, + ); + + $sessions = collect([ + $make('a', 'MoveZ'), + $make('b', 'Personal-Flutter-MoveZ'), // Cursor's decoded multi-segment name + $make('c', 'OtherApp'), + $make('d', ''), + ]); + + expect(ProjectFilter::apply($sessions, 'C:\\Work\\MoveZ')->pluck('id')->all())->toBe(['a', 'b']) + ->and(ProjectFilter::apply($sessions, null))->toHaveCount(4); +}); diff --git a/movez/tests/Unit/Writers/ClaudeCodeWriterTest.php b/movez/tests/Unit/Writers/ClaudeCodeWriterTest.php new file mode 100644 index 0000000..b9d3378 --- /dev/null +++ b/movez/tests/Unit/Writers/ClaudeCodeWriterTest.php @@ -0,0 +1,123 @@ +fakePath; + } + + protected function getDesktopRegistryDir(): ?string + { + return $this->fakeRegistry; + } + }; +} + +function claudeSession(string $id = 'sess-xyz', string $project = ''): SessionDTO +{ + return new SessionDTO( + id: $id, + title: 'ClaudeCode Test', + sourceTool: 'cursor', + sourceMachineSha: 'abc', + createdAt: Carbon::parse('2026-01-15T10:00:00Z'), + lastActiveAt: Carbon::parse('2026-01-15T10:05:00Z'), + turns: collect([ + new TurnDTO('user', 'Q', Carbon::parse('2026-01-15T10:00:00Z')), + new TurnDTO('assistant', 'A', Carbon::parse('2026-01-15T10:01:00Z')), + ]), + project: $project, + ); +} + +it('write creates one Claude Code JSONL transcript per session', function (): void { + withTempDir(function (string $dir): void { + claudeWriterFor($dir)->write(collect([claudeSession()]), '/home/dev/proj'); + + // POSIX paths are encoded by replacing separators with dashes + $file = $dir . '/-home-dev-proj/sess-xyz.jsonl'; + expect(file_exists($file))->toBeTrue(); + + $entries = array_map(fn($l) => json_decode($l, true), file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); + $messages = array_values(array_filter($entries, fn($e) => in_array($e['type'], ['user', 'assistant'], true))); + + expect($messages)->toHaveCount(2) + ->and($messages[0]['message']['content'][0]['text'])->toBe('Q') + ->and($messages[0]['cwd'])->toBe('/home/dev/proj') + ->and($messages[0]['entrypoint'])->toBe('movez-import:cursor') + ->and($messages[1]['entrypoint'])->toBe('movez-import:cursor'); + }); +}); + +it('normalises Windows paths to the desktop app format', function (): void { + withTempDir(function (string $dir): void { + claudeWriterFor($dir)->write(collect([claudeSession()]), 'c:/Work/Proj'); + + $file = $dir . '/C--Work-Proj/sess-xyz.jsonl'; + expect(file_exists($file))->toBeTrue(); + + $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $user = json_decode($lines[3], true); + expect($user['cwd'])->toBe('C:\\Work\\Proj'); + }); +}); + +it('output can be parsed back by the Claude Code parser', function (): void { + withTempDir(function (string $dir): void { + claudeWriterFor($dir)->write(collect([claudeSession()]), '/home/dev/proj'); + + $parser = new class($dir) extends ClaudeCodeParser { + public function __construct(private string $fakePath) {} + + public function getStoragePath(string $projectPath): string + { + return $this->fakePath; + } + }; + + $parsed = $parser->parse('/any')->first(); + + expect($parsed->id)->toBe('sess-xyz') + ->and($parsed->project)->toBe('proj') + ->and($parsed->turns)->toHaveCount(2); + }); +}); + +it('does not duplicate desktop registry entries on re-import', function (): void { + withTempDir(function (string $dir): void { + $registry = $dir . '/registry'; + mkdir($registry); + $writer = claudeWriterFor($dir . '/projects', $registry); + + $writer->write(collect([claudeSession()]), '/home/dev/proj'); + $writer->write(collect([claudeSession()]), '/home/dev/proj'); + + $entries = glob($registry . '/local_*.json'); + expect($entries)->toHaveCount(1); + + $meta = json_decode((string) file_get_contents($entries[0]), true); + expect($meta['cliSessionId'])->toBe('sess-xyz'); + }); +}); + +it('rejects session ids that would escape the storage directory', function (): void { + withTempDir(function (string $dir): void { + expect(fn() => claudeWriterFor($dir)->write(collect([claudeSession('../../evil')]), '/home/dev/proj')) + ->toThrow(InvalidArgumentException::class); + }); +}); + +it('toolName returns claude-code', function (): void { + expect((new ClaudeCodeWriter())->toolName())->toBe('claude-code'); +}); diff --git a/contextbridge/tests/Unit/Writers/CursorWriterTest.php b/movez/tests/Unit/Writers/CursorWriterTest.php similarity index 95% rename from contextbridge/tests/Unit/Writers/CursorWriterTest.php rename to movez/tests/Unit/Writers/CursorWriterTest.php index 91b3db7..f0f74ba 100644 --- a/contextbridge/tests/Unit/Writers/CursorWriterTest.php +++ b/movez/tests/Unit/Writers/CursorWriterTest.php @@ -33,8 +33,10 @@ protected function getStoragePath(): string return $this->fakePath; } - // Stub out global DB write — no real SQLite needed in unit tests + // Never touch the real Cursor databases in tests protected function writeToGlobalDb(\App\DTOs\SessionDTO $session): void {} + + protected function writeToWorkspaceDbs(array $sessionsByProject): void {} }; $writer->write(collect([makeWriterSession($sessionId)]), '/any'); From e52e4487ad806cf6a4b6e1b61b99aadc3292427c Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:33:02 -0400 Subject: [PATCH 2/8] Fix CI, release and local build pipeline - release.yml: build the PHAR from movez/ (the old `php movez` entry point did not exist), publish it as movez.phar, add a Windows desktop installer job that bundles PHP + the PHAR, and publish all assets in one release - ci.yml: point at movez/, MOVEZ_* env vars, PHP 8.2/8.3 matrix, real extension type check (no continue-on-error), VSIX packaging check, web type check and a desktop app type check - build.bat: fail clearly when box.phar or movez.phar is missing - Electron: stable installer name, Windows-only target, vue-tsc type check - Stop tracking *.tsbuildinfo; ignore extension and web build output Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 66 ++++++---- .github/workflows/release.yml | 123 ++++++++++++++++--- .gitignore | 13 +- build.bat | 10 ++ electron-app/package-lock.json | 163 ++++++++++++++++++++++++- electron-app/package.json | 16 ++- electron-app/tsconfig.node.tsbuildinfo | 1 - electron-app/tsconfig.web.tsbuildinfo | 1 - 8 files changed, 335 insertions(+), 58 deletions(-) delete mode 100644 electron-app/tsconfig.node.tsbuildinfo delete mode 100644 electron-app/tsconfig.web.tsbuildinfo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17ba379..8a3c788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,35 +8,30 @@ on: jobs: cli-tests: - name: CLI Tests (PHP 8.2) + name: CLI Tests (PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + matrix: + php: ["8.2", "8.3"] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: "8.2" + php-version: ${{ matrix.php }} extensions: pdo_sqlite, openssl, zip, mbstring, json - coverage: xdebug + coverage: none - name: Install CLI dependencies - working-directory: contextbridge + working-directory: movez run: composer install --no-interaction --prefer-dist --optimize-autoloader - name: Run CLI tests - working-directory: contextbridge + working-directory: movez run: php vendor/bin/pest env: - CB_KEY_PATH: /tmp/cb_test_key - CB_TOKEN_PATH: /tmp/cb_test_token - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - files: movez/coverage.xml - flags: cli - continue-on-error: true - if: false + MOVEZ_KEY_PATH: /tmp/movez_test_key + MOVEZ_TOKEN_PATH: /tmp/movez_test_token web-tests: name: Web Tests (PHP 8.2 + Node 20) @@ -48,7 +43,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: "8.2" - extensions: pdo_sqlite, mbstring, openssl, zip, bcmath + extensions: pdo_sqlite, mbstring, openssl, zip, bcmath, pcntl, posix - uses: actions/setup-node@v4 with: @@ -64,6 +59,10 @@ jobs: working-directory: web run: npm ci + - name: Type check frontend + working-directory: web + run: npm run type-check + - name: Build frontend assets working-directory: web run: npm run build @@ -99,11 +98,36 @@ jobs: working-directory: vscode-extension run: npm ci - - name: TypeScript type check + - name: Type check and compile working-directory: vscode-extension - run: npx tsc --noEmit + run: npm test - - name: Run extension tests + - name: Package VSIX (non-interactive) working-directory: vscode-extension - run: npm test - continue-on-error: true + run: npx vsce package --no-dependencies --out movez-vscode.vsix + + desktop-typecheck: + name: Desktop App Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: electron-app/package-lock.json + + - name: Install desktop dependencies + working-directory: electron-app + run: npm ci + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: "1" + + - name: Type check + working-directory: electron-app + run: npm run typecheck + + - name: Build + working-directory: electron-app + run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 454e719..8317e24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,29 +19,26 @@ jobs: with: php-version: "8.2" extensions: pdo_sqlite, openssl, zip, mbstring + ini-values: phar.readonly=0 - name: Install CLI dependencies (no dev) - working-directory: contextbridge + working-directory: movez run: composer install --no-dev --optimize-autoloader --no-interaction - name: Build PHAR - working-directory: contextbridge - run: php movez app:build movez --build-version=${{ github.ref_name }} --no-interaction + working-directory: movez + run: php movez app:build movez.phar --build-version=${{ github.ref_name }} --no-interaction - - name: Upload PHAR to release - uses: softprops/action-gh-release@v2 - with: - files: contextbridge/builds/movez - name: MoveZ ${{ github.ref_name }} - body: | - ## MoveZ ${{ github.ref_name }} + - name: Smoke test PHAR + working-directory: movez + run: | + php builds/movez.phar --version + php builds/movez.phar list --raw | grep -q '^export' - ### Installation - ```bash - chmod +x movez - mv movez /usr/local/bin/movez - movez doctor - ``` + - uses: actions/upload-artifact@v4 + with: + name: movez-phar + path: movez/builds/movez.phar build-vsix: name: Build VS Code Extension @@ -52,6 +49,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "20" + cache: npm + cache-dependency-path: vscode-extension/package-lock.json - name: Install extension dependencies working-directory: vscode-extension @@ -59,9 +58,93 @@ jobs: - name: Package extension working-directory: vscode-extension - run: npx vsce package --no-dependencies + run: npx vsce package --no-dependencies --out movez-vscode.vsix + + - uses: actions/upload-artifact@v4 + with: + name: movez-vsix + path: vscode-extension/movez-vscode.vsix + + build-desktop: + name: Build Windows Desktop Installer + needs: build-phar + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: electron-app/package-lock.json + + - uses: actions/download-artifact@v4 + with: + name: movez-phar + path: electron-app/resources - - name: Upload VSIX to release - uses: softprops/action-gh-release@v2 + - name: Bundle PHP runtime + shell: pwsh + working-directory: electron-app/resources + run: | + Invoke-WebRequest -Uri "https://windows.php.net/downloads/releases/latest/php-8.3-nts-Win32-vs16-x64-latest.zip" -OutFile php.zip + Expand-Archive php.zip -DestinationPath php + Remove-Item php.zip + @( + 'extension_dir = "ext"', + 'extension=openssl', + 'extension=pdo_sqlite', + 'extension=zip', + 'extension=mbstring', + 'extension=fileinfo' + ) | Set-Content -Encoding ascii php/php.ini + php/php.exe -v + php/php.exe movez.phar --version + + - name: Install desktop dependencies + working-directory: electron-app + run: npm ci + + - name: Set version from tag + working-directory: electron-app + shell: pwsh + run: npm version "$("${{ github.ref_name }}".TrimStart('v'))" --no-git-tag-version --allow-same-version + + - name: Build installer + working-directory: electron-app + run: npm run package -- --win --publish never + + - uses: actions/upload-artifact@v4 with: - files: vscode-extension/*.vsix + name: movez-desktop + path: electron-app/dist/*.exe + + publish: + name: Publish GitHub Release + needs: [build-phar, build-vsix, build-desktop] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - uses: softprops/action-gh-release@v2 + with: + name: MoveZ ${{ github.ref_name }} + files: artifacts/* + body: | + ## MoveZ ${{ github.ref_name }} + + | File | What it is | + |---|---| + | `MoveZ-Setup-*.exe` | Windows desktop app (bundles PHP and the CLI) | + | `movez.phar` | Cross-platform CLI (needs PHP 8.2+ with pdo_sqlite, openssl, zip, mbstring) | + | `movez-vscode.vsix` | VS Code / Cursor extension — install with "Extensions: Install from VSIX…" | + + ### CLI quick start + ```bash + curl -L https://github.com/kv4u/MoveZ/releases/latest/download/movez.phar -o movez + chmod +x movez && sudo mv movez /usr/local/bin/movez + movez doctor + ``` diff --git a/.gitignore b/.gitignore index 5539c9e..a1d55b3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,13 @@ vendor/ # Build artifacts electron-app/dist/ electron-app/out/ -contextbridge/contextbridge.phar -contextbridge/box.phar -contextbridge/builds/ +movez/movez.phar +movez/box.phar +movez/builds/ vscode-extension/*.vsix +vscode-extension/out/ +web/public/build/ +*.tsbuildinfo # Bundled runtime binaries (downloaded during setup, not source) electron-app/resources/php/ @@ -45,5 +48,5 @@ npm-debug.log* .php-cs-fixer.cache # Personal utility scripts -contextbridge/bundle_export.php -contextbridge/make_cbz.php +movez/bundle_export.php +movez/make_cbz.php diff --git a/build.bat b/build.bat index c4d467d..72edc04 100644 --- a/build.bat +++ b/build.bat @@ -18,6 +18,11 @@ set ELECTRON_APP=%~dp0electron-app :: ----------------------------------------------- echo [1/3] Building movez.phar ... cd /d "%PHAR_SRC%" +if not exist "%PHAR_SRC%\box.phar" ( + echo [ERROR] movez\box.phar not found. Download it from https://github.com/box-project/box/releases + pause + exit /b 1 +) "%BUNDLED_PHP%" -d phar.readonly=0 box.phar compile --composer-bin "%COMPOSER_BAT%" if errorlevel 1 ( echo [ERROR] PHAR build failed. @@ -29,6 +34,11 @@ if errorlevel 1 ( :: Step 2 - Copy PHAR to Electron resources :: ----------------------------------------------- echo [2/3] Copying PHAR to electron-app\resources\ ... +if not exist "%PHAR_SRC%\movez.phar" ( + echo [ERROR] movez\movez.phar was not produced by Box. + pause + exit /b 1 +) copy /y "%PHAR_SRC%\movez.phar" "%PHAR_DEST%" if errorlevel 1 ( echo [ERROR] Could not copy PHAR. diff --git a/electron-app/package-lock.json b/electron-app/package-lock.json index 5f22cea..5b85b9d 100644 --- a/electron-app/package-lock.json +++ b/electron-app/package-lock.json @@ -25,7 +25,8 @@ "electron-vite": "^3.0.0", "tailwindcss": "^3.4.17", "typescript": "^5.7.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vue-tsc": "^2.2.0" } }, "node_modules/@alloc/quick-lru": { @@ -1994,6 +1995,35 @@ "vue": "^3.2.25" } }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", @@ -2044,12 +2074,81 @@ "@vue/shared": "3.5.30" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", @@ -2227,6 +2326,13 @@ "ajv": "^6.9.1" } }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3542,6 +3648,13 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/debounce-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", @@ -5011,6 +5124,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -6024,6 +6147,13 @@ "dev": true, "license": "MIT" }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -6388,6 +6518,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -8076,6 +8213,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", @@ -8138,6 +8282,23 @@ "vue": "^3.5.0" } }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/electron-app/package.json b/electron-app/package.json index a55bdba..1de8bd1 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -10,7 +10,7 @@ "build": "electron-vite build", "preview": "electron-vite preview", "package": "electron-vite build && electron-builder", - "typecheck": "tsc --noEmit" + "typecheck": "vue-tsc --noEmit -p tsconfig.web.json && tsc --noEmit -p tsconfig.node.json" }, "dependencies": { "electron-store": "^8.2.0", @@ -29,7 +29,8 @@ "electron-vite": "^3.0.0", "tailwindcss": "^3.4.17", "typescript": "^5.7.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vue-tsc": "^2.2.0" }, "build": { "appId": "com.movez.desktop", @@ -53,17 +54,14 @@ "to": "movez.phar" } ], - "mac": { - "target": "dmg", - "icon": "resources/icon.icns" - }, "win": { "target": "nsis", "icon": "resources/icon.ico" }, - "linux": { - "target": "AppImage", - "icon": "resources/icon.png" + "nsis": { + "artifactName": "MoveZ-Setup-${version}.exe", + "oneClick": false, + "allowToChangeInstallationDirectory": true } } } diff --git a/electron-app/tsconfig.node.tsbuildinfo b/electron-app/tsconfig.node.tsbuildinfo deleted file mode 100644 index e62a1da..0000000 --- a/electron-app/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/modulerunnertransport.d-dj_me5sf.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/vite/types/internal/lightningcssoptions.d.ts","./node_modules/vite/types/internal/csspreprocessoroptions.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/electron-vite/dist/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@vue/shared/dist/shared.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@vue/compiler-core/dist/compiler-core.d.ts","./node_modules/magic-string/dist/magic-string.cjs.d.ts","./node_modules/typescript/lib/typescript.d.ts","./node_modules/@vue/compiler-sfc/dist/compiler-sfc.d.ts","./node_modules/vue/compiler-sfc/index.d.ts","./node_modules/@vitejs/plugin-vue/dist/index.d.ts","./electron.vite.config.ts","./node_modules/type-fest/source/primitive.d.ts","./node_modules/type-fest/source/typed-array.d.ts","./node_modules/type-fest/source/basic.d.ts","./node_modules/type-fest/source/observable-like.d.ts","./node_modules/type-fest/source/union-to-intersection.d.ts","./node_modules/type-fest/source/keys-of-union.d.ts","./node_modules/type-fest/source/distributed-omit.d.ts","./node_modules/type-fest/source/distributed-pick.d.ts","./node_modules/type-fest/source/empty-object.d.ts","./node_modules/type-fest/source/if-empty-object.d.ts","./node_modules/type-fest/source/optional-keys-of.d.ts","./node_modules/type-fest/source/required-keys-of.d.ts","./node_modules/type-fest/source/has-required-keys.d.ts","./node_modules/type-fest/source/is-never.d.ts","./node_modules/type-fest/source/if-never.d.ts","./node_modules/type-fest/source/unknown-array.d.ts","./node_modules/type-fest/source/internal/array.d.ts","./node_modules/type-fest/source/internal/characters.d.ts","./node_modules/type-fest/source/is-any.d.ts","./node_modules/type-fest/source/is-float.d.ts","./node_modules/type-fest/source/is-integer.d.ts","./node_modules/type-fest/source/numeric.d.ts","./node_modules/type-fest/source/is-literal.d.ts","./node_modules/type-fest/source/trim.d.ts","./node_modules/type-fest/source/is-equal.d.ts","./node_modules/type-fest/source/and.d.ts","./node_modules/type-fest/source/or.d.ts","./node_modules/type-fest/source/greater-than.d.ts","./node_modules/type-fest/source/greater-than-or-equal.d.ts","./node_modules/type-fest/source/less-than.d.ts","./node_modules/type-fest/source/internal/tuple.d.ts","./node_modules/type-fest/source/internal/string.d.ts","./node_modules/type-fest/source/internal/keys.d.ts","./node_modules/type-fest/source/internal/numeric.d.ts","./node_modules/type-fest/source/simplify.d.ts","./node_modules/type-fest/source/omit-index-signature.d.ts","./node_modules/type-fest/source/pick-index-signature.d.ts","./node_modules/type-fest/source/merge.d.ts","./node_modules/type-fest/source/if-any.d.ts","./node_modules/type-fest/source/internal/type.d.ts","./node_modules/type-fest/source/internal/object.d.ts","./node_modules/type-fest/source/internal/index.d.ts","./node_modules/type-fest/source/except.d.ts","./node_modules/type-fest/source/require-at-least-one.d.ts","./node_modules/type-fest/source/non-empty-object.d.ts","./node_modules/type-fest/source/non-empty-string.d.ts","./node_modules/type-fest/source/unknown-record.d.ts","./node_modules/type-fest/source/unknown-set.d.ts","./node_modules/type-fest/source/unknown-map.d.ts","./node_modules/type-fest/source/tagged-union.d.ts","./node_modules/type-fest/source/writable.d.ts","./node_modules/type-fest/source/writable-deep.d.ts","./node_modules/type-fest/source/conditional-simplify.d.ts","./node_modules/type-fest/source/non-empty-tuple.d.ts","./node_modules/type-fest/source/array-tail.d.ts","./node_modules/type-fest/source/enforce-optional.d.ts","./node_modules/type-fest/source/simplify-deep.d.ts","./node_modules/type-fest/source/merge-deep.d.ts","./node_modules/type-fest/source/merge-exclusive.d.ts","./node_modules/type-fest/source/require-exactly-one.d.ts","./node_modules/type-fest/source/require-all-or-none.d.ts","./node_modules/type-fest/source/require-one-or-none.d.ts","./node_modules/type-fest/source/single-key-object.d.ts","./node_modules/type-fest/source/partial-deep.d.ts","./node_modules/type-fest/source/required-deep.d.ts","./node_modules/type-fest/source/subtract.d.ts","./node_modules/type-fest/source/paths.d.ts","./node_modules/type-fest/source/pick-deep.d.ts","./node_modules/type-fest/source/array-splice.d.ts","./node_modules/type-fest/source/literal-union.d.ts","./node_modules/type-fest/source/union-to-tuple.d.ts","./node_modules/type-fest/source/omit-deep.d.ts","./node_modules/type-fest/source/is-null.d.ts","./node_modules/type-fest/source/is-unknown.d.ts","./node_modules/type-fest/source/if-unknown.d.ts","./node_modules/type-fest/source/partial-on-undefined-deep.d.ts","./node_modules/type-fest/source/undefined-on-partial-deep.d.ts","./node_modules/type-fest/source/readonly-deep.d.ts","./node_modules/type-fest/source/promisable.d.ts","./node_modules/type-fest/source/arrayable.d.ts","./node_modules/type-fest/source/tagged.d.ts","./node_modules/type-fest/source/invariant-of.d.ts","./node_modules/type-fest/source/set-optional.d.ts","./node_modules/type-fest/source/set-readonly.d.ts","./node_modules/type-fest/source/set-required.d.ts","./node_modules/type-fest/source/set-required-deep.d.ts","./node_modules/type-fest/source/set-non-nullable.d.ts","./node_modules/type-fest/source/set-non-nullable-deep.d.ts","./node_modules/type-fest/source/value-of.d.ts","./node_modules/type-fest/source/async-return-type.d.ts","./node_modules/type-fest/source/conditional-keys.d.ts","./node_modules/type-fest/source/conditional-except.d.ts","./node_modules/type-fest/source/conditional-pick.d.ts","./node_modules/type-fest/source/conditional-pick-deep.d.ts","./node_modules/type-fest/source/stringified.d.ts","./node_modules/type-fest/source/join.d.ts","./node_modules/type-fest/source/sum.d.ts","./node_modules/type-fest/source/less-than-or-equal.d.ts","./node_modules/type-fest/source/array-slice.d.ts","./node_modules/type-fest/source/string-slice.d.ts","./node_modules/type-fest/source/fixed-length-array.d.ts","./node_modules/type-fest/source/multidimensional-array.d.ts","./node_modules/type-fest/source/multidimensional-readonly-array.d.ts","./node_modules/type-fest/source/iterable-element.d.ts","./node_modules/type-fest/source/entry.d.ts","./node_modules/type-fest/source/entries.d.ts","./node_modules/type-fest/source/set-return-type.d.ts","./node_modules/type-fest/source/set-parameter-type.d.ts","./node_modules/type-fest/source/asyncify.d.ts","./node_modules/type-fest/source/jsonify.d.ts","./node_modules/type-fest/source/jsonifiable.d.ts","./node_modules/type-fest/source/find-global-type.d.ts","./node_modules/type-fest/source/structured-cloneable.d.ts","./node_modules/type-fest/source/schema.d.ts","./node_modules/type-fest/source/literal-to-primitive.d.ts","./node_modules/type-fest/source/literal-to-primitive-deep.d.ts","./node_modules/type-fest/source/string-key-of.d.ts","./node_modules/type-fest/source/exact.d.ts","./node_modules/type-fest/source/readonly-tuple.d.ts","./node_modules/type-fest/source/override-properties.d.ts","./node_modules/type-fest/source/has-optional-keys.d.ts","./node_modules/type-fest/source/writable-keys-of.d.ts","./node_modules/type-fest/source/readonly-keys-of.d.ts","./node_modules/type-fest/source/has-readonly-keys.d.ts","./node_modules/type-fest/source/has-writable-keys.d.ts","./node_modules/type-fest/source/spread.d.ts","./node_modules/type-fest/source/is-tuple.d.ts","./node_modules/type-fest/source/tuple-to-object.d.ts","./node_modules/type-fest/source/tuple-to-union.d.ts","./node_modules/type-fest/source/int-range.d.ts","./node_modules/type-fest/source/int-closed-range.d.ts","./node_modules/type-fest/source/array-indices.d.ts","./node_modules/type-fest/source/array-values.d.ts","./node_modules/type-fest/source/set-field-type.d.ts","./node_modules/type-fest/source/shared-union-fields.d.ts","./node_modules/type-fest/source/all-union-fields.d.ts","./node_modules/type-fest/source/shared-union-fields-deep.d.ts","./node_modules/type-fest/source/if-null.d.ts","./node_modules/type-fest/source/words.d.ts","./node_modules/type-fest/source/camel-case.d.ts","./node_modules/type-fest/source/camel-cased-properties.d.ts","./node_modules/type-fest/source/camel-cased-properties-deep.d.ts","./node_modules/type-fest/source/delimiter-case.d.ts","./node_modules/type-fest/source/kebab-case.d.ts","./node_modules/type-fest/source/delimiter-cased-properties.d.ts","./node_modules/type-fest/source/kebab-cased-properties.d.ts","./node_modules/type-fest/source/delimiter-cased-properties-deep.d.ts","./node_modules/type-fest/source/kebab-cased-properties-deep.d.ts","./node_modules/type-fest/source/pascal-case.d.ts","./node_modules/type-fest/source/pascal-cased-properties.d.ts","./node_modules/type-fest/source/pascal-cased-properties-deep.d.ts","./node_modules/type-fest/source/snake-case.d.ts","./node_modules/type-fest/source/snake-cased-properties.d.ts","./node_modules/type-fest/source/snake-cased-properties-deep.d.ts","./node_modules/type-fest/source/screaming-snake-case.d.ts","./node_modules/type-fest/source/split.d.ts","./node_modules/type-fest/source/replace.d.ts","./node_modules/type-fest/source/string-repeat.d.ts","./node_modules/type-fest/source/includes.d.ts","./node_modules/type-fest/source/get.d.ts","./node_modules/type-fest/source/last-array-element.d.ts","./node_modules/type-fest/source/global-this.d.ts","./node_modules/type-fest/source/package-json.d.ts","./node_modules/type-fest/source/tsconfig-json.d.ts","./node_modules/type-fest/index.d.ts","./node_modules/electron-store/index.d.ts","./electron/cli.ts","./node_modules/electron/electron.d.ts","./node_modules/@electron-toolkit/utils/dist/index.d.ts","./electron/ipc/sessions.ts","./electron/ipc/migrate.ts","./electron/ipc/sync.ts","./electron/ipc/doctor.ts","./electron/ipc/settings.ts","./electron/ipc/dialog.ts","./electron/main.ts","./electron/preload.ts","./node_modules/electron-vite/node.d.ts"],"fileIdsList":[[55,104,121,122,126,187,196],[55,104,105,121,122,363],[55,104,121,122,365],[55,104,121,122,364,365],[55,104,121,122,363,365],[55,104,121,122,126,365,366,367,368,369,370,371,372],[55,104,121,122,188],[55,104,121,122],[55,101,102,104,121,122],[55,103,104,121,122],[104,121,122],[55,104,109,121,122,139],[55,104,105,110,115,121,122,124,136,147],[55,104,105,106,115,121,122,124],[50,51,52,55,104,121,122],[55,104,107,121,122,148],[55,104,108,109,116,121,122,125],[55,104,109,121,122,136,144],[55,104,110,112,115,121,122,124],[55,103,104,111,121,122],[55,104,112,113,121,122],[55,104,114,115,121,122],[55,103,104,115,121,122],[55,104,115,116,117,121,122,136,147],[55,104,115,116,117,121,122,131,136,139],[55,97,104,112,115,118,121,122,124,136,147],[55,104,115,116,118,119,121,122,124,136,144,147],[55,104,118,120,121,122,136,144,147],[53,54,55,56,57,58,59,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153],[55,104,115,121,122],[55,104,121,122,123,147],[55,104,112,115,121,122,124,136],[55,104,121,122,125],[55,104,121,122,126],[55,103,104,121,122,127],[55,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153],[55,104,121,122,129],[55,104,121,122,130],[55,104,115,121,122,131,132],[55,104,121,122,131,133,148,150],[55,104,116,121,122],[55,104,115,121,122,136,137,139],[55,104,121,122,138,139],[55,104,121,122,136,137],[55,104,121,122,139],[55,104,121,122,140],[55,101,104,121,122,136,141,147],[55,104,115,121,122,142,143],[55,104,121,122,142,143],[55,104,109,121,122,124,136,144],[55,104,121,122,145],[55,104,121,122,124,146],[55,104,118,121,122,130,147],[55,104,109,121,122,148],[55,104,121,122,136,149],[55,104,121,122,123,150],[55,104,121,122,151],[55,97,104,121,122],[55,97,104,115,117,121,122,127,136,139,147,149,150,152],[55,104,121,122,136,153],[55,104,121,122,186,195],[55,104,121,122,188,189,190],[55,104,121,122,181,188,190,191,192,193],[55,104,121,122,362],[55,104,121,122,186],[55,104,121,122,152],[55,104,115,116,121,122,154],[55,104,121,122,178],[55,104,121,122,176,178],[55,104,121,122,167,175,176,177,179,181],[55,104,121,122,165],[55,104,121,122,168,173,178,181],[55,104,121,122,164,181],[55,104,121,122,168,169,172,173,174,181],[55,104,121,122,168,169,170,172,173,181],[55,104,121,122,165,166,167,168,169,173,174,175,177,178,179,181],[55,104,121,122,163,165,166,167,168,169,170,172,173,174,175,176,177,178,179,180],[55,104,121,122,163,181],[55,104,121,122,168,170,171,173,174,181],[55,104,121,122,172,181],[55,104,121,122,173,174,178,181],[55,104,121,122,166,176],[55,104,121,122,155,156],[55,104,121,122,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,216,217,218,219,220,221,222,223,224,225,226,227,232,233,234,235,236,240,241,242,243,244,245,246,247,248,249,251,252,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361],[55,104,121,122,203,213,232,239,332],[55,104,121,122,222],[55,104,121,122,219,222,223,225,226,239,266,294,295],[55,104,121,122,213,226,239,263],[55,104,121,122,213,239],[55,104,121,122,304],[55,104,121,122,239,336],[55,104,121,122,213,239,337],[55,104,121,122,239,337],[55,104,121,122,240,288],[55,104,121,122,212],[55,104,121,122,206,222,239,244,250,289],[55,104,121,122,288],[55,104,121,122,220,235,239,336],[55,104,121,122,213,239,336,340],[55,104,121,122,239,336,340],[55,104,121,122,203],[55,104,121,122,232],[55,104,121,122,302],[55,104,121,122,198,203,222,239,271],[55,104,121,122,222,239],[55,104,121,122,239,264,267,314,353],[55,104,121,122,225],[55,104,121,122,219,222,223,224,239],[55,104,121,122,208],[55,104,121,122,320],[55,104,121,122,209],[55,104,121,122,319],[55,104,121,122,216],[55,104,121,122,206],[55,104,121,122,211],[55,104,121,122,270],[55,104,121,122,271],[55,104,121,122,294,327],[55,104,121,122,239,263],[55,104,121,122,212,213],[55,104,121,122,214,215,228,229,230,231,237,238],[55,104,121,122,216,220,229],[55,104,121,122,211,213,219,229],[55,104,121,122,203,208,209,212,213,222,229,230,232,235,236,237],[55,104,121,122,215,219,221,228],[55,104,121,122,213,219,225,227],[55,104,121,122,198,211,216],[55,104,121,122,217,219,239],[55,104,121,122,198,211,212,219,239],[55,104,121,122,212,213,236,239],[55,104,121,122,200],[55,104,121,122,199,200,206,211,213,216,219,239,271],[55,104,121,122,239,336,340,344],[55,104,121,122,239,336,340,342],[55,104,121,122,202],[55,104,121,122,226],[55,104,121,122,233,312],[55,104,121,122,198],[55,104,121,122,213,233,234,235,239,244,250,251,252,253,254],[55,104,121,122,232,233,234],[55,104,121,122,222,263],[55,104,121,122,210,241],[55,104,121,122,217,218],[55,104,121,122,211,213,222,239,254,264,266,267,268],[55,104,121,122,235],[55,104,121,122,200,267],[55,104,121,122,211,239],[55,104,121,122,235,239,272],[55,104,121,122,239,337,346],[55,104,121,122,206,213,216,225,239,263],[55,104,121,122,202,211,213,232,239,264],[55,104,121,122,239],[55,104,121,122,212,236,239],[55,104,121,122,212,236,239,240],[55,104,121,122,212,236,239,257],[55,104,121,122,239,336,340,349],[55,104,121,122,232,239],[55,104,121,122,213,232,239,264,268,284],[55,104,121,122,232,239,240],[55,104,121,122,213,239,271],[55,104,121,122,213,216,239,254,262,264,268,282],[55,104,121,122,208,213,232,239,240],[55,104,121,122,211,213,239],[55,104,121,122,211,213,232,239],[55,104,121,122,239,250],[55,104,121,122,207,239],[55,104,121,122,220,223,224,239],[55,104,121,122,209,232],[55,104,121,122,219,220],[55,104,121,122,239,293,296],[55,104,121,122,199,309],[55,104,121,122,219,227,239],[55,104,121,122,219,239,263],[55,104,121,122,213,236,324],[55,104,121,122,202,211],[55,104,121,122,232,240],[55,69,73,104,121,122,147],[55,69,104,121,122,136,147],[55,64,104,121,122],[55,66,69,104,121,122,144,147],[55,104,121,122,124,144],[55,104,121,122,154],[55,64,104,121,122,154],[55,66,69,104,121,122,124,147],[55,61,62,65,68,104,115,121,122,136,147],[55,69,76,104,121,122],[55,61,67,104,121,122],[55,69,90,91,104,121,122],[55,65,69,104,121,122,139,147,154],[55,90,104,121,122,154],[55,63,64,104,121,122,154],[55,69,104,121,122],[55,63,64,65,66,67,68,69,70,71,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,91,92,93,94,95,96,104,121,122],[55,69,84,104,121,122],[55,69,76,77,104,121,122],[55,67,69,77,78,104,121,122],[55,68,104,121,122],[55,61,64,69,104,121,122],[55,69,73,77,78,104,121,122],[55,73,104,121,122],[55,67,69,72,104,121,122,147],[55,61,66,69,76,104,121,122],[55,104,121,122,136],[55,64,69,90,104,121,122,152,154],[55,104,115,116,118,119,120,121,122,124,136,144,147,153,154,156,157,158,160,161,162,181,182,183,184,185,186],[55,104,121,122,157,158,159,160],[55,104,121,122,157],[55,104,121,122,158],[55,104,121,122,156,186],[55,104,121,122,194]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"6b039f55681caaf111d5eb84d292b9bee9e0131d0db1ad0871eef0964f533c73","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"035d0934d304483f07148427a5bd5b98ac265dae914a6b49749fe23fbd893ec7","impliedFormat":99},{"version":"e2ed5b81cbed3a511b21a18ab2539e79ac1f4bc1d1d28f8d35d8104caa3b429f","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"7965dc3c7648e2a7a586d11781cabb43d4859920716bc2fdc523da912b06570d","impliedFormat":1},{"version":"90c2bd9a3e72fe08b8fa5982e78cb8dc855a1157b26e11e37a793283c52bf64b","impliedFormat":1},{"version":"a8122fe390a2a987079e06c573b1471296114677923c1c094c24a53ddd7344a2","impliedFormat":1},{"version":"70c2cb19c0c42061a39351156653aa0cf5ba1ecdc8a07424dd38e3a1f1e3c7f4","impliedFormat":1},{"version":"a8fb10fd8c7bc7d9b8f546d4d186d1027f8a9002a639bec689b5000dab68e35c","impliedFormat":1},{"version":"c9b467ea59b86bd27714a879b9ad43c16f186012a26d0f7110b1322025ceaa83","impliedFormat":1},{"version":"57ea19c2e6ba094d8087c721bac30ff1c681081dbd8b167ac068590ef633e7a5","impliedFormat":1},{"version":"cba81ec9ae7bc31a4dc56f33c054131e037649d6b9a2cfa245124c67e23e4721","impliedFormat":1},{"version":"ad193f61ba708e01218496f093c23626aa3808c296844a99189be7108a9c8343","impliedFormat":1},{"version":"a0544b3c8b70b2f319a99ea380b55ab5394ede9188cdee452a5d0ce264f258b2","impliedFormat":1},{"version":"8c654c17c334c7c168c1c36e5336896dc2c892de940886c1639bebd9fc7b9be4","impliedFormat":1},{"version":"6a4da742485d5c2eb6bcb322ae96993999ffecbd5660b0219a5f5678d8225bb0","impliedFormat":1},{"version":"c65ca21d7002bdb431f9ab3c7a6e765a489aa5196e7e0ef00aed55b1294df599","impliedFormat":1},{"version":"c8fc655c2c4bafc155ceee01c84ab3d6c03192ced5d3f2de82e20f3d1bd7f9fa","impliedFormat":1},{"version":"be5a7ff3b47f7e553565e9483bdcadb0ca2040ac9e5ec7b81c7e115a81059882","impliedFormat":1},{"version":"1a93f36ecdb60a95e3a3621b561763e2952da81962fae217ab5441ac1d77ffc5","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"7870becb94cbc11d2d01b77c4422589adcba4d8e59f726246d40cd0d129784d8","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"f70b8328a15ca1d10b1436b691e134a49bc30dcf3183a69bfaa7ba77e1b78ecd","impliedFormat":1},{"version":"683b035f752e318d02e303894e767a1ac16ac4493baa2b593195d7976e6b7310","impliedFormat":99},{"version":"e7ba74900e0edb9cb93edb1547cbb65238d26af0e5f10d24be3a61d862262cfd","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"f468b74459f1ad4473b36a36d49f2b255f3c6b5d536c81239c2b2971df089eaf","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"524a409ad72186b7f6cb16898c349465cfa876f641d6cb6137b3123d5cfca619","impliedFormat":1},{"version":"2be2227c3810dfd84e46674fd33b8d09a4a28ad9cb633ed536effd411665ea1e","impliedFormat":1},{"version":"e134052a6b1ded61693b4037f615dc72f14e2881e79c1ddbff6c514c8a516b05","impliedFormat":1},{"version":"957a44f864ab3c182edc747428e8eec1765257deee7fac86c1147eeac897d832","impliedFormat":1},{"version":"3feec212c0aeb91e5a6e62caaf9f128954590210f8c302910ea377c088f6b61a","impliedFormat":1},{"version":"f3e2483d5a6562f60624d9e3ebeeca2678a3bea1e1fac6c425b5b222d9eec0e0","impliedFormat":1},"9a51bb4033cb5d284382fc92a3440ec48f3e583b165bea8047bc89b135e3b0f2",{"version":"cd51ceafea7762ad639afb3ca5b68e1e4ffeaacaa402d7ef2cae17016e29e098","impliedFormat":1},{"version":"1b8357b3fef5be61b5de6d6a4805a534d68fe3e040c11f1944e27d4aec85936a","impliedFormat":1},{"version":"130ec22c8432ade59047e0225e552c62a47683d870d44785bee95594c8d65408","impliedFormat":1},{"version":"4f24c2781b21b6cd65eede543669327d68a8cf0c6d9cf106a1146b164a7c8ef9","affectsGlobalScope":true,"impliedFormat":1},{"version":"928f96b9948742cbaec33e1c34c406c127c2dad5906edb7df08e92b963500a41","impliedFormat":1},{"version":"56613f2ebdd34d4527ca1ee969ab7e82333c3183fc715e5667c999396359e478","impliedFormat":1},{"version":"d9720d542df1d7feba0aa80ed11b4584854951f9064232e8d7a76e65dc676136","impliedFormat":1},{"version":"d0fb3d0c64beba3b9ab25916cc018150d78ccb4952fac755c53721d9d624ba0d","impliedFormat":1},{"version":"86b484bcf6344a27a9ee19dd5cef1a5afbbd96aeb07708cc6d8b43d7dfa8466c","impliedFormat":1},{"version":"ba93f0192c9c30d895bee1141dd0c307b75df16245deef7134ac0152294788cc","impliedFormat":1},{"version":"fca7cd7512b19d38254171fb5e35d2b16ac56710b7915b7801994612953da16c","impliedFormat":1},{"version":"7e43693f6ea74c3866659265e0ce415b4da6ed7fabd2920ad7ea8a5e746c6a94","impliedFormat":1},{"version":"eb31477c87de3309cbe4e9984fa74a052f31581edb89103f8590f01874b4e271","impliedFormat":1},{"version":"4e251317bb109337e4918e5d7bcda7ef2d88f106cac531dcea03f7eee1dd2240","impliedFormat":1},{"version":"0f2c77683296ca2d0e0bee84f8aa944a05df23bc4c5b5fef31dda757e75f660f","impliedFormat":1},{"version":"1a67ba5891772a62706335b59a50720d89905196c90719dad7cec9c81c2990e6","impliedFormat":1},{"version":"cf41091fcbf45daff9aba653406b83d11a3ec163ff9d7a71890035117e733d98","impliedFormat":1},{"version":"aa514fadda13ad6ddadc2342e835307b962254d994f45a0cb495cc76eca13eff","impliedFormat":1},{"version":"ce92e662f86a36fc38c5aaa2ec6e6d6eed0bc6cf231bd06a9cb64cc652487550","impliedFormat":1},{"version":"3821c8180abb683dcf4ba833760764a79e25bc284dc9b17d32e138c34ada1939","impliedFormat":1},{"version":"0ef2a86ec84da6b2b06f830b441889c5bb8330a313691d4edbe85660afa97c44","impliedFormat":1},{"version":"b2a793bde18962a2e1e0f9fa5dce43dd3e801331d36d3e96a7451727185fb16f","impliedFormat":1},{"version":"9d8fc1d9b6b4b94127eec180183683a6ef4735b0e0a770ba9f7e2d98dd571e0c","impliedFormat":1},{"version":"8504003e88870caa5474ab8bd270f318d0985ba7ede4ee30fe37646768b5362a","impliedFormat":1},{"version":"892abbe1081799073183bab5dc771db813938e888cf49eb166f0e0102c0c1473","impliedFormat":1},{"version":"65465a64d5ee2f989ad4cf8db05f875204a9178f36b07a1e4d3a09a39f762e2e","impliedFormat":1},{"version":"2878f694f7d3a13a88a5e511da7ac084491ca0ddde9539e5dad76737ead9a5a9","impliedFormat":1},{"version":"d21c5f692d23afa03113393088bcb1ef90a69272a774950a9f69c58131ac5b7e","impliedFormat":1},{"version":"0915ce92bb54e905387b7907e98982620cb7143f7b44291974fb2e592602fe00","impliedFormat":1},{"version":"9dfb317a36a813f4356dc1488e26a36d95e3ac7f38a05fbf9dda97cfd13ef6ea","impliedFormat":1},{"version":"7c0a4d3819fb911cdb5a6759c0195c72b0c54094451949ebaa89ffceadd129ca","impliedFormat":1},{"version":"4733c832fb758f546a4246bc62f2e9d68880eb8abf0f08c6bec484decb774dc9","impliedFormat":1},{"version":"58d91c410f31f4dd6fa8d50ad10b4ae9a8d1789306e73a5fbe8abea6a593099b","impliedFormat":1},{"version":"3aea7345c25f1060791fc83a6466b889924db87389e5c344fa0c27b75257ebe4","impliedFormat":1},{"version":"a8289d1d525cf4a3a2d5a8db6b8e14e19f43d122cc47f8fb6b894b0aa2e2bde6","impliedFormat":1},{"version":"e6804515ba7c8f647e145ecc126138dd9d27d3e6283291d0f50050700066a0ea","impliedFormat":1},{"version":"9420a04edbe321959de3d1aab9fa88b45951a14c22d8a817f75eb4c0a80dba02","impliedFormat":1},{"version":"6927ceeb41bb451f47593de0180c8ff1be7403965d10dc9147ee8d5c91372fff","impliedFormat":1},{"version":"d9c6f10eebf03d123396d4fee1efbe88bc967a47655ec040ffe7e94271a34fc7","impliedFormat":1},{"version":"f2a392b336e55ccbeb8f8a07865c86857f1a5fc55587c1c7d79e4851b0c75c9a","impliedFormat":1},{"version":"fd53e2a54dae7bb3a9c3b061715fff55a0bb3878472d4a93b2da6f0f62262c9f","impliedFormat":1},{"version":"1f129869a0ee2dcb7ea9a92d6bc8ddf2c2cdaf2d244eec18c3a78efeb5e05c83","impliedFormat":1},{"version":"554962080d3195cae300341a8b472fb0553f354f658344ae181b9aa02d351dbd","impliedFormat":1},{"version":"89cd9ab3944b306e790b148dd0a13ca120daf7379a98729964ea6288a54a1beb","impliedFormat":1},{"version":"28fa41063a242eafcf51e1a62013fccdd9fd5d6760ded6e3ff5ce10a13c2ab31","impliedFormat":1},{"version":"e53a8b6e43f20fa792479f8069c41b1a788a15ffdfd56be1ab8ef46ea01bd43e","impliedFormat":1},{"version":"ada60ff3698e7fd0c7ed0e4d93286ee28aed87f648f6748e668a57308fde5a67","impliedFormat":1},{"version":"f65e0341f11f30b47686efab11e1877b1a42cf9b1a232a61077da2bdeee6d83e","impliedFormat":1},{"version":"e6918b864e3c2f3a7d323f1bb31580412f12ab323f6c3a55fb5dc532c827e26d","impliedFormat":1},{"version":"5d6f919e1966d45ea297c2478c1985d213e41e2f9a6789964cdb53669e3f7a6f","impliedFormat":1},{"version":"d7735a9ccd17767352ab6e799d76735016209aadd5c038a2fc07a29e7b235f02","impliedFormat":1},{"version":"843e98d09268e2b5b9e6ff60487cf68f4643a72c2e55f7c29b35d1091a4ee4e9","impliedFormat":1},{"version":"ef4c9ef3ec432ccbf6508f8aa12fbb8b7f4d535c8b484258a3888476de2c6c36","impliedFormat":1},{"version":"77ff2aeb024d9e1679c00705067159c1b98ccac8310987a0bdaf0e38a6ca7333","impliedFormat":1},{"version":"8f9effea32088f37d15858a890e1a7ccf9af8d352d47fea174f6b95448072956","impliedFormat":1},{"version":"952c4a8d2338e19ef26c1c0758815b1de6c082a485f88368f5bece1e555f39d4","impliedFormat":1},{"version":"1d953cb875c69aeb1ec8c58298a5226241c6139123b1ff885cedf48ac57b435c","impliedFormat":1},{"version":"1a80e164acd9ee4f3e2a919f9a92bfcdb3412d1fe680b15d60e85eadbaa460f8","impliedFormat":1},{"version":"f981ffdbd651f67db134479a5352dac96648ca195f981284e79dc0a1dbc53fd5","impliedFormat":1},{"version":"019c29de7d44d84684e65bdabb53ee8cc08f28b150ac0083d00e31a8fe2727d8","impliedFormat":1},{"version":"e35738485bf670f13eab658ea34d27ef2b875f3aae8fc00fb783d29e5737786d","impliedFormat":1},{"version":"bcd951d1a489d00e432c73760ce7f39adb0ef4e6a9c8ffef5dd7f093325a8377","impliedFormat":1},{"version":"672c1ebc4fa15a1c9b4911f1c68de2bc889f4d166a68c5be8f1e61f94014e9d8","impliedFormat":1},{"version":"b0378c1bc3995a1e7b40528dcd81670b2429d8c1dcc1f8d1dc8f76f33d3fc1b8","impliedFormat":1},{"version":"5a0d920468aa4e792285943cadad77bcb312ba2acf1c665e364ada1b1ee56264","impliedFormat":1},{"version":"c27c5144d294ba5e38f0cd483196f911047500a735490f85f318b4d5eb8ac2cc","impliedFormat":1},{"version":"900d1889110107cea3e40b30217c6e66f19db8683964a57afd9a72ecc821fe21","impliedFormat":1},{"version":"a2e4333bf0c330ae26b90c68e395ad0a8af06121f1c977979c75c4a5f9f6bc29","impliedFormat":1},{"version":"08c027d3d6e294b5607341125d1c4689b4fece03bdb9843bd048515fe496a73e","impliedFormat":1},{"version":"2cbf557a03f80df74106cb7cfb38386db82725b720b859e511bdead881171c32","impliedFormat":1},{"version":"918956b37f3870f02f0659d14bba32f7b0e374fd9c06a241db9da7f5214dcd79","impliedFormat":1},{"version":"260e6d25185809efc852e9c002600ad8a85f8062fa24801f30bead41de98c609","impliedFormat":1},{"version":"dd9694eecd70a405490ad23940ccd8979a628f1d26928090a4b05a943ac61714","impliedFormat":1},{"version":"42ca885a3c8ffdffcd9df252582aef9433438cf545a148e3a5e7568ca8575a56","impliedFormat":1},{"version":"309586820e31406ed70bb03ea8bca88b7ec15215e82d0aa85392da25d0b68630","impliedFormat":1},{"version":"db436ca96e762259f14cb74d62089c7ca513f2fc725e7dcfbac0716602547898","impliedFormat":1},{"version":"1410d60fe495685e97ed7ca6ff8ac6552b8c609ebe63bd97e51b7afe3c75b563","impliedFormat":1},{"version":"c6843fd4514c67ab4caf76efab7772ceb990fbb1a09085fbcf72b4437a307cf7","impliedFormat":1},{"version":"03ed68319c97cd4ce8f1c4ded110d9b40b8a283c3242b9fe934ccfa834e45572","impliedFormat":1},{"version":"956618754d139c7beb3c97df423347433473163d424ff8248af18851dd7d772a","impliedFormat":1},{"version":"7d8f40a7c4cc81db66ac8eaf88f192996c8a5542c192fdebb7a7f2498c18427d","impliedFormat":1},{"version":"c69ecf92a8a9fb3e4019e6c520260e4074dc6cb0044a71909807b8e7cc05bb65","impliedFormat":1},{"version":"07d0370c85ac112aa6f1715dc88bafcee4bcea1483bc6b372be5191d6c1a15c7","impliedFormat":1},{"version":"7fb0164ebb34ead4b1231eca7b691f072acf357773b6044b26ee5d2874c5f296","impliedFormat":1},{"version":"9e4fc88d0f62afc19fa5e8f8c132883378005c278fdb611a34b0d03f5eb6c20c","impliedFormat":1},{"version":"cc9bf8080004ee3d8d9ef117c8df0077d6a76b13cb3f55fd3eefbb3e8fcd1e63","impliedFormat":1},{"version":"1f0ee5ddb64540632c6f9a5b63e242b06e49dd6472f3f5bd7dfeb96d12543e15","impliedFormat":1},{"version":"b6aa8c6f2f5ebfb17126492623691e045468533ec2cc7bd47303ce48de7ab8aa","impliedFormat":1},{"version":"18b86125c67d99150f54225df07349ddd07acde086b55f3eeac1c34c81e424d8","impliedFormat":1},{"version":"68434152ef6e484df25a9bd0f4c9abdfb0d743f5a39bff2b2dc2a0f94ed5f391","impliedFormat":1},{"version":"b848b40bfeb73dfe2e782c5b7588ef521010a3d595297e69386670cbde6b4d82","impliedFormat":1},{"version":"aa79b64f5b3690c66892f292e63dfe3e84eb678a886df86521f67c109d57a0c5","impliedFormat":1},{"version":"a692e092c3b9860c9554698d84baf308ba51fc8f32ddd6646e01a287810b16c6","impliedFormat":1},{"version":"18076e7597cd9baa305cd85406551f28e3450683a699b7152ce7373b6b4a1db7","impliedFormat":1},{"version":"1848ebe5252ccb5ca1ca4ff52114516bdbbc7512589d6d0839beeea768bfb400","impliedFormat":1},{"version":"d2e3a1de4fde9291f9fb3b43672a8975a83e79896466f1af0f50066f78dbf39e","impliedFormat":1},{"version":"d0d03f7d2ba2cf425890e0f35391f1904d0d152c77179ddfc28dfad9d4a09c03","impliedFormat":1},{"version":"e37650b39727a6cf036c45a2b6df055e9c69a0afdd6dbab833ab957eb7f1a389","impliedFormat":1},{"version":"c58d6d730e95e67a62ebd7ba324e04bcde907ef6ba0f41922f403097fe54dd78","impliedFormat":1},{"version":"0f5773d0dd61aff22d2e3223be3b4b9c4a8068568918fb29b3f1ba3885cf701f","impliedFormat":1},{"version":"31073e7d0e51f33b1456ff2ab7f06546c95e24e11c29d5b39a634bc51f86d914","impliedFormat":1},{"version":"9ce0473b0fbaf7287afb01b6a91bd38f73a31093e59ee86de1fd3f352f3fc817","impliedFormat":1},{"version":"6f0d708924c3c4ee64b0fef8f10ad2b4cb87aa70b015eb758848c1ea02db0ed7","impliedFormat":1},{"version":"6addbb18f70100a2de900bace1c800b8d760421cdd33c1d69ee290b71e28003d","impliedFormat":1},{"version":"37569cc8f21262ca62ec9d3aa8eb5740f96e1f325fad3d6aa00a19403bd27b96","impliedFormat":1},{"version":"e0ef70ca30cdc08f55a9511c51a91415e814f53fcc355b14fc8947d32ce9e1aa","impliedFormat":1},{"version":"14be139e0f6d380a3d24aaf9b67972add107bea35cf7f2b1b1febac6553c3ede","impliedFormat":1},{"version":"23195b09849686462875673042a12b7f4cd34b4e27d38e40ca9c408dae8e6656","impliedFormat":1},{"version":"ff1731974600a4dad7ec87770e95fc86ca3d329b1ce200032766340f83585e47","impliedFormat":1},{"version":"91bc53a57079cf32e1a10ccf1a1e4a068e9820aa2fc6abc9af6bd6a52f590ffb","impliedFormat":1},{"version":"8dd284442b56814717e70f11ca22f4ea5b35feeca680f475bfcf8f65ba4ba296","impliedFormat":1},{"version":"a304e0af52f81bd7e6491e890fd480f3dc2cb0541dec3c7bd440dba9fea5c34e","impliedFormat":1},{"version":"c60fd0d7a1ba07631dfae8b757be0bffd5ef329e563f9a213e4a5402351c679f","impliedFormat":1},{"version":"02687b095a01969e6e300d246c9566a62fa87029ce2c7634439af940f3b09334","impliedFormat":1},{"version":"e79e530a8216ee171b4aca8fc7b99bd37f5e84555cba57dc3de4cd57580ff21a","impliedFormat":1},{"version":"ceb2c0bc630cca2d0fdd48b0f48915d1e768785efaabf50e31c8399926fee5b1","impliedFormat":1},{"version":"f351eaa598ba2046e3078e5480a7533be7051e4db9212bb40f4eeb84279aa24d","impliedFormat":1},{"version":"12aeda564ee3f1d96ac759553d6749534fafeb2e5142ea2867f22ed39f9d3260","impliedFormat":1},{"version":"4ce53edb8fb1d2f8b2f6814084b773cdf5846f49bf5a426fbe4029327bda95bf","impliedFormat":1},{"version":"85d63aaff358e8390b666a6bc68d3f56985f18764ab05f750cb67910f7bccb1a","impliedFormat":1},{"version":"0a0bf0cb43af5e0ac1703b48325ebc18ad86f6bf796bdbe96a429c0e95ca4486","impliedFormat":1},{"version":"563573a23a61b147358ddee42f88f887817f0de1fc5dbc4be7603d53cbd467ad","impliedFormat":1},{"version":"dd0cad0db617f71019108686cf5caabcad13888b2ae22f889a4c83210e4ba008","impliedFormat":1},{"version":"f08d2151bd91cdaa152532d51af04e29201cfc5d1ea40f8f7cfca0eb4f0b7cf3","impliedFormat":1},{"version":"b9c889d8a4595d02ebb3d3a72a335900b2fe9e5b5c54965da404379002b4ac44","impliedFormat":1},{"version":"a3cd30ebae3d0217b6b3204245719fc2c2f29d03b626905cac7127e1fb70e79c","impliedFormat":1},{"version":"bd56c2399a7eadccfca7398ca2244830911bdbb95b8ab7076e5a9210e9754696","impliedFormat":1},{"version":"f52fb387ac45e7b8cdc98209714c4aedc78d59a70f92e9b5041309b6b53fc880","impliedFormat":1},{"version":"1502a23e43fd7e9976a83195dc4eaf54acaff044687e0988a3bd4f19fc26b02b","impliedFormat":1},{"version":"5faa3d4b828440882a089a3f8514f13067957f6e5e06ec21ddd0bc2395df1c33","impliedFormat":1},{"version":"f0f95d40b0b5a485b3b97bd99931230e7bf3cbbe1c692bd4d65c69d0cdd6fa9d","impliedFormat":1},{"version":"380b4fe5dac74984ac6a58a116f7726bede1bdca7cec5362034c0b12971ac9c1","impliedFormat":1},{"version":"00de72aa7abede86b016f0b3bfbf767a08b5cff060991b0722d78b594a4c2105","impliedFormat":1},{"version":"965759788855797f61506f53e05c613afb95b16002c60a6f8653650317870bc3","impliedFormat":1},{"version":"f70a315e029dacf595f025d13fa7599e8585d5ccfc44dd386db2aa6596aaf553","impliedFormat":1},{"version":"f385a078ad649cc24f8c31e4f2e56a5c91445a07f25fbdc4a0a339c964b55679","impliedFormat":1},{"version":"08599363ef46d2c59043a8aeec3d5e0d87e32e606c7b1acf397e43f8acadc96a","impliedFormat":1},{"version":"4f5bbef956920cfd90f2cbffccb3c34f8dfc64faaba368d9d41a46925511b6b0","impliedFormat":1},{"version":"0ae9d5bbf4239616d06c50e49fc21512278171c1257a1503028fc4a95ada3ed0","impliedFormat":1},{"version":"cba49e77f6c1737f7a3ce9a50b484d21980665fff93c1c64e0ee0b5086ea460a","impliedFormat":1},{"version":"9c686df0769cca468ebf018749df4330d5ff9414e0d226c1956ebaf45c85ff61","impliedFormat":1},{"version":"89d5970d28f207d30938563e567e67395aa8c1789c43029fe03fe1d07893c74c","impliedFormat":1},{"version":"869e789f7a8abcc769f08ba70b96df561e813a4001b184d3feb8c3d13b095261","impliedFormat":1},{"version":"392f3eb64f9c0f761eb7a391d9fbef26ffa270351d451d11bd70255664170acc","impliedFormat":1},{"version":"f829212a0e8e4fd1b079645d4e97e6ec73734dd21aae4dfc921d2958774721d0","impliedFormat":1},{"version":"5e20af039b2e87736fd7c9e4b47bf143c46918856e78ce21da02a91c25d817e8","impliedFormat":1},{"version":"f321514602994ba6e0ab622ef52debd4e9f64a7b4494c03ee017083dc1965753","impliedFormat":1},{"version":"cc8734156129aa6230a71987d94bdfac723045459da707b1804ecec321e60937","impliedFormat":1},{"version":"bb89466514349b86260efdee9850e497d874e4098334e9b06a146f1e305fca3f","impliedFormat":1},{"version":"fc0ee9d0476dec3d1b37a0f968e371a3d23aac41742bc6706886e1c6ac486749","impliedFormat":1},{"version":"f7da03d84ce7121bc17adca0af1055021b834e861326462a90dbf6154cf1e106","impliedFormat":1},{"version":"fed8c2c205f973bfb03ef3588750f60c1f20e2362591c30cd2c850213115163b","impliedFormat":1},{"version":"32a2b99a3aacda16747447cc9589e33c363a925d221298273912ecf93155e184","impliedFormat":1},{"version":"07bfa278367913dd253117ec68c31205825b2626e1cb4c158f2112e995923ee8","impliedFormat":1},{"version":"6a76e6141ff2fe28e88e63e0d06de686f31184ab68b04ae16f0f92103295cc2a","impliedFormat":1},{"version":"f05d5d16d85abe57eb713bc12efefc00675c09016e3292360e2de0790f51fa48","impliedFormat":1},{"version":"2e3ceed776a470729c084f3a941101d681dd1867babbaf6e1ca055d738dd3878","impliedFormat":1},{"version":"3d9fb85cc7089ca54873c9924ff47fcf05d570f3f8a3a2349906d6d953fa2ccf","impliedFormat":1},{"version":"d82c245bfb76da44dd573948eca299ff75759b9714f8410468d2d055145a4b64","impliedFormat":1},{"version":"6b5b31af3f5cfcf5635310328f0a3a94f612902024e75dc484eb79123f5b8ebe","impliedFormat":1},{"version":"db08c1807e3ae065930d88a3449d926273816d019e6c2a534e82da14e796686d","impliedFormat":1},{"version":"9e5c7463fc0259a38938c9afbdeda92e802cff87560277fd3e385ad24663f214","impliedFormat":1},{"version":"ef83477cca76be1c2d0539408c32b0a2118abcd25c9004f197421155a4649c37","impliedFormat":1},{"version":"2c3936b0f811f38ab1a4f0311993bf599c27c2da5750e76aa5dfbed8193c9922","impliedFormat":1},{"version":"c253c7ea2877126b1c3311dc70b7664fe4d696cb09215857b9d7ea8b7fdce1f0","impliedFormat":1},{"version":"56c834913179e022eb8254fd1da303d49927911e0542820182ee754335fdd2f2","impliedFormat":99},"a7a2dad2683bb1f75a177d5f486681efa99478fc362437eabda11b813f77929b",{"version":"e559d0282980f0f8f2e400dc668514cdb2b0f2dac62f427dbc4df3272eb811fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"a2885552d05eef8f56b5cc6aa822deec70107daa6a283ecc2b517bc731057110","impliedFormat":1},"4a889fb6f6e17fa0e1f1f75e0e891afa3c8e6c1038067a2c233ec78d3f1ec1a3","27bdfd2fd9db756803f2854b69f1f924a1bd9078ccc024689bbe4333c2d16af5","56bbffabf12b7482dcbcfe0a55abca363f22d15c59ab878e437eebb3c766aff1","125e2906036f755c310b2f2bbd01d157521320e4620cd57e023a08770f27df1a","190d23411634db33d5cac07ba42c85eadac0535acf953eed60190db3834ffdf3","fc673bfa5de33a651b7dff6a80cf7946a00e25583ff76c790cacb4dcbe8be602","1ad25d1f993f33e2f4b0000d7fa4b482118546080878d99aa12f9b714f021d5f","2e7f25a4d03d0dd22da31497ffc54d2130db045182861ce63ea3bdd586105096",{"version":"2efbc5086f10635b6507392c3d9d073e94d9308a5cf08af5ae4389c250d1791c","affectsGlobalScope":true,"impliedFormat":99}],"root":[197,364,[367,374]],"options":{"composite":true},"referencedMap":[[197,1],[364,2],[372,3],[370,4],[368,4],[367,4],[371,5],[369,4],[373,6],[374,3],[190,7],[188,8],[366,3],[155,8],[101,9],[102,9],[103,10],[55,11],[104,12],[105,13],[106,14],[50,8],[53,15],[51,8],[52,8],[107,16],[108,17],[109,18],[110,19],[111,20],[112,21],[113,21],[114,22],[115,23],[116,24],[117,25],[56,8],[54,8],[118,26],[119,27],[120,28],[154,29],[121,30],[122,8],[123,31],[124,32],[125,33],[126,34],[127,35],[128,36],[129,37],[130,38],[131,39],[132,39],[133,40],[134,8],[135,41],[136,42],[138,43],[137,44],[139,45],[140,46],[141,47],[142,48],[143,49],[144,50],[145,51],[146,52],[147,53],[148,54],[149,55],[150,56],[151,57],[57,8],[58,8],[59,8],[98,58],[99,8],[100,8],[152,59],[153,60],[196,61],[191,62],[194,63],[189,8],[60,8],[363,64],[187,65],[375,66],[365,67],[162,8],[192,8],[179,68],[177,69],[178,70],[166,71],[167,69],[174,72],[165,73],[170,74],[180,8],[171,75],[176,76],[181,77],[164,78],[172,79],[173,80],[168,81],[175,68],[169,82],[156,83],[163,8],[362,84],[333,85],[223,86],[329,8],[296,87],[266,88],[252,89],[330,8],[277,8],[287,8],[306,90],[200,8],[337,91],[339,92],[338,93],[289,94],[288,95],[291,96],[290,97],[250,8],[340,98],[344,99],[342,100],[204,101],[205,101],[206,8],[253,102],[303,103],[302,8],[315,104],[240,105],[309,8],[298,8],[357,106],[359,8],[226,107],[225,108],[318,109],[321,110],[210,111],[322,112],[236,113],[207,114],[212,115],[335,116],[272,117],[356,86],[328,118],[327,119],[214,120],[215,8],[239,121],[230,122],[231,123],[238,124],[229,125],[228,126],[237,127],[279,8],[216,8],[222,8],[217,8],[218,128],[220,129],[211,8],[270,8],[324,130],[271,116],[301,8],[293,8],[308,131],[307,132],[341,100],[345,133],[343,134],[203,135],[358,8],[295,107],[227,136],[313,137],[312,8],[267,138],[255,139],[256,8],[235,140],[299,141],[300,141],[242,142],[243,8],[251,8],[219,143],[201,8],[269,144],[233,8],[208,8],[224,86],[317,145],[360,146],[261,147],[273,148],[346,93],[348,149],[347,149],[264,150],[265,151],[234,8],[198,8],[276,8],[275,152],[320,112],[316,8],[354,152],[258,153],[241,154],[257,153],[259,155],[262,152],[209,109],[311,8],[352,156],[331,157],[285,158],[284,8],[280,159],[305,160],[281,159],[283,161],[282,162],[304,117],[334,163],[332,164],[254,165],[232,8],[260,166],[349,100],[351,133],[350,134],[353,167],[323,168],[314,8],[355,169],[297,170],[292,8],[310,171],[263,172],[294,173],[247,8],[278,8],[221,152],[361,8],[325,174],[326,8],[199,8],[274,152],[202,8],[268,175],[213,8],[246,8],[244,8],[245,8],[286,8],[336,152],[249,152],[319,86],[248,176],[1,8],[48,8],[49,8],[9,8],[13,8],[12,8],[3,8],[14,8],[15,8],[16,8],[17,8],[18,8],[19,8],[20,8],[21,8],[4,8],[22,8],[23,8],[5,8],[24,8],[28,8],[25,8],[26,8],[27,8],[29,8],[30,8],[31,8],[6,8],[32,8],[33,8],[34,8],[35,8],[7,8],[39,8],[36,8],[37,8],[38,8],[40,8],[8,8],[41,8],[46,8],[47,8],[42,8],[43,8],[44,8],[45,8],[2,8],[11,8],[10,8],[193,8],[76,177],[86,178],[75,177],[96,179],[67,180],[66,181],[95,182],[89,183],[94,184],[69,185],[83,186],[68,187],[92,188],[64,189],[63,182],[93,190],[65,191],[70,192],[71,8],[74,192],[61,8],[97,193],[87,194],[78,195],[79,196],[81,197],[77,198],[80,199],[90,182],[72,200],[73,201],[82,202],[62,203],[85,194],[84,192],[88,8],[91,204],[186,205],[161,206],[160,207],[158,207],[157,8],[159,208],[184,8],[183,8],[182,8],[185,209],[195,210]],"semanticDiagnosticsPerFile":[[128,[{"start":84273,"length":13,"messageText":"All declarations of 'noDeprecation' must have identical modifiers.","category":1,"code":2687}]],[186,[{"start":559,"length":17,"code":2307,"category":1,"messageText":{"messageText":"Cannot find module 'rollup/parseAst' or its corresponding type declarations.","category":1,"code":2307,"next":[{"info":{"moduleReference":"rollup/parseAst"}}]}}]],[187,[{"start":281,"length":11,"messageText":"Cannot find module '@swc/core' or its corresponding type declarations.","category":1,"code":2307}]],[194,[{"start":870,"length":2,"messageText":"Module '\"D:/Flutter/MoveZ/electron-app/node_modules/typescript/lib/typescript\"' can only be default-imported using the 'esModuleInterop' flag","category":1,"code":1259,"relatedInformation":[{"file":"./node_modules/typescript/lib/typescript.d.ts","start":588072,"length":12,"messageText":"This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.","category":1,"code":2594}]}]],[197,[{"start":106,"length":3,"messageText":"Module '\"D:/Flutter/MoveZ/electron-app/node_modules/@vitejs/plugin-vue/dist/index\"' can only be default-imported using the 'esModuleInterop' flag","category":1,"code":1259,"relatedInformation":[{"file":"./node_modules/@vitejs/plugin-vue/dist/index.d.ts","start":4453,"length":19,"messageText":"This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.","category":1,"code":2594}]}]],[363,[{"start":89,"length":6,"code":2307,"category":1,"messageText":{"messageText":"Cannot find module 'conf' or its corresponding type declarations.","category":1,"code":2307,"next":[{"info":{"moduleReference":"conf"}}]}},{"start":119,"length":6,"code":2307,"category":1,"messageText":{"messageText":"Cannot find module 'conf' or its corresponding type declarations.","category":1,"code":2307,"next":[{"info":{"moduleReference":"conf"}}]}}]],[364,[{"start":258,"length":3,"code":2339,"category":1,"messageText":"Property 'get' does not exist on type 'ElectronStore<{ cliPath: string; }>'."}]],[365,[{"start":1004537,"length":13,"messageText":"All declarations of 'noDeprecation' must have identical modifiers.","category":1,"code":2687}]],[371,[{"start":413,"length":5,"code":2339,"category":1,"messageText":"Property 'store' does not exist on type 'ElectronStore'."},{"start":515,"length":3,"code":2339,"category":1,"messageText":"Property 'set' does not exist on type 'ElectronStore'."},{"start":548,"length":5,"code":2339,"category":1,"messageText":"Property 'store' does not exist on type 'ElectronStore'."},{"start":694,"length":3,"code":2339,"category":1,"messageText":"Property 'set' does not exist on type 'ElectronStore'."},{"start":740,"length":5,"code":2339,"category":1,"messageText":"Property 'store' does not exist on type 'ElectronStore'."}]]],"affectedFilesPendingEmit":[197,364,372,370,368,367,371,369,373,374],"emitSignatures":[197,364,367,368,369,370,371,372,373,374],"version":"5.9.3"} \ No newline at end of file diff --git a/electron-app/tsconfig.web.tsbuildinfo b/electron-app/tsconfig.web.tsbuildinfo deleted file mode 100644 index e027ff4..0000000 --- a/electron-app/tsconfig.web.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/importmeta.d.ts","./node_modules/vite/client.d.ts","./node_modules/@vue/shared/dist/shared.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@vue/compiler-core/dist/compiler-core.d.ts","./node_modules/@vue/compiler-dom/dist/compiler-dom.d.ts","./node_modules/@vue/reactivity/dist/reactivity.d.ts","./node_modules/@vue/runtime-core/dist/runtime-core.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts","./node_modules/vue/dist/vue.d.mts","./src/env.d.ts","./node_modules/vue-demi/lib/index.d.ts","./node_modules/pinia/dist/pinia.d.ts","./node_modules/vue-router/dist/router-cwonjprp.d.mts","./node_modules/vue-router/dist/vue-router.d.mts","./src/router/index.ts","./src/main.ts","./src/types/bridge.d.ts","./src/stores/sessions.ts","./src/stores/settings.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/keyv/src/index.d.ts","./node_modules/@types/http-cache-semantics/index.d.ts","./node_modules/@types/responselike/index.d.ts","./node_modules/@types/cacheable-request/index.d.ts","./node_modules/@types/ms/index.d.ts","./node_modules/@types/debug/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/@types/fs-extra/index.d.ts","./node_modules/@types/keyv/index.d.ts","./node_modules/@types/yauzl/index.d.ts"],"fileIdsList":[[55,79,128,145,146],[79,128,145,146],[79,128,139,142,145,146,171,178,179,180,181],[79,128,145,146,183],[79,128,140,145,146,178],[79,128,139,145,146,178],[79,125,126,128,145,146],[79,127,128,145,146],[128,145,146],[79,128,133,145,146,163],[79,128,129,134,139,145,146,148,160,171],[79,128,129,130,139,145,146,148],[74,75,76,79,128,145,146],[79,128,131,145,146,172],[79,128,132,133,140,145,146,149],[79,128,133,145,146,160,168],[79,128,134,136,139,145,146,148],[79,127,128,135,145,146],[79,128,136,137,145,146],[79,128,138,139,145,146],[79,127,128,139,145,146],[79,128,139,140,141,145,146,160,171],[79,128,139,140,141,145,146,155,160,163],[79,121,128,136,139,142,145,146,148,160,171],[79,128,139,140,142,143,145,146,148,160,168,171],[79,128,142,144,145,146,160,168,171],[77,78,79,80,81,82,83,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177],[79,128,139,145,146],[79,128,145,146,147,171],[79,128,136,139,145,146,148,160],[79,128,145,146,149],[79,128,145,146,150],[79,127,128,145,146,151],[79,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177],[79,128,145,146,153],[79,128,145,146,154],[79,128,139,145,146,155,156],[79,128,145,146,155,157,172,174],[79,128,140,145,146],[79,128,139,145,146,160,161,163],[79,128,145,146,162,163],[79,128,145,146,160,161],[79,128,145,146,163],[79,128,145,146,164],[79,125,128,145,146,160,165,171],[79,128,139,145,146,166,167],[79,128,145,146,166,167],[79,128,133,145,146,148,160,168],[79,128,145,146,169],[79,128,145,146,148,170],[79,128,142,145,146,154,171],[79,128,133,145,146,172],[79,128,145,146,160,173],[79,128,145,146,147,174],[79,128,145,146,175],[79,121,128,145,146],[79,121,128,139,141,145,146,151,160,163,171,173,174,176],[79,128,145,146,160,177],[79,128,142,145,146,160,178],[79,128,139,145,146,160,178],[54,55,56,79,128,145,146],[57,79,128,145,146],[54,79,128,145,146],[54,59,60,62,79,128,145,146],[59,60,61,62,79,128,145,146],[63,65,68,79,128,145,146],[79,93,97,128,145,146,171],[79,93,128,145,146,160,171],[79,88,128,145,146],[79,90,93,128,145,146,168,171],[79,128,145,146,148,168],[79,128,145,146,178],[79,88,128,145,146,178],[79,90,93,128,145,146,148,171],[79,85,86,89,92,128,139,145,146,160,171],[79,93,100,128,145,146],[79,85,91,128,145,146],[79,93,114,115,128,145,146],[79,89,93,128,145,146,163,171,178],[79,114,128,145,146,178],[79,87,88,128,145,146,178],[79,93,128,145,146],[79,87,88,89,90,91,92,93,94,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,115,116,117,118,119,120,128,145,146],[79,93,108,128,145,146],[79,93,100,101,128,145,146],[79,91,93,101,102,128,145,146],[79,92,128,145,146],[79,85,88,93,128,145,146],[79,93,97,101,102,128,145,146],[79,97,128,145,146],[79,91,93,96,128,145,146,171],[79,85,90,93,100,128,145,146],[79,128,145,146,160],[79,88,93,114,128,145,146,176,178],[52,79,128,145,146],[48,79,128,145,146],[49,79,128,145,146],[50,51,79,128,145,146],[63,66,68,79,128,145,146],[63,66,67,68,79,128,145,146],[58,62,79,128,145,146],[53,63,66,68,79,128,145,146],[53,63,64,66,68,69,79,128,145,146],[64,68,79,128,145,146],[63,66,68,71,79,128,145,146]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"19efad8495a7a6b064483fccd1d2b427403dd84e67819f86d1c6ee3d7abf749c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eef826bc4a19de22155487984e345a34c9cd511dd1170edc7a447cb8231dd4a","affectsGlobalScope":true,"impliedFormat":99},{"version":"f468b74459f1ad4473b36a36d49f2b255f3c6b5d536c81239c2b2971df089eaf","impliedFormat":1},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"524a409ad72186b7f6cb16898c349465cfa876f641d6cb6137b3123d5cfca619","impliedFormat":1},{"version":"ebe84ad8344962b7117a3b95065f47383215020eaf1b626463863b45b4d16e62","impliedFormat":1},{"version":"de5abbc02627403ac5796572520dc2b42df54f1257e7218a9ef37509d9b56972","impliedFormat":1},{"version":"bc41b74f965a6bf0a038e32753c6920aad2f2fed97cd32a105e9677070ed50e5","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"88ad1af02cacc61bf79683b021d326eeafc91231660a06495c5046d4649ec3a2","impliedFormat":1},{"version":"c0191592be8eb7906f99ac4b8798d80a585b94001ea1a5f50d6ce5b0d13a5c62","impliedFormat":99},"b61ba723029bad40d8f46de302240fd24ce70dd3ff906d1822926ef179f69628",{"version":"52f5c39e78a90c1d8ed7db18f39d890b2e8464a3f44d4233617893f6648e317d","impliedFormat":1},{"version":"a69e8bce30aea7ec98f3b6ddfbc378c92826fede01aafbaec703057c2503ea51","impliedFormat":1},{"version":"860814185d89237c84a6bd1e4f108c2c0b2401609a74a33bc7cab14ea4ee9f21","impliedFormat":99},{"version":"e1ddcbacb7658bf0683bca4ebd31bd05f64823712651263bfc75128a45f00ff7","impliedFormat":99},{"version":"602692f3ff061bbc77b2797fcf75c8cb5ef5ff424e981b50d46e6acaba777fbf","signature":"160b37c5f96761b5535fe25fdbfb3c6dd7b24327c9542918633db70d7527de9b"},{"version":"853dfac490f3e1f82c072bb00f11e5f4696a2fb7a245e6602db34302ed37d410","signature":"123a0559c56d522e9dbb0a1b42b74b96ae78937e658f1eb35827768973e3825c"},{"version":"24963113e39ffb79bf7300aaa394f413620234c545113fc689f817fbab12be30","affectsGlobalScope":true},"c767e36f9ca15d4d54a06792bc8958cf1088e3cccb6b0bcc3d56c5ab4295d1df","2160074a5c3efcd5374601fdf1d880092795df9cf034d2eb37291f7a15a48c91",{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"6b039f55681caaf111d5eb84d292b9bee9e0131d0db1ad0871eef0964f533c73","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"42baf4ca38c38deaf411ea73f37bc39ff56c6e5c761a968b64ac1b25c92b5cd8","impliedFormat":1},{"version":"4f6ae308c5f2901f2988c817e1511520619e9025b9b12cc7cce2ab2e6ffed78a","impliedFormat":1},{"version":"8718fa41d7cf4aa91de4e8f164c90f88e0bf343aa92a1b9b725a9c675c64e16b","impliedFormat":1},{"version":"f992cd6cc0bcbaa4e6c810468c90f2d8595f8c6c3cf050c806397d3de8585562","impliedFormat":1},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"89e326922cadcc2331d7e851011cf9f0456a681aaf3c95b48b81f8d80e8cdfba","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ed19da84b7dbf00952ad0b98ce5c194f1903bcf7c94d8103e8e0d63b271543ae","impliedFormat":1},{"version":"fec943fdb3275eb6e006b35e04a8e2e99e9adf3f4b969ddf15315ac7575a93e4","impliedFormat":1},{"version":"74d5a87c3616cd5d8691059d531504403aa857e09cbaecb1c64dfb9ace0db185","impliedFormat":1}],"root":[64,[69,73]],"options":{"composite":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":7},"referencedMap":[[56,1],[55,2],[182,3],[184,4],[185,2],[186,5],[180,2],[187,6],[183,2],[125,7],[126,7],[127,8],[79,9],[128,10],[129,11],[130,12],[74,2],[77,13],[75,2],[76,2],[131,14],[132,15],[133,16],[134,17],[135,18],[136,19],[137,19],[138,20],[139,21],[140,22],[141,23],[80,2],[78,2],[142,24],[143,25],[144,26],[178,27],[145,28],[146,2],[147,29],[148,30],[149,31],[150,32],[151,33],[152,34],[153,35],[154,36],[155,37],[156,37],[157,38],[158,2],[159,39],[160,40],[162,41],[161,42],[163,43],[164,44],[165,45],[166,46],[167,47],[168,48],[169,49],[170,50],[171,51],[172,52],[173,53],[174,54],[175,55],[81,2],[82,2],[83,2],[122,56],[123,2],[124,2],[176,57],[177,58],[181,59],[188,60],[57,61],[58,62],[59,63],[60,64],[62,65],[54,2],[84,2],[61,2],[179,28],[66,66],[46,2],[47,2],[8,2],[9,2],[11,2],[10,2],[2,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[3,2],[20,2],[21,2],[4,2],[22,2],[26,2],[23,2],[24,2],[25,2],[27,2],[28,2],[29,2],[5,2],[30,2],[31,2],[32,2],[33,2],[6,2],[37,2],[34,2],[35,2],[36,2],[38,2],[7,2],[39,2],[44,2],[45,2],[40,2],[41,2],[42,2],[43,2],[1,2],[100,67],[110,68],[99,67],[120,69],[91,70],[90,71],[119,72],[113,73],[118,74],[93,75],[107,76],[92,77],[116,78],[88,79],[87,72],[117,80],[89,81],[94,82],[95,2],[98,82],[85,2],[121,83],[111,84],[102,85],[103,86],[105,87],[101,88],[104,89],[114,72],[96,90],[97,91],[106,92],[86,93],[109,84],[108,82],[112,2],[115,94],[53,95],[49,96],[48,2],[50,97],[51,2],[52,98],[65,99],[67,99],[68,100],[63,101],[64,102],[70,103],[69,104],[72,105],[73,105],[71,2]],"affectedFilesPendingEmit":[[70,17],[69,17],[72,17],[73,17]],"emitSignatures":[69,70,72,73],"version":"5.9.3"} \ No newline at end of file From 9d002fe11e467ef85f6e374320b4350772d40348 Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:38:24 -0400 Subject: [PATCH 3/8] Fix web dashboard, sync API and migration wizard - Sync API now matches the spec ({ sessions, count }); encrypted blobs live in their own sync_blobs table instead of masquerading as an AiSession, push/pull events record real session counts - New `php artisan movez:token ` issues API tokens; api_token is hidden from serialization and seeded tokens are printed once - Migration wizard returns the exact `movez transfer` command to run locally instead of claiming a migration was queued when nothing ran; checks response.ok and shows validation errors - Dashboard stat cards render (runtime template string needed the compiler build) via a proper StatCard SFC - Stop sending raw session_data blobs to pages; safe JSON decoding - Remove leftover app.js/bootstrap.js/welcome.blade.php, add tsconfig, vue-tsc type-check and test:e2e scripts, fix ambiguous e2e locators - strict_types everywhere, Pest-only tests, MoveZ naming; new tests for dashboard, sessions, sync isolation and the wizard command Co-Authored-By: Claude Opus 5.5 --- web/.env.example | 2 +- web/app/Http/Controllers/Controller.php | 2 + .../Http/Controllers/MigrationController.php | 61 +++- .../Http/Controllers/ProjectController.php | 6 +- web/app/Http/Controllers/SyncController.php | 52 ++-- .../Http/Middleware/HandleInertiaRequests.php | 2 + web/app/Models/AiSession.php | 17 +- web/app/Models/SyncBlob.php | 27 ++ web/app/Models/User.php | 30 +- web/app/Providers/AppServiceProvider.php | 2 + web/bootstrap/app.php | 2 + web/bootstrap/providers.php | 2 + web/composer.json | 6 +- web/composer.lock | 2 +- web/database/factories/SyncEventFactory.php | 25 ++ web/database/factories/UserFactory.php | 2 + .../0001_01_01_000000_create_users_table.php | 2 + .../0001_01_01_000001_create_cache_table.php | 2 + .../0001_01_01_000002_create_jobs_table.php | 2 + ...026_01_01_000001_create_projects_table.php | 2 + ..._01_01_000002_create_ai_sessions_table.php | 2 + ..._01_01_000003_create_sync_events_table.php | 2 + ...01_000004_add_api_token_to_users_table.php | 2 + ...6_01_01_000005_create_sync_blobs_table.php | 28 ++ web/database/seeders/DatabaseSeeder.php | 5 +- web/e2e/dashboard.spec.ts | 13 +- web/package-lock.json | 179 ++++++++++- web/package.json | 9 +- web/resources/css/app.css | 2 + web/resources/js/Components/StatCard.vue | 14 + web/resources/js/Pages/Dashboard.vue | 10 +- web/resources/js/Pages/Migration/Wizard.vue | 56 +++- web/resources/js/app.js | 1 - web/resources/js/app.ts | 5 +- web/resources/js/bootstrap.js | 4 - web/resources/js/types/index.d.ts | 6 +- web/resources/views/welcome.blade.php | 277 ------------------ web/routes/api.php | 2 + web/routes/console.php | 24 +- web/routes/web.php | 2 + web/tests/Feature/DashboardAndSessionTest.php | 54 ++++ web/tests/Feature/MigrationControllerTest.php | 31 +- web/tests/Feature/SyncServerTest.php | 96 ++++-- web/tests/TestCase.php | 2 + web/tests/Unit/ExampleTest.php | 17 +- web/tsconfig.json | 21 ++ 46 files changed, 693 insertions(+), 419 deletions(-) create mode 100644 web/app/Models/SyncBlob.php create mode 100644 web/database/factories/SyncEventFactory.php create mode 100644 web/database/migrations/2026_01_01_000005_create_sync_blobs_table.php create mode 100644 web/resources/js/Components/StatCard.vue delete mode 100644 web/resources/js/app.js delete mode 100644 web/resources/js/bootstrap.js delete mode 100644 web/resources/views/welcome.blade.php create mode 100644 web/tests/Feature/DashboardAndSessionTest.php create mode 100644 web/tsconfig.json diff --git a/web/.env.example b/web/.env.example index c0660ea..829bd41 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Laravel +APP_NAME=MoveZ APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/web/app/Http/Controllers/Controller.php b/web/app/Http/Controllers/Controller.php index 8677cd5..e2af3d2 100644 --- a/web/app/Http/Controllers/Controller.php +++ b/web/app/Http/Controllers/Controller.php @@ -1,5 +1,7 @@ self::SUPPORTED_TOOLS, - 'projects' => Project::latest()->get(['id', 'name']), + 'supportedTools' => self::SOURCE_TOOLS, + 'writableTools' => self::TARGET_TOOLS, + 'projects' => Project::latest()->get(['id', 'name', 'path']), ]); } - public function start(Request $request): \Illuminate\Http\JsonResponse + /** + * Session files live on the user's machine, not on this server, so the + * wizard validates the choices and returns the CLI command to run locally. + */ + public function start(Request $request): JsonResponse { $validated = $request->validate([ - 'from_tool' => ['required', 'string', 'in:' . implode(',', self::SUPPORTED_TOOLS)], - 'to_tool' => ['required', 'string', 'in:' . implode(',', self::SUPPORTED_TOOLS)], + 'from_tool' => ['required', 'string', 'in:' . implode(',', self::SOURCE_TOOLS)], + 'to_tool' => ['required', 'string', 'in:' . implode(',', self::TARGET_TOOLS), 'different:from_tool'], 'project_id' => ['nullable', 'integer', 'exists:projects,id'], - 'from_path' => ['nullable', 'string'], - 'to_path' => ['nullable', 'string'], + 'from_path' => ['nullable', 'string', 'max:1024'], + 'to_path' => ['nullable', 'string', 'max:1024'], ]); - // Dispatch a migration job (async via Horizon) - // For now, return success — the job implementation is in Phase 6 Jobs/ + $projectPath = isset($validated['project_id']) + ? Project::whereKey($validated['project_id'])->value('path') + : null; + + $args = [ + 'movez', 'transfer', + '--from=' . $validated['from_tool'], + '--to=' . $validated['to_tool'], + ]; + + if ($projectPath) { + $args[] = '--project=' . $this->quote($projectPath); + } + + if (!empty($validated['from_path']) && !empty($validated['to_path'])) { + $args[] = '--from-path=' . $this->quote($validated['from_path']); + $args[] = '--to-path=' . $this->quote($validated['to_path']); + } + return response()->json([ - 'status' => 'queued', - 'message' => 'Migration queued. Sessions will appear shortly.', - 'data' => $validated, + 'status' => 'ready', + 'message' => 'Run this command on the machine that has your sessions.', + 'command' => implode(' ', $args), ]); } + + private function quote(string $value): string + { + return preg_match('#^[A-Za-z0-9_/\\\\:.\-]+$#', $value) ? $value : '"' . str_replace('"', '\"', $value) . '"'; + } } diff --git a/web/app/Http/Controllers/ProjectController.php b/web/app/Http/Controllers/ProjectController.php index 4b4dde5..3f7da84 100644 --- a/web/app/Http/Controllers/ProjectController.php +++ b/web/app/Http/Controllers/ProjectController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers; use App\Models\Project; -use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; @@ -12,8 +11,7 @@ class ProjectController extends Controller { public function index(): Response { - $projects = Project::with('aiSessions') - ->withCount('aiSessions') + $projects = Project::withCount('aiSessions') ->latest() ->paginate(20); @@ -24,8 +22,6 @@ public function index(): Response public function show(Project $project): Response { - $project->load('aiSessions'); - return Inertia::render('Projects/Show', [ 'project' => $project, 'sessions' => $project->aiSessions()->latest()->get(), diff --git a/web/app/Http/Controllers/SyncController.php b/web/app/Http/Controllers/SyncController.php index a6a45ac..fea947e 100644 --- a/web/app/Http/Controllers/SyncController.php +++ b/web/app/Http/Controllers/SyncController.php @@ -3,57 +3,61 @@ namespace App\Http\Controllers; -use App\Models\AiSession; +use App\Models\SyncBlob; use App\Models\SyncEvent; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class SyncController extends Controller { + /** + * POST /api/sync/push — body: { sessions: , count?: int } + */ public function push(Request $request): JsonResponse { $validated = $request->validate([ - 'data' => ['required', 'string'], + 'sessions' => ['required', 'string'], + 'count' => ['nullable', 'integer', 'min:0'], + 'source_tool' => ['nullable', 'string', 'max:50'], ]); - $user = auth()->user(); - - // Store the encrypted blob (it stays encrypted at rest) - AiSession::updateOrCreate( - ['session_id' => 'sync_blob_' . $user->id], - [ - 'project_id' => null, - 'source_tool' => 'sync', - 'title' => 'Synced sessions', - 'session_data' => $validated['data'], - 'exported_at' => now(), - ] + $user = $request->user(); + $count = (int) ($validated['count'] ?? 0); + + // Stored as-is: the payload stays encrypted at rest + SyncBlob::updateOrCreate( + ['user_id' => $user->id], + ['payload' => $validated['sessions'], 'session_count' => $count], ); SyncEvent::create([ 'user_id' => $user->id, 'event_type' => 'push', - 'session_count' => 0, + 'session_count' => $count, + 'source_tool' => $validated['source_tool'] ?? null, ]); - return response()->json(['status' => 'ok']); + return response()->json(['status' => 'ok', 'count' => $count]); } + /** + * GET /api/sync/pull — returns { sessions: |null, count: int } + */ public function pull(Request $request): JsonResponse { - $user = auth()->user(); - - $blob = AiSession::where('session_id', 'sync_blob_' . $user->id)->first(); + $user = $request->user(); + $blob = SyncBlob::where('user_id', $user->id)->first(); - if (!$blob) { - return response()->json(['data' => null]); + if ($blob === null) { + return response()->json(['sessions' => null, 'count' => 0]); } SyncEvent::create([ - 'user_id' => $user->id, - 'event_type' => 'pull', + 'user_id' => $user->id, + 'event_type' => 'pull', + 'session_count' => $blob->session_count, ]); - return response()->json(['data' => $blob->session_data]); + return response()->json(['sessions' => $blob->payload, 'count' => $blob->session_count]); } } diff --git a/web/app/Http/Middleware/HandleInertiaRequests.php b/web/app/Http/Middleware/HandleInertiaRequests.php index c19ce18..b970198 100644 --- a/web/app/Http/Middleware/HandleInertiaRequests.php +++ b/web/app/Http/Middleware/HandleInertiaRequests.php @@ -1,5 +1,7 @@ 'datetime', ]; @@ -27,11 +30,21 @@ public function project(): BelongsTo } /** - * Decrypt and decode session_data on the fly. + * Decode session_data, falling back to an empty array for anything + * that is not a JSON object/array. + * + * @return array */ public function getDecodedSessionData(): array { $raw = $this->session_data; - return is_array($raw) ? $raw : (json_decode($raw, true) ?? []); + + if (is_array($raw)) { + return $raw; + } + + $decoded = is_string($raw) ? json_decode($raw, true) : null; + + return is_array($decoded) ? $decoded : []; } } diff --git a/web/app/Models/SyncBlob.php b/web/app/Models/SyncBlob.php new file mode 100644 index 0000000..b48a33a --- /dev/null +++ b/web/app/Models/SyncBlob.php @@ -0,0 +1,27 @@ + 'integer', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/web/app/Models/User.php b/web/app/Models/User.php index 68f3a66..2d8c0fe 100644 --- a/web/app/Models/User.php +++ b/web/app/Models/User.php @@ -1,12 +1,16 @@ */ @@ -32,6 +37,7 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'api_token', ]; /** @@ -46,4 +52,26 @@ protected function casts(): array 'password' => 'hashed', ]; } + + /** + * Generate a new API token, store its SHA-256 hash and return the plaintext. + * The plaintext is shown once and never stored. + */ + public function issueApiToken(): string + { + $token = Str::random(48); + $this->forceFill(['api_token' => hash('sha256', $token)])->save(); + + return $token; + } + + public function projects(): HasMany + { + return $this->hasMany(Project::class); + } + + public function syncBlob(): HasOne + { + return $this->hasOne(SyncBlob::class); + } } diff --git a/web/app/Providers/AppServiceProvider.php b/web/app/Providers/AppServiceProvider.php index 452e6b6..8325058 100644 --- a/web/app/Providers/AppServiceProvider.php +++ b/web/app/Providers/AppServiceProvider.php @@ -1,5 +1,7 @@ */ +class SyncEventFactory extends Factory +{ + protected $model = SyncEvent::class; + + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'event_type' => $this->faker->randomElement(['push', 'pull']), + 'session_count' => $this->faker->numberBetween(0, 50), + 'source_tool' => $this->faker->randomElement(['cursor', 'claude-code', 'codex', null]), + 'created_at' => now(), + ]; + } +} diff --git a/web/database/factories/UserFactory.php b/web/database/factories/UserFactory.php index c4ceb07..e85bec6 100644 --- a/web/database/factories/UserFactory.php +++ b/web/database/factories/UserFactory.php @@ -1,5 +1,7 @@ id(); + $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete(); + $table->longText('payload'); + $table->unsignedInteger('session_count')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sync_blobs'); + } +}; diff --git a/web/database/seeders/DatabaseSeeder.php b/web/database/seeders/DatabaseSeeder.php index deef294..1b30eaa 100644 --- a/web/database/seeders/DatabaseSeeder.php +++ b/web/database/seeders/DatabaseSeeder.php @@ -7,7 +7,6 @@ use App\Models\Project; use App\Models\User; use Illuminate\Database\Seeder; -use Illuminate\Support\Str; class DatabaseSeeder extends Seeder { @@ -16,7 +15,9 @@ public function run(): void $users = User::factory(2)->create(); foreach ($users as $user) { - $user->update(['api_token' => hash('sha256', Str::random(40))]); + // Only the hash is stored, so print the plaintext once for local testing + $token = $user->issueApiToken(); + $this->command?->info("API token for {$user->email}: {$token}"); $projects = Project::factory(3)->create(['user_id' => $user->id]); diff --git a/web/e2e/dashboard.spec.ts b/web/e2e/dashboard.spec.ts index 0b9c510..58683a5 100644 --- a/web/e2e/dashboard.spec.ts +++ b/web/e2e/dashboard.spec.ts @@ -6,22 +6,23 @@ test.describe('Dashboard', () => { await expect(page).toHaveTitle(/MoveZ/); await expect(page.locator('h1')).toContainText('MoveZ'); - await expect(page.locator('text=Migration Wizard')).toBeVisible(); - await expect(page.locator('text=Projects')).toBeVisible(); + const nav = page.locator('header nav'); + await expect(nav.getByRole('link', { name: 'Migration Wizard' })).toBeVisible(); + await expect(nav.getByRole('link', { name: 'Projects', exact: true })).toBeVisible(); }); test('shows stats cards with labels', async ({ page }) => { await page.goto('/'); - await expect(page.locator('text=Total Sessions')).toBeVisible(); - await expect(page.locator('text=Total Projects')).toBeVisible(); - await expect(page.locator('text=Sync Status')).toBeVisible(); + await expect(page.getByText('Total Sessions', { exact: true })).toBeVisible(); + await expect(page.getByText('Total Projects', { exact: true })).toBeVisible(); + await expect(page.getByText('Sync Status', { exact: true })).toBeVisible(); }); test('quick action cards are clickable', async ({ page }) => { await page.goto('/'); - await page.locator('text=Browse Projects').click(); + await page.getByText('Browse Projects', { exact: true }).click(); await expect(page).toHaveURL(/\/projects/); }); }); diff --git a/web/package-lock.json b/web/package-lock.json index 06f2fdb..88d38a2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,11 +17,12 @@ "devDependencies": { "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.2.2", - "axios": "^1.11.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.2.2", - "vite": "^7.0.7" + "typescript": "^5.7.2", + "vite": "^7.0.7", + "vue-tsc": "^2.2.0" } }, "node_modules/@babel/helper-string-parser": { @@ -1340,6 +1341,35 @@ "vue": "^3.2.25" } }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", @@ -1390,6 +1420,42 @@ "@vue/shared": "3.5.30" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@vue/reactivity": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", @@ -1528,6 +1594,13 @@ } } }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1583,6 +1656,23 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1741,6 +1831,13 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -2112,6 +2209,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2466,6 +2573,29 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2496,6 +2626,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2915,6 +3052,20 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3013,6 +3164,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", @@ -3034,6 +3192,23 @@ } } }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/web/package.json b/web/package.json index 640b281..20375c0 100644 --- a/web/package.json +++ b/web/package.json @@ -4,16 +4,19 @@ "type": "module", "scripts": { "build": "vite build", - "dev": "vite" + "dev": "vite", + "type-check": "vue-tsc --noEmit", + "test:e2e": "playwright test" }, "devDependencies": { "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.2.2", - "axios": "^1.11.0", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.2.2", - "vite": "^7.0.7" + "typescript": "^5.7.2", + "vite": "^7.0.7", + "vue-tsc": "^2.2.0" }, "dependencies": { "@inertiajs/vue3": "^2.3.18", diff --git a/web/resources/css/app.css b/web/resources/css/app.css index 3e6abea..da67bbd 100644 --- a/web/resources/css/app.css +++ b/web/resources/css/app.css @@ -4,6 +4,8 @@ @source '../../storage/framework/views/*.php'; @source '../**/*.blade.php'; @source '../**/*.js'; +@source '../**/*.ts'; +@source '../**/*.vue'; @theme { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', diff --git a/web/resources/js/Components/StatCard.vue b/web/resources/js/Components/StatCard.vue new file mode 100644 index 0000000..22ecfcb --- /dev/null +++ b/web/resources/js/Components/StatCard.vue @@ -0,0 +1,14 @@ + + + diff --git a/web/resources/js/Pages/Dashboard.vue b/web/resources/js/Pages/Dashboard.vue index 816dfd4..f48ee2b 100644 --- a/web/resources/js/Pages/Dashboard.vue +++ b/web/resources/js/Pages/Dashboard.vue @@ -62,17 +62,9 @@ diff --git a/web/resources/js/Pages/Migration/Wizard.vue b/web/resources/js/Pages/Migration/Wizard.vue index 8da4e06..2f6d0eb 100644 --- a/web/resources/js/Pages/Migration/Wizard.vue +++ b/web/resources/js/Pages/Migration/Wizard.vue @@ -63,7 +63,7 @@

Which AI tool are you migrating sessions into?

+
@@ -166,7 +176,7 @@ class="rounded-lg bg-green-600 px-6 py-2 text-sm font-medium text-white disabled:opacity-40 hover:bg-green-700 transition-colors" data-testid="confirm-button" > - {{ isSubmitting ? 'Migrating...' : '✓ Start Migration' }} + {{ isSubmitting ? 'Preparing...' : '✓ Get Migration Command' }}
@@ -175,21 +185,26 @@ diff --git a/web/resources/js/app.js b/web/resources/js/app.js deleted file mode 100644 index e59d6a0..0000000 --- a/web/resources/js/app.js +++ /dev/null @@ -1 +0,0 @@ -import './bootstrap'; diff --git a/web/resources/js/app.ts b/web/resources/js/app.ts index d8e637d..f591344 100644 --- a/web/resources/js/app.ts +++ b/web/resources/js/app.ts @@ -1,12 +1,13 @@ import '../css/app.css'; -import { createApp, h, DefineComponent } from 'vue'; +import { createApp, h } from 'vue'; +import type { DefineComponent } from 'vue'; import { createInertiaApp } from '@inertiajs/vue3'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; const appName = 'MoveZ'; createInertiaApp({ - title: (title) => `${title} — ${appName}`, + title: (title) => (title ? `${title} — ${appName}` : appName), resolve: (name) => resolvePageComponent( `./Pages/${name}.vue`, diff --git a/web/resources/js/bootstrap.js b/web/resources/js/bootstrap.js deleted file mode 100644 index 5f1390b..0000000 --- a/web/resources/js/bootstrap.js +++ /dev/null @@ -1,4 +0,0 @@ -import axios from 'axios'; -window.axios = axios; - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/web/resources/js/types/index.d.ts b/web/resources/js/types/index.d.ts index 7f804ba..932fdca 100644 --- a/web/resources/js/types/index.d.ts +++ b/web/resources/js/types/index.d.ts @@ -18,6 +18,8 @@ export interface TurnDTO { export interface SessionDTO { id: string; title: string; + project?: string; + turn_count?: number; source_tool: string; source_machine_id: string; created_at: string; @@ -27,12 +29,14 @@ export interface SessionDTO { export interface AiSession { id: number; - project_id: number; + project_id: number | null; source_tool: string; session_id: string; title: string; exported_at: string | null; created_at: string; + updated_at: string; + project?: Project | null; } export interface Project { diff --git a/web/resources/views/welcome.blade.php b/web/resources/views/welcome.blade.php deleted file mode 100644 index b7355d7..0000000 --- a/web/resources/views/welcome.blade.php +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - {{ config('app.name', 'Laravel') }} - - - - - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

- - -
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- Light Mode 12 SVG --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- Dark Mode 12 SVG --}} - -
-
-
-
- - @if (Route::has('login')) - - @endif - - diff --git a/web/routes/api.php b/web/routes/api.php index 5eb131e..4d28a1d 100644 --- a/web/routes/api.php +++ b/web/routes/api.php @@ -1,5 +1,7 @@ comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); +Artisan::command('movez:token {email : Email of an existing user}', function (string $email): int { + $user = User::where('email', $email)->first(); + + if ($user === null) { + $this->error("No user with email {$email}"); + return 1; + } + + $token = $user->issueApiToken(); + + $this->info("New API token for {$email} (shown once — the server only stores its hash):"); + $this->line($token); + $this->line(''); + $this->line('Use it with: MOVEZ_TOKEN= movez sync:push --server=' . config('app.url')); + + return 0; +})->purpose('Issue a new sync API token for a user (replaces any existing token)'); diff --git a/web/routes/web.php b/web/routes/web.php index 167d8c4..907f5a6 100644 --- a/web/routes/web.php +++ b/web/routes/web.php @@ -1,5 +1,7 @@ create(); + SyncEvent::factory()->create(); + + $this->withoutVite()->get('/') + ->assertStatus(200) + ->assertInertia(fn (Assert $page) => $page + ->component('Dashboard') + ->where('stats.total_sessions', 3) + ->where('stats.total_projects', 3) + ->whereNot('stats.last_sync', null) + ); +}); + +it('session page decodes session data without leaking the raw payload', function (): void { + $session = AiSession::factory()->create(); + + $this->withoutVite()->get("/sessions/{$session->id}") + ->assertStatus(200) + ->assertInertia(fn (Assert $page) => $page + ->component('Sessions/Show') + ->has('session.project') + ->missing('session.session_data') + ->has('sessionData.turns') + ); +}); + +it('session page tolerates malformed session data', function (): void { + $session = AiSession::factory()->create(['session_data' => '"just a string"']); + + $this->withoutVite()->get("/sessions/{$session->id}") + ->assertStatus(200) + ->assertInertia(fn (Assert $page) => $page->where('sessionData', [])); +}); + +it('projects index lists session counts without loading sessions', function (): void { + $project = Project::factory()->create(); + AiSession::factory(2)->create(['project_id' => $project->id]); + + $this->withoutVite()->get('/projects') + ->assertInertia(fn (Assert $page) => $page + ->component('Projects/Index') + ->where('projects.data.0.ai_sessions_count', 2) + ->missing('projects.data.0.ai_sessions') + ); +}); diff --git a/web/tests/Feature/MigrationControllerTest.php b/web/tests/Feature/MigrationControllerTest.php index 3e7c36e..4a69393 100644 --- a/web/tests/Feature/MigrationControllerTest.php +++ b/web/tests/Feature/MigrationControllerTest.php @@ -1,6 +1,7 @@ assertInertia(fn (Assert $page) => $page ->component('Migration/Wizard') ->has('supportedTools') + ->has('writableTools') ->has('projects') ); }); it('migration start requires from_tool and to_tool', function (): void { - $response = $this->postJson('/migration/start', []); - $response->assertStatus(422); + $this->postJson('/migration/start', [])->assertStatus(422); }); -it('migration start with valid data returns queued status', function (): void { - $response = $this->postJson('/migration/start', [ - 'from_tool' => 'cursor', - 'to_tool' => 'claude-code', - ]); +it('migration start rejects tools the CLI cannot write to', function (): void { + $this->postJson('/migration/start', ['from_tool' => 'cursor', 'to_tool' => 'cline']) + ->assertStatus(422) + ->assertJsonValidationErrors('to_tool'); +}); + +it('migration start returns the CLI command to run', function (): void { + $project = Project::factory()->create(['path' => '/home/dev/my app']); - $response->assertStatus(200)->assertJson(['status' => 'queued']); + $this->postJson('/migration/start', [ + 'from_tool' => 'cursor', + 'to_tool' => 'claude-code', + 'project_id' => $project->id, + 'from_path' => '/old/home', + 'to_path' => '/home/dev', + ]) + ->assertStatus(200) + ->assertJson([ + 'status' => 'ready', + 'command' => 'movez transfer --from=cursor --to=claude-code --project="/home/dev/my app" --from-path=/old/home --to-path=/home/dev', + ]); }); diff --git a/web/tests/Feature/SyncServerTest.php b/web/tests/Feature/SyncServerTest.php index b0a2d51..03bd384 100644 --- a/web/tests/Feature/SyncServerTest.php +++ b/web/tests/Feature/SyncServerTest.php @@ -1,41 +1,93 @@ create(); + $user->forceFill(['api_token' => hash('sha256', $token)])->save(); + + return $user; +} + it('sync push returns 401 without a token', function (): void { - $response = $this->postJson('/api/sync/push', ['data' => 'test']); - $response->assertStatus(401); + $this->postJson('/api/sync/push', ['sessions' => 'test'])->assertStatus(401); }); it('sync pull returns 401 without a token', function (): void { - $response = $this->getJson('/api/sync/pull'); - $response->assertStatus(401); + $this->getJson('/api/sync/pull')->assertStatus(401); +}); + +it('sync rejects an unknown token', function (): void { + syncUser('real-token'); + + $this->withToken('wrong-token')->getJson('/api/sync/pull')->assertStatus(401); +}); + +it('sync push requires a sessions payload', function (): void { + syncUser('tok'); + + $this->withToken('tok')->postJson('/api/sync/push', ['data' => 'old-key']) + ->assertStatus(422) + ->assertJsonValidationErrors('sessions'); +}); + +it('sync push stores the encrypted blob and records the event', function (): void { + $user = syncUser('test-token-abc'); + + $this->withToken('test-token-abc') + ->postJson('/api/sync/push', ['sessions' => 'encrypted-blob-xyz', 'count' => 3]) + ->assertStatus(200) + ->assertJson(['status' => 'ok', 'count' => 3]); + + $this->assertDatabaseHas('sync_blobs', ['user_id' => $user->id, 'payload' => 'encrypted-blob-xyz', 'session_count' => 3]); + $this->assertDatabaseHas('sync_events', ['user_id' => $user->id, 'event_type' => 'push', 'session_count' => 3]); + + // Sync payloads are not browsable sessions + expect(AiSession::count())->toBe(0); }); -it('sync push stores encrypted data with valid token', function (): void { - $user = User::factory()->create([ - 'api_token' => hash('sha256', 'test-token-abc'), - ]); +it('sync pull returns the latest blob for the same user', function (): void { + syncUser('test-pull-token'); + + $this->withToken('test-pull-token')->postJson('/api/sync/push', ['sessions' => 'first', 'count' => 1]); + $this->withToken('test-pull-token')->postJson('/api/sync/push', ['sessions' => 'second', 'count' => 2]); - $response = $this->withHeaders(['Authorization' => 'Bearer test-token-abc']) - ->postJson('/api/sync/push', ['data' => 'encrypted-blob-xyz']); + $this->withToken('test-pull-token')->getJson('/api/sync/pull') + ->assertStatus(200) + ->assertExactJson(['sessions' => 'second', 'count' => 2]); +}); + +it('sync pull returns null sessions before anything was pushed', function (): void { + syncUser('fresh'); - $response->assertStatus(200)->assertJson(['status' => 'ok']); + $this->withToken('fresh')->getJson('/api/sync/pull') + ->assertStatus(200) + ->assertExactJson(['sessions' => null, 'count' => 0]); }); -it('sync pull returns data with valid token', function (): void { - $user = User::factory()->create([ - 'api_token' => hash('sha256', 'test-pull-token'), - ]); +it('users cannot pull each other\'s data', function (): void { + syncUser('alice'); + syncUser('bob'); + + $this->withToken('alice')->postJson('/api/sync/push', ['sessions' => 'alice-secret']); + + $this->withToken('bob')->getJson('/api/sync/pull') + ->assertExactJson(['sessions' => null, 'count' => 0]); +}); + +it('api token is never serialized', function (): void { + $user = syncUser('hidden'); + + expect($user->toArray())->not->toHaveKey('api_token'); +}); - // Push first - $this->withHeaders(['Authorization' => 'Bearer test-pull-token']) - ->postJson('/api/sync/push', ['data' => 'my-encrypted-data']); +it('movez:token issues a working token', function (): void { + $user = User::factory()->create(['email' => 'dev@example.com']); - // Then pull - $response = $this->withHeaders(['Authorization' => 'Bearer test-pull-token']) - ->getJson('/api/sync/pull'); + $this->artisan('movez:token', ['email' => 'dev@example.com'])->assertExitCode(0); - $response->assertStatus(200)->assertJson(['data' => 'my-encrypted-data']); + expect($user->fresh()->api_token)->toHaveLength(64); }); diff --git a/web/tests/TestCase.php b/web/tests/TestCase.php index fe1ffc2..6ac0072 100644 --- a/web/tests/TestCase.php +++ b/web/tests/TestCase.php @@ -1,5 +1,7 @@ assertTrue(true); - } -} +it('runs the unit suite', function (): void { + expect(true)->toBeTrue(); +}); diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..e94166e --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["resources/js/*"] + } + }, + "include": ["resources/js/**/*.ts", "resources/js/**/*.d.ts", "resources/js/**/*.vue"] +} From 17b01a9c1efb10211845cdf7e860f97d889ac07b Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:40:45 -0400 Subject: [PATCH 4/8] Make the VS Code extension work with the real CLI - Shared CLI runner: runs movez.phar through PHP (new movez.phpPath), reads settings on every call, runs in the workspace folder, surfaces CLI errors in notifications and a MoveZ output channel instead of hiding them - Sessions view shows sessions for the open project with real turn counts - Export via save dialog to .cbz (optionally encrypted); import accepts .cbz/.json; target lists only contain tools the CLI can write to - Sync token stored in SecretStorage ("MoveZ: Set Sync Token") and passed as MOVEZ_TOKEN instead of a command-line argument - Migration wizard runs `movez transfer` locally behind a strict CSP instead of iframing a server that cannot reach local sessions - Add icon, README, LICENSE, repository/license metadata and .vscodeignore so `vsce package` runs non-interactively in CI; `npm test` type-checks Co-Authored-By: Claude Opus 5.5 --- vscode-extension/.vscodeignore | 7 + vscode-extension/LICENSE | 21 ++ vscode-extension/README.md | 43 +++ vscode-extension/package-lock.json | 386 +------------------- vscode-extension/package.json | 38 +- vscode-extension/resources/icon.png | Bin 0 -> 1807 bytes vscode-extension/resources/icon.svg | 7 + vscode-extension/src/MigrationPanel.ts | 216 ++++++----- vscode-extension/src/SessionTreeProvider.ts | 72 ++-- vscode-extension/src/cli.ts | 113 ++++++ vscode-extension/src/extension.ts | 205 +++++++---- vscode-extension/src/types.ts | 3 + 12 files changed, 515 insertions(+), 596 deletions(-) create mode 100644 vscode-extension/.vscodeignore create mode 100644 vscode-extension/LICENSE create mode 100644 vscode-extension/README.md create mode 100644 vscode-extension/resources/icon.png create mode 100644 vscode-extension/resources/icon.svg create mode 100644 vscode-extension/src/cli.ts diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..8705965 --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,7 @@ +src/** +node_modules/** +**/*.map +**/*.ts +tsconfig.json +.vscode-test/** +*.vsix diff --git a/vscode-extension/LICENSE b/vscode-extension/LICENSE new file mode 100644 index 0000000..827508b --- /dev/null +++ b/vscode-extension/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MoveZ Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..24d7c02 --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,43 @@ +# MoveZ for VS Code & Cursor + +Browse, export, import and sync your AI coding sessions (Cursor, Claude Code, Codex, Copilot CLI, Cline, Continue) without leaving the editor. + +This extension is a front end for the [MoveZ CLI](https://github.com/kv4u/MoveZ). Install the CLI first. + +## Requirements + +- The `movez` CLI on your `PATH`, **or** `movez.phar` plus PHP 8.2+ (with `pdo_sqlite`, `openssl`, `zip`, `mbstring`). + - Download `movez.phar` from [Releases](https://github.com/kv4u/MoveZ/releases/latest). + - The Windows desktop app ships its own PHP: point `movez.phpPath` at `…\MoveZ\resources\php\php.exe` and `movez.cliPath` at `…\MoveZ\resources\movez.phar`. + +## Features + +- **Sessions view** (MoveZ icon in the activity bar): sessions belonging to the open folder, newest first. Use the refresh button after working in another tool. +- **MoveZ: Export Sessions**: save sessions for this project as a portable `.cbz` bundle, optionally encrypted with your MoveZ key. +- **MoveZ: Import Sessions**: load a `.cbz` or `.json` bundle into Cursor, Claude Code, Codex or Copilot CLI. +- **MoveZ: Migration Wizard**: copy sessions from one tool into another on this machine, with optional path remapping. +- **MoveZ: Sync Push / Sync Pull**: move encrypted sessions between machines through your self-hosted MoveZ server. +- **MoveZ: Set Sync Token**: stores the sync API token in your OS keychain (VS Code SecretStorage). + +CLI output and errors are logged to the **MoveZ** output channel. + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `movez.cliPath` | `movez` | Path to the `movez` executable or `movez.phar` | +| `movez.phpPath` | `php` | PHP used to run a `.phar` | +| `movez.serverUrl` | | Base URL of your MoveZ sync server | +| `movez.token` | | Deprecated: use **MoveZ: Set Sync Token** instead | + +## Sync between machines + +Bundles and sync payloads are encrypted with the key at `~/.movez/key`. Copy that file to every machine you want to pull on, or decryption will fail. + +## Install from VSIX + +Download `movez-vscode.vsix` from [Releases](https://github.com/kv4u/MoveZ/releases/latest), then run **Extensions: Install from VSIX…**. + +## License + +MIT diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 08d6bfd..cfb3692 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -7,10 +7,10 @@ "": { "name": "movez", "version": "0.1.0", + "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", "@types/vscode": "^1.85.0", - "@vscode/test-electron": "^2.3.8", "@vscode/vsce": "^2.22.0", "typescript": "^5.3.0" }, @@ -219,23 +219,6 @@ "node": ">=20.0.0" } }, - "node_modules/@vscode/test-electron": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", - "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "jszip": "^3.10.1", - "ora": "^8.1.0", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@vscode/vsce": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", @@ -433,19 +416,6 @@ "node": ">= 14" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -717,35 +687,6 @@ "license": "ISC", "optional": true }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -803,13 +744,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1034,13 +968,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -1201,19 +1128,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1457,13 +1371,6 @@ "license": "BSD-3-Clause", "optional": true }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1526,32 +1433,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -1568,13 +1449,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -1605,19 +1479,6 @@ "npm": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -1664,16 +1525,6 @@ "node": ">=6" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/linkify-it": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", @@ -1733,49 +1584,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1869,19 +1677,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -2008,22 +1803,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -2043,50 +1822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", @@ -2206,13 +1941,6 @@ "node": ">=10" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -2271,46 +1999,6 @@ "node": ">=0.8" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -2375,13 +2063,6 @@ "node": ">=10" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2458,19 +2139,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2520,25 +2188,13 @@ "simple-concat": "^1.0.0" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -2548,41 +2204,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "optional": true }, "node_modules/strip-json-comments": { "version": "2.0.1", @@ -2766,7 +2389,8 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/uuid": { "version": "8.3.2", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index b7b853d..e005be3 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -4,11 +4,26 @@ "description": "Transfer and migrate AI coding sessions between tools and machines", "version": "0.1.0", "publisher": "movez", + "license": "MIT", + "icon": "resources/icon.png", + "repository": { + "type": "git", + "url": "https://github.com/kv4u/MoveZ.git", + "directory": "vscode-extension" + }, + "bugs": { + "url": "https://github.com/kv4u/MoveZ/issues" + }, + "homepage": "https://github.com/kv4u/MoveZ#readme", "engines": { "vscode": "^1.85.0" }, - "categories": ["Other"], - "activationEvents": ["onStartupFinished"], + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], "main": "./out/extension.js", "contributes": { "viewsContainers": { @@ -55,6 +70,11 @@ "title": "Sync Pull", "category": "MoveZ" }, + { + "command": "movez.setToken", + "title": "Set Sync Token", + "category": "MoveZ" + }, { "command": "movez.refreshSessions", "title": "Refresh Sessions", @@ -77,7 +97,12 @@ "movez.cliPath": { "type": "string", "default": "movez", - "description": "Path to the movez CLI executable or PHAR file" + "description": "Path to the movez CLI executable, or to movez.phar (run through movez.phpPath)" + }, + "movez.phpPath": { + "type": "string", + "default": "php", + "description": "PHP executable used when movez.cliPath points to a .phar file" }, "movez.serverUrl": { "type": "string", @@ -87,7 +112,8 @@ "movez.token": { "type": "string", "default": "", - "description": "API token for the sync server" + "deprecationMessage": "Use the \"MoveZ: Set Sync Token\" command instead — it stores the token in your OS keychain.", + "description": "Deprecated: plain-text sync API token. Prefer \"MoveZ: Set Sync Token\"." } } } @@ -95,15 +121,13 @@ "scripts": { "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "pretest": "npm run compile", - "test": "node ./out/__tests__/runTests.js", + "test": "tsc --noEmit -p ./", "vscode:prepublish": "npm run compile", "package": "vsce package --no-dependencies" }, "devDependencies": { "@types/node": "^20.0.0", "@types/vscode": "^1.85.0", - "@vscode/test-electron": "^2.3.8", "@vscode/vsce": "^2.22.0", "typescript": "^5.3.0" } diff --git a/vscode-extension/resources/icon.png b/vscode-extension/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5d9b7e22dc3d51f1c3f04ed20f60c170efb909b4 GIT binary patch literal 1807 zcmd^=`y&&I0>_2T%vtkx;+RdDICQ#4MA*>g{b~|pRFcyqWH+yoZP%$r;V_i9LPhh6 zX|c_eM-$1}0 ze1*FBRz?Q+(FJ3FHoj=3U?7U%r$OP7CJPUB|CJsfKMl1rmbb>oA8=4VjPVgn<@HxU zsK;L32>>X5)3S8D0|?25XPGoqz#VB-9#23@$35gs+KAOAVg+x6{$q{T{e9$Z-G7&) zoVcMI?oh}_3CBh*@#2IT191bk+crwAm306)ZRO^CD5}aw0o9A)b`lvdT4a(fWFyu# zvfi43%&j1h@KcSFC(91L-(sTD{LbagGW*?xnoWe@BT-$8XhdsL+~;4Gdt7Xd4WM%> zUUCZQ2ld#=44ct81@LOxS&BMSIjanoola|snXl6~f##NS_LbNZ!EI%9EO9k}a`SvT zA0Yr*?U^QH82{+ogyi%Li;rlVe;EB5anOHa+({|+&!H#&$Y5zrx_6rCfGUn=6YBeg zP-{iL2CsSkd=!sYN&lE)Xs5GB4Rw>4>E85F?+auUf_;1R)&Du^80~dMtCWcyOZBmT z;5yhBUYoGke7?7H(|vGpy~Gr;a$EUF zQwy~=w&=~TyP54L{*)z{Tv+oc9kep5(8gicwhfj76CVotwomF{bc}+Zh3!o^&i(Av znw&OFIw#Ny;#!h$dW{8jkgbgzarFB+oKJHQ$62!b^VR8GT^y|8Z%*c;H()zzU6riw z_?3SFcQvU3!L<@;M01zq>zAnCpGNCvvQ8!O_Z_t>J%e6)%s<%3MC8aSblcWUO@uHn zP3<$>Jhg(V3T~ZB+(p$JvbIKor#@Fi!;MoOgE)+)pdG4{x`~|oT{4U>at2LKj6kR5 zr_NGVUl^*~&D?mG@*M!9cXQq;9w<_iwKSBE_5@88DYsi8C?p3NVXDmkO(;JxQ-V2; z!Wkb{ibu{Byv0!*kxHp{4p7;9t-DeN=N9buv`vEZfD06wnHE|PYuogpBg@f&(Aa!=g(T?1W zd}YSkIxPly96!h&h{-J zPp3}AKh{lxUm>rybVRL)(K{nLTA}^1a#>+Z%TX3hM&+|kkIawdscoEnin@PDex?Jq zPRHVVs((qjlou%BYHB9!N?6GHUU6Por~8|o#_oo2XBKJZ{=D8^CJ}7_R*8YFi>}JC zuQXhSvpQQMmiFn5W%Sbv!@O^siLcWye+KgCANVC5o+RHhP5kq^_u~6#QHS=orgW^) zK%B-qxQQ!Cq$4(09Vm23z&skNqHk8H?(xc5i`b*B3hk_Q!gq#-Xv}XPX}vAQ3TV9g zvaiF!S%=#ArZdMX4?bP+hUXdgxNiK^mBGy^Hb4}^40;zAG}H4sO+^xGc^*e7>1rut zUc4}=XZ6&{SCr8%REE`0>t~`;p&HBD + + + + + + diff --git a/vscode-extension/src/MigrationPanel.ts b/vscode-extension/src/MigrationPanel.ts index ee07e5b..14772c1 100644 --- a/vscode-extension/src/MigrationPanel.ts +++ b/vscode-extension/src/MigrationPanel.ts @@ -1,5 +1,20 @@ import * as vscode from 'vscode'; +import { randomBytes } from 'crypto'; +import { READABLE_TOOLS, WRITABLE_TOOLS, runCli, workspaceFolder } from './cli'; + +interface TransferRequest { + type: 'transfer'; + from: string; + to: string; + project: string; + fromPath: string; + toPath: string; +} +/** + * In-editor migration wizard. Runs `movez transfer` locally — session files + * live on this machine, so no server round-trip is involved. + */ export class MigrationPanel { public static currentPanel: MigrationPanel | undefined; @@ -8,20 +23,20 @@ export class MigrationPanel { private readonly _panel: vscode.WebviewPanel; private _disposables: vscode.Disposable[] = []; - private constructor( - panel: vscode.WebviewPanel, - private readonly serverUrl: string, - ) { + private constructor(panel: vscode.WebviewPanel, private readonly onTransferred: () => void) { this._panel = panel; this._panel.webview.html = this._getHtmlContent(); this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.onDidReceiveMessage( + (msg: TransferRequest) => this.handleMessage(msg), + null, + this._disposables, + ); } - public static createOrShow(extensionUri: vscode.Uri, serverUrl: string): void { - const column = vscode.window.activeTextEditor - ? vscode.window.activeTextEditor.viewColumn - : undefined; + public static createOrShow(onTransferred: () => void): void { + const column = vscode.window.activeTextEditor?.viewColumn; if (MigrationPanel.currentPanel) { MigrationPanel.currentPanel._panel.reveal(column); @@ -32,13 +47,10 @@ export class MigrationPanel { MigrationPanel.viewType, 'MoveZ Migration Wizard', column ?? vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - }, + { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [] }, ); - MigrationPanel.currentPanel = new MigrationPanel(panel, serverUrl); + MigrationPanel.currentPanel = new MigrationPanel(panel, onTransferred); } public dispose(): void { @@ -47,117 +59,123 @@ export class MigrationPanel { this._panel.dispose(); while (this._disposables.length) { - const x = this._disposables.pop(); - x?.dispose(); + this._disposables.pop()?.dispose(); } } - private _getHtmlContent(): string { - const serverUrl = this.serverUrl; + private async handleMessage(msg: TransferRequest): Promise { + if (msg?.type !== 'transfer' || !READABLE_TOOLS.includes(msg.from) || !WRITABLE_TOOLS.includes(msg.to)) { + return; + } - if (serverUrl) { - // If a server URL is configured, show an iframe pointing to the dashboard - return ` - - - - - MoveZ Migration Wizard - - - - - -`; + const args = ['transfer', `--from=${msg.from}`, `--to=${msg.to}`]; + if (msg.project) { + args.push(`--project=${msg.project}`); } + if (msg.fromPath && msg.toPath) { + args.push(`--from-path=${msg.fromPath}`, `--to-path=${msg.toPath}`); + } + + try { + const { stdout } = await runCli(args, { timeoutMs: 300_000 }); + await this._panel.webview.postMessage({ type: 'done', ok: true, message: stdout.trim() || 'Transfer complete.' }); + this.onTransferred(); + } catch (err) { + await this._panel.webview.postMessage({ + type: 'done', + ok: false, + message: err instanceof Error ? err.message : String(err), + }); + } + } + + private _getHtmlContent(): string { + const nonce = randomBytes(16).toString('base64'); + const csp = `default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}';`; + const options = (tools: string[]) => tools.map((t) => ``).join(''); + const project = escapeHtml(workspaceFolder() ?? ''); - // Fallback: embedded wizard steps return ` + MoveZ Migration Wizard -

⚡ MoveZ Migration Wizard

-
Configure a sync server URL in settings for the full web wizard experience.
- -
-

Step 1: Source Tool

- -
-
+

Copies sessions from one AI tool into another on this machine.

-
-

Step 2: Target Tool

- -
- -
+ + -
-

Step 3: Confirm

-

Transfer sessions from to .

- - -
+ + + + + -
-

✅ Done!

-

- +
+
+
- `; } } + +function escapeHtml(value: string): string { + return value.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); +} diff --git a/vscode-extension/src/SessionTreeProvider.ts b/vscode-extension/src/SessionTreeProvider.ts index 547c9f6..cd93876 100644 --- a/vscode-extension/src/SessionTreeProvider.ts +++ b/vscode-extension/src/SessionTreeProvider.ts @@ -1,69 +1,69 @@ import * as vscode from 'vscode'; -import { execFile } from 'child_process'; -import { promisify } from 'util'; +import { runCli, workspaceFolder } from './cli'; import type { SessionDTO } from './types'; -const execFileAsync = promisify(execFile); - export class SessionTreeItem extends vscode.TreeItem { - constructor( - public readonly session: SessionDTO, - collapsibleState: vscode.TreeItemCollapsibleState, - ) { - super(session.title || session.id, collapsibleState); + constructor(public readonly session: SessionDTO) { + super(session.title || session.id, vscode.TreeItemCollapsibleState.None); + + const turns = session.turn_count ?? session.turns?.length ?? 0; - this.description = `${session.source_tool} · ${session.turns.length} turn(s)`; + this.description = `${session.source_tool} · ${turns} turn(s)`; this.tooltip = new vscode.MarkdownString( `**${session.title}**\n\n` + `Tool: ${session.source_tool}\n\n` + + (session.project ? `Project: ${session.project}\n\n` : '') + `Last active: ${new Date(session.last_active_at).toLocaleString()}\n\n` + - `Turns: ${session.turns.length}`, + `Turns: ${turns}`, ); this.contextValue = 'movezSession'; this.iconPath = new vscode.ThemeIcon('comment-discussion'); } } -export class SessionTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = - new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = - this._onDidChangeTreeData.event; - - private sessions: SessionDTO[] = []; +class MessageItem extends vscode.TreeItem { + constructor(message: string, icon: string) { + super(message, vscode.TreeItemCollapsibleState.None); + this.iconPath = new vscode.ThemeIcon(icon); + } +} - constructor(private readonly cliPath: string) {} +export class SessionTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; refresh(): void { this._onDidChangeTreeData.fire(); } - getTreeItem(element: SessionTreeItem): vscode.TreeItem { + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { return element; } - async getChildren(_element?: SessionTreeItem): Promise { - if (_element) { + async getChildren(element?: vscode.TreeItem): Promise { + if (element) { return []; } - await this.loadSessions(); + // Show sessions for the open project; all sessions when no folder is open + const project = workspaceFolder(); + const args = ['list-sessions', '--json', ...(project ? [`--project=${project}`] : [])]; - return this.sessions.map( - (s) => new SessionTreeItem(s, vscode.TreeItemCollapsibleState.None), - ); - } - - private async loadSessions(): Promise { try { - const { stdout } = await execFileAsync(this.cliPath, ['list-sessions', '--json'], { - timeout: 10_000, - }); + const { stdout } = await runCli(args, { timeoutMs: 30_000 }); + const data = JSON.parse(stdout.trim() || '[]'); + const sessions = Array.isArray(data) ? (data as SessionDTO[]) : []; + + if (sessions.length === 0) { + return [new MessageItem(project ? 'No sessions for this project' : 'No sessions found', 'info')]; + } - const data = JSON.parse(stdout.trim()); - this.sessions = Array.isArray(data) ? data : []; - } catch { - this.sessions = []; + return sessions + .sort((a, b) => b.last_active_at.localeCompare(a.last_active_at)) + .map((s) => new SessionTreeItem(s)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return [new MessageItem(`MoveZ CLI error: ${message.split('\n')[0]}`, 'warning')]; } } } diff --git a/vscode-extension/src/cli.ts b/vscode-extension/src/cli.ts new file mode 100644 index 0000000..a448d52 --- /dev/null +++ b/vscode-extension/src/cli.ts @@ -0,0 +1,113 @@ +import * as vscode from 'vscode'; +import { execFile } from 'child_process'; + +export interface CliResult { + stdout: string; + stderr: string; +} + +export interface CliOptions { + timeoutMs?: number; + /** Extra environment variables, e.g. MOVEZ_TOKEN (never pass secrets as arguments). */ + env?: Record; +} + +let output: vscode.OutputChannel | undefined; + +export function outputChannel(): vscode.OutputChannel { + output ??= vscode.window.createOutputChannel('MoveZ'); + return output; +} + +/** First workspace folder, or undefined when no folder is open. */ +export function workspaceFolder(): string | undefined { + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; +} + +/** + * Resolve how to launch the CLI. Settings are read on every call so changes + * apply without reloading the window. A .phar path is run through PHP, which + * is required on Windows where .phar files are not directly executable. + */ +function resolveCommand(args: string[]): { cmd: string; cmdArgs: string[] } { + const config = vscode.workspace.getConfiguration('movez'); + const cliPath = config.get('cliPath', 'movez') || 'movez'; + const phpPath = config.get('phpPath', 'php') || 'php'; + + if (cliPath.toLowerCase().endsWith('.phar')) { + return { cmd: phpPath, cmdArgs: ['-d', 'memory_limit=512M', cliPath, ...args] }; + } + + return { cmd: cliPath, cmdArgs: args }; +} + +/** + * Run the movez CLI. Rejects with the CLI's own error output so callers can + * show a meaningful message; everything is also logged to the MoveZ output channel. + */ +export function runCli(args: string[], options: CliOptions = {}): Promise { + const { cmd, cmdArgs } = resolveCommand(args); + const log = outputChannel(); + log.appendLine(`> ${cmd} ${cmdArgs.join(' ')}`); + + return new Promise((resolve, reject) => { + execFile( + cmd, + cmdArgs, + { + cwd: workspaceFolder(), + timeout: options.timeoutMs ?? 60_000, + maxBuffer: 64 * 1024 * 1024, + windowsHide: true, + env: { ...process.env, ...options.env }, + }, + (error, stdout, stderr) => { + if (stderr.trim()) { + log.appendLine(stderr.trim()); + } + + if (error) { + const detail = (stderr || stdout).trim() || error.message; + log.appendLine(`✘ ${detail}`); + reject(new Error( + (error as NodeJS.ErrnoException).code === 'ENOENT' + ? `Cannot find the MoveZ CLI at "${cmd}". Set "movez.cliPath" (and "movez.phpPath" for a .phar).` + : detail, + )); + return; + } + + if (stdout.trim()) { + log.appendLine(stdout.trim()); + } + resolve({ stdout, stderr }); + }, + ); + }); +} + +const TOKEN_KEY = 'movez.syncToken'; + +/** Sync token from SecretStorage, falling back to the deprecated plain-text setting. */ +export async function getToken(secrets: vscode.SecretStorage): Promise { + const stored = await secrets.get(TOKEN_KEY); + if (stored) { + return stored; + } + + return vscode.workspace.getConfiguration('movez').get('token', ''); +} + +export async function setToken(secrets: vscode.SecretStorage, token: string): Promise { + if (token) { + await secrets.store(TOKEN_KEY, token); + } else { + await secrets.delete(TOKEN_KEY); + } +} + +/** Tools the CLI can write to (Windsurf is not supported yet). */ +export const WRITABLE_TOOLS = ['cursor', 'claude-code', 'codex', 'copilot-cli']; + +/** Tools the CLI can read from. */ +export const READABLE_TOOLS = ['cursor', 'claude-code', 'codex', 'copilot-cli', 'cline', 'continue']; diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 7614938..90cca26 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1,57 +1,83 @@ import * as vscode from 'vscode'; -import { execFile } from 'child_process'; -import { promisify } from 'util'; import * as path from 'path'; import { SessionTreeProvider } from './SessionTreeProvider'; import { MigrationPanel } from './MigrationPanel'; +import { READABLE_TOOLS, WRITABLE_TOOLS, getToken, outputChannel, runCli, setToken, workspaceFolder } from './cli'; -const execFileAsync = promisify(execFile); - -function getCli(): string { - const config = vscode.workspace.getConfiguration('movez'); - return config.get('cliPath', 'movez'); +function getServerUrl(): string { + return vscode.workspace.getConfiguration('movez').get('serverUrl', '').replace(/\/+$/, ''); } -function getServerUrl(): string { - const config = vscode.workspace.getConfiguration('movez'); - return config.get('serverUrl', ''); +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); } -function getToken(): string { - const config = vscode.workspace.getConfiguration('movez'); - return config.get('token', ''); +function showError(prefix: string, err: unknown): void { + void vscode.window + .showErrorMessage(`${prefix}: ${errorMessage(err)}`, 'Show Log') + .then((choice) => choice && outputChannel().show()); } export function activate(context: vscode.ExtensionContext): void { - const provider = new SessionTreeProvider(getCli()); + const provider = new SessionTreeProvider(); context.subscriptions.push( + outputChannel(), vscode.window.registerTreeDataProvider('movezSessions', provider), + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('movez.cliPath') || e.affectsConfiguration('movez.phpPath')) { + provider.refresh(); + } + }), ); // Refresh Sessions context.subscriptions.push( - vscode.commands.registerCommand('movez.refreshSessions', () => { - provider.refresh(); - }), + vscode.commands.registerCommand('movez.refreshSessions', () => provider.refresh()), ); // Export Sessions context.subscriptions.push( vscode.commands.registerCommand('movez.exportSession', async () => { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; - const outputPath = path.join(workspaceFolder, `movez-export-${Date.now()}.json`); + const tool = await vscode.window.showQuickPick(['auto', ...READABLE_TOOLS], { + placeHolder: 'Export sessions from which tool?', + }); + if (!tool) { + return; + } + + const project = workspaceFolder(); + const target = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(project ?? '', `movez-export-${Date.now()}.cbz`)), + filters: { 'MoveZ bundle': ['cbz'] }, + }); + if (!target) { + return; + } + + const encrypt = await vscode.window.showQuickPick(['No', 'Yes'], { + placeHolder: 'Encrypt the bundle with your MoveZ key (~/.movez/key)?', + }); + if (!encrypt) { + return; + } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Exporting sessions...' }, async () => { try { - await execFileAsync(getCli(), ['export', '--tool=auto', `--output=${outputPath}`], { - timeout: 30_000, - }); - vscode.window.showInformationMessage(`Sessions exported to: ${outputPath}`); + const args = ['export', `--tool=${tool}`, `--output=${target.fsPath}`]; + if (project) { + args.push(`--project=${project}`); + } + if (encrypt === 'Yes') { + args.push('--encrypt'); + } + + const { stdout } = await runCli(args, { timeoutMs: 300_000 }); + vscode.window.showInformationMessage(stdout.trim().split('\n')[0] || `Exported to ${target.fsPath}`); } catch (err) { - vscode.window.showErrorMessage(`Export failed: ${String(err)}`); + showError('Export failed', err); } }, ); @@ -63,37 +89,36 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('movez.importSession', async () => { const uri = await vscode.window.showOpenDialog({ canSelectMany: false, - filters: { 'Bundle files': ['json', 'enc.json', 'cbz'] }, + filters: { 'MoveZ bundles': ['cbz', 'json'] }, }); - if (!uri || uri.length === 0) { return; } - const tools = ['cursor', 'windsurf', 'claude-code', 'codex', 'copilot-cli']; - const targetTool = await vscode.window.showQuickPick(tools, { - placeHolder: 'Select target AI tool', + const targetTool = await vscode.window.showQuickPick(WRITABLE_TOOLS, { + placeHolder: 'Import into which AI tool?', }); - if (!targetTool) { return; } - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; + const project = workspaceFolder(); await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Importing sessions...' }, async () => { try { - await execFileAsync( - getCli(), - ['import', `--input=${uri[0].fsPath}`, `--tool=${targetTool}`, `--project=${workspaceFolder}`], - { timeout: 30_000 }, - ); + // Encrypted .cbz bundles and *.enc.json files are detected by the CLI + const args = ['import', `--input=${uri[0].fsPath}`, `--tool=${targetTool}`]; + if (project) { + args.push(`--project=${project}`); + } + + await runCli(args, { timeoutMs: 300_000 }); vscode.window.showInformationMessage(`Sessions imported into ${targetTool}`); provider.refresh(); } catch (err) { - vscode.window.showErrorMessage(`Import failed: ${String(err)}`); + showError('Import failed', err); } }, ); @@ -103,33 +128,77 @@ export function activate(context: vscode.ExtensionContext): void { // Migration Wizard context.subscriptions.push( vscode.commands.registerCommand('movez.openMigrationWizard', () => { - MigrationPanel.createOrShow(context.extensionUri, getServerUrl()); + MigrationPanel.createOrShow(() => provider.refresh()); + }), + ); + + // Store the sync token in VS Code's SecretStorage (not settings.json) + context.subscriptions.push( + vscode.commands.registerCommand('movez.setToken', async () => { + const token = await vscode.window.showInputBox({ + prompt: 'MoveZ sync API token (stored in your OS keychain via VS Code SecretStorage)', + password: true, + ignoreFocusOut: true, + }); + if (token === undefined) { + return; + } + + await setToken(context.secrets, token.trim()); + vscode.window.showInformationMessage(token.trim() ? 'MoveZ sync token saved.' : 'MoveZ sync token cleared.'); }), ); + async function syncCredentials(): Promise<{ server: string; token: string } | undefined> { + const server = getServerUrl(); + if (!server) { + vscode.window.showErrorMessage('Set "movez.serverUrl" in settings first.'); + return undefined; + } + + const token = await getToken(context.secrets); + if (!token) { + const choice = await vscode.window.showErrorMessage('No sync token configured.', 'Set Token'); + if (choice) { + await vscode.commands.executeCommand('movez.setToken'); + } + return undefined; + } + + return { server, token }; + } + // Sync Push context.subscriptions.push( vscode.commands.registerCommand('movez.syncPush', async () => { - const serverUrl = getServerUrl(); - const token = getToken(); + const creds = await syncCredentials(); + if (!creds) { + return; + } - if (!serverUrl) { - vscode.window.showErrorMessage('Set movez.serverUrl in settings first.'); + const tool = await vscode.window.showQuickPick(['auto', ...READABLE_TOOLS], { + placeHolder: 'Push sessions from which tool?', + }); + if (!tool) { return; } + const project = workspaceFolder(); + await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Syncing push...' }, + { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Pushing sessions...' }, async () => { try { - await execFileAsync( - getCli(), - ['sync:push', `--server=${serverUrl}`, `--token=${token}`, '--tool=auto'], - { timeout: 60_000 }, - ); - vscode.window.showInformationMessage('Sessions pushed to sync server'); + const args = ['sync:push', `--server=${creds.server}`, `--tool=${tool}`]; + if (project) { + args.push(`--project=${project}`); + } + + // Token goes through the environment so it never appears in process lists + const { stdout } = await runCli(args, { timeoutMs: 300_000, env: { MOVEZ_TOKEN: creds.token } }); + vscode.window.showInformationMessage(stdout.trim().split('\n')[0] || 'Sessions pushed'); } catch (err) { - vscode.window.showErrorMessage(`Sync push failed: ${String(err)}`); + showError('Sync push failed', err); } }, ); @@ -139,44 +208,34 @@ export function activate(context: vscode.ExtensionContext): void { // Sync Pull context.subscriptions.push( vscode.commands.registerCommand('movez.syncPull', async () => { - const serverUrl = getServerUrl(); - const token = getToken(); - - if (!serverUrl) { - vscode.window.showErrorMessage('Set movez.serverUrl in settings first.'); + const creds = await syncCredentials(); + if (!creds) { return; } - const tools = ['cursor', 'windsurf', 'claude-code', 'codex', 'copilot-cli']; - const targetTool = await vscode.window.showQuickPick(tools, { + const targetTool = await vscode.window.showQuickPick(WRITABLE_TOOLS, { placeHolder: 'Import pulled sessions into which tool?', }); - if (!targetTool) { return; } - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; + const project = workspaceFolder(); await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Syncing pull...' }, + { location: vscode.ProgressLocation.Notification, title: 'MoveZ: Pulling sessions...' }, async () => { try { - await execFileAsync( - getCli(), - [ - 'sync:pull', - `--server=${serverUrl}`, - `--token=${token}`, - `--tool=${targetTool}`, - `--project=${workspaceFolder}`, - ], - { timeout: 60_000 }, - ); - vscode.window.showInformationMessage(`Sessions pulled into ${targetTool}`); + const args = ['sync:pull', `--server=${creds.server}`, `--tool=${targetTool}`]; + if (project) { + args.push(`--project=${project}`); + } + + const { stdout } = await runCli(args, { timeoutMs: 300_000, env: { MOVEZ_TOKEN: creds.token } }); + vscode.window.showInformationMessage(stdout.trim().split('\n')[0] || `Sessions pulled into ${targetTool}`); provider.refresh(); } catch (err) { - vscode.window.showErrorMessage(`Sync pull failed: ${String(err)}`); + showError('Sync pull failed', err); } }, ); diff --git a/vscode-extension/src/types.ts b/vscode-extension/src/types.ts index 25f4885..03d2fd1 100644 --- a/vscode-extension/src/types.ts +++ b/vscode-extension/src/types.ts @@ -18,10 +18,13 @@ export interface TurnDTO { export interface SessionDTO { id: string; title: string; + project: string; source_tool: string; source_machine_id: string; created_at: string; // ISO 8601 last_active_at: string; + /** Present even when `turns` is empty (list-sessions only loads metadata). */ + turn_count: number; turns: TurnDTO[]; } From f2eea9c484406b611935182d255da92d399c7fc4 Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:43:45 -0400 Subject: [PATCH 5/8] Add `movez show` to load a single session with all turns list-sessions only reads metadata for Claude Code (60 MB+ transcripts), so clients had no way to display a session's turns. ClaudeCodeParser can now parse a single transcript by id without scanning every file. Co-Authored-By: Claude Opus 5.5 --- movez/app/Commands/ShowCommand.php | 61 ++++++++ movez/app/Parsers/ClaudeCodeParser.php | 145 ++++++++++-------- .../Commands/ExportImportCommandTest.php | 15 ++ 3 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 movez/app/Commands/ShowCommand.php diff --git a/movez/app/Commands/ShowCommand.php b/movez/app/Commands/ShowCommand.php new file mode 100644 index 0000000..5123b78 --- /dev/null +++ b/movez/app/Commands/ShowCommand.php @@ -0,0 +1,61 @@ +option('tool'); + $id = (string) $this->option('id'); + $project = (string) ($this->option('project') ?? getcwd()); + + if ($toolName === '' || $id === '') { + $this->error('Both --tool and --id are required'); + return self::FAILURE; + } + + try { + $parser = $detector->getParser($toolName); + $session = $parser instanceof ClaudeCodeParser + ? $parser->findById($project, $id) + : $parser->parse($project)->first(fn(SessionDTO $s) => $s->id === $id); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + return self::FAILURE; + } + + if ($session === null) { + $this->error("Session {$id} not found in {$toolName}"); + return self::FAILURE; + } + + if ($this->option('json')) { + $this->line((string) json_encode($session->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + return self::SUCCESS; + } + + $this->info($session->title); + $this->line("{$session->sourceTool} · {$session->project} · {$session->turnCount()} turn(s)"); + foreach ($session->turns as $turn) { + $this->line(''); + $this->line("[{$turn->role}] " . $turn->content); + } + + return self::SUCCESS; + } +} diff --git a/movez/app/Parsers/ClaudeCodeParser.php b/movez/app/Parsers/ClaudeCodeParser.php index e5e342e..a6a7543 100644 --- a/movez/app/Parsers/ClaudeCodeParser.php +++ b/movez/app/Parsers/ClaudeCodeParser.php @@ -145,85 +145,110 @@ public function parse(string $projectPath): Collection $sessions = collect(); foreach ($files as $file) { - $lines = $this->readJsonlFile($file); - if (empty($lines)) { - continue; + $session = $this->parseFile($file); + if ($session !== null) { + $sessions->push($session); } + } - $sessionId = pathinfo($file, PATHINFO_FILENAME); - $turns = collect(); - $createdAt = null; - $lastAt = null; + return $sessions; + } - $firstUserContent = null; - $cwd = null; + /** + * Parse a single session by id without reading every transcript — + * Claude Code stores one file per session, named after its id. + */ + public function findById(string $projectPath, string $id): ?SessionDTO + { + $storagePath = $this->getStoragePath($projectPath); + $id = basename($id); - foreach ($lines as $line) { - if (!is_array($line)) { - continue; - } + $files = array_merge( + glob($storagePath . '/' . $id . '.jsonl') ?: [], + glob($storagePath . '/*/' . $id . '.jsonl') ?: [], + ); - // Real Claude Code format: type=user|assistant with nested message object - $type = $line['type'] ?? null; - if (!in_array($type, ['user', 'assistant'], true)) { - continue; - } + return $files === [] ? null : $this->parseFile($files[0]); + } - if ($cwd === null && is_string($line['cwd'] ?? null)) { - $cwd = $line['cwd']; - } + private function parseFile(string $file): ?SessionDTO + { + $lines = $this->readJsonlFile($file); + if (empty($lines)) { + return null; + } - $msg = is_array($line['message'] ?? null) ? $line['message'] : []; - $role = $msg['role'] ?? $type; + $sessionId = pathinfo($file, PATHINFO_FILENAME); + $turns = collect(); + $createdAt = null; + $lastAt = null; - // Content is a string or an array of blocks; thinking/tool blocks are dropped - $rawContent = ContentFlattener::flatten($msg['content'] ?? ''); - if (trim($rawContent) === '') { - continue; // e.g. tool_result-only entries - } + $firstUserContent = null; + $cwd = null; - $ts = Carbon::parse($line['timestamp'] ?? 'now'); + foreach ($lines as $line) { + if (!is_array($line)) { + continue; + } - if ($createdAt === null) { - $createdAt = $ts; - } - $lastAt = $ts; + // Real Claude Code format: type=user|assistant with nested message object + $type = $line['type'] ?? null; + if (!in_array($type, ['user', 'assistant'], true)) { + continue; + } - if ($firstUserContent === null && $role === 'user') { - $firstUserContent = $rawContent; - } + if ($cwd === null && is_string($line['cwd'] ?? null)) { + $cwd = $line['cwd']; + } - $turns->push(new TurnDTO( - role: $role, - content: (string) $rawContent, - timestamp: $ts, - filesReferenced: [], - fileDiffs: collect(), - reasoningTrace: null, - toolCalls: [], - )); + $msg = is_array($line['message'] ?? null) ? $line['message'] : []; + $role = $msg['role'] ?? $type; + + // Content is a string or an array of blocks; thinking/tool blocks are dropped + $rawContent = ContentFlattener::flatten($msg['content'] ?? ''); + if (trim($rawContent) === '') { + continue; // e.g. tool_result-only entries } - $project = $this->projectName($cwd, basename(dirname($file))); + $ts = Carbon::parse($line['timestamp'] ?? 'now'); - // Use first user message as title (truncated to 80 chars) - $title = $firstUserContent !== null - ? mb_substr(trim($firstUserContent), 0, 80) - : basename($file); + if ($createdAt === null) { + $createdAt = $ts; + } + $lastAt = $ts; - $sessions->push(new SessionDTO( - id: $sessionId, - title: $title, - sourceTool: $this->toolName(), - sourceMachineSha: $this->machineSha(), - createdAt: $createdAt ?? Carbon::now(), - lastActiveAt: $lastAt ?? Carbon::now(), - turns: $turns, - project: $project, + if ($firstUserContent === null && $role === 'user') { + $firstUserContent = $rawContent; + } + + $turns->push(new TurnDTO( + role: $role, + content: (string) $rawContent, + timestamp: $ts, + filesReferenced: [], + fileDiffs: collect(), + reasoningTrace: null, + toolCalls: [], )); } - return $sessions; + $project = $this->projectName($cwd, basename(dirname($file))); + + // Use first user message as title (truncated to 80 chars) + $title = $firstUserContent !== null + ? mb_substr(trim($firstUserContent), 0, 80) + : basename($file); + + return new SessionDTO( + id: $sessionId, + title: $title, + sourceTool: $this->toolName(), + sourceMachineSha: $this->machineSha(), + createdAt: $createdAt ?? Carbon::now(), + lastActiveAt: $lastAt ?? Carbon::now(), + turns: $turns, + project: $project, + ); } /** diff --git a/movez/tests/Feature/Commands/ExportImportCommandTest.php b/movez/tests/Feature/Commands/ExportImportCommandTest.php index 620fb57..3baaf68 100644 --- a/movez/tests/Feature/Commands/ExportImportCommandTest.php +++ b/movez/tests/Feature/Commands/ExportImportCommandTest.php @@ -108,3 +108,18 @@ function seedClaudeSession(string $storage, string $id, string $cwd, string $tex it('inspire boilerplate command is gone', function (): void { expect(array_keys(\Illuminate\Support\Facades\Artisan::all()))->not->toContain('inspire'); }); + +it('show returns a single session with its turns', function (): void { + withTempDir(function (string $dir): void { + seedClaudeSession($dir . '/claude', 'sess-a', '/work/alpha', 'Alpha task'); + seedClaudeSession($dir . '/claude', 'sess-b', '/work/beta', 'Beta task'); + config(["movez.tools.claude-code.storage.{$this->os}" => $dir . '/claude']); + + $this->artisan('show', ['--tool' => 'claude-code', '--id' => 'sess-b', '--json' => true]) + ->expectsOutputToContain('"title": "Beta task"') + ->assertExitCode(0); + + $this->artisan('show', ['--tool' => 'claude-code', '--id' => 'missing']) + ->assertExitCode(1); + }); +}); From 9f54cad782b807c6ceaad96363faf2250fc97987 Mon Sep 17 00:00:00 2001 From: KeyVan Date: Thu, 8 Oct 2026 10:43:45 -0400 Subject: [PATCH 6/8] Fix desktop app sync, settings and session details - Sync pull no longer fails on --from-path/--to-path (CLI now accepts them); the sync token is passed as MOVEZ_TOKEN, not a CLI argument - Session detail loads full turns via `movez show` when the list only has metadata (Claude Code sessions always showed "No turns") - Settings IPC only accepts known keys with validated values, since cliPath/phpPath are executed by the main process - Enable the renderer sandbox; platform-aware bundled PHP binary name - Source/target pickers only offer tools the CLI can read/write - Real type check (vue-tsc + tsc); fixes the missing phpPath type Co-Authored-By: Claude Opus 5.5 --- electron-app/electron/cli.ts | 18 ++++++++++---- electron-app/electron/ipc/sessions.ts | 10 ++++++++ electron-app/electron/ipc/settings.ts | 31 +++++++++++++++++++++--- electron-app/electron/ipc/sync.ts | 11 ++++++--- electron-app/electron/main.ts | 2 +- electron-app/electron/preload.ts | 1 + electron-app/src/pages/Migrate.vue | 4 +-- electron-app/src/pages/SessionDetail.vue | 28 +++++++++++---------- electron-app/src/pages/Sessions.vue | 5 ++-- electron-app/src/pages/Sync.vue | 4 +-- electron-app/src/stores/settings.ts | 1 + electron-app/src/tools.ts | 5 ++++ electron-app/src/types/bridge.d.ts | 4 ++- 13 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 electron-app/src/tools.ts diff --git a/electron-app/electron/cli.ts b/electron-app/electron/cli.ts index dfccd16..c504b3a 100644 --- a/electron-app/electron/cli.ts +++ b/electron-app/electron/cli.ts @@ -17,7 +17,7 @@ function bundledPhpPath(): string { const base = app.isPackaged ? process.resourcesPath : join(__dirname, '../../resources') - return join(base, 'php', 'php.exe') + return join(base, 'php', process.platform === 'win32' ? 'php.exe' : 'php') } /** Path to bundled PHAR inside the installed app resources */ @@ -50,13 +50,20 @@ function buildSpawnArgs(cliPath: string, args: string[]): { cmd: string; cmdArgs return { cmd: cliPath, cmdArgs: args } } -export function runCli(args: string[]): Promise { +/** Extra environment for the CLI — used for secrets (MOVEZ_TOKEN) so they never appear in process lists. */ +type CliEnv = Record + +function spawnOptions(env?: CliEnv) { + return { windowsHide: true, env: { ...process.env, ...env } } +} + +export function runCli(args: string[], env?: CliEnv): Promise { return new Promise((resolve) => { const { cmd, cmdArgs } = buildSpawnArgs(getCliPath(), args) const chunks: Buffer[] = [] const errChunks: Buffer[] = [] - const proc = spawn(cmd, cmdArgs, { windowsHide: true }) + const proc = spawn(cmd, cmdArgs, spawnOptions(env)) proc.stdout.on('data', (d) => chunks.push(Buffer.from(d))) proc.stderr.on('data', (d) => errChunks.push(Buffer.from(d))) @@ -78,10 +85,11 @@ export function runCli(args: string[]): Promise { export function streamCli( args: string[], onData: (line: string) => void, - onDone: (code: number) => void + onDone: (code: number) => void, + env?: CliEnv ): void { const { cmd, cmdArgs } = buildSpawnArgs(getCliPath(), args) - const proc = spawn(cmd, cmdArgs, { windowsHide: true }) + const proc = spawn(cmd, cmdArgs, spawnOptions(env)) proc.stdout.on('data', (d: Buffer) => { d.toString('utf8').split('\n').filter(Boolean).forEach(onData) diff --git a/electron-app/electron/ipc/sessions.ts b/electron-app/electron/ipc/sessions.ts index 3e7c38c..8c4d00d 100644 --- a/electron-app/electron/ipc/sessions.ts +++ b/electron-app/electron/ipc/sessions.ts @@ -21,6 +21,16 @@ export function registerSessionHandlers(): void { } }) + // Load one session with all of its turns (list-sessions may only return metadata) + ipcMain.handle('sessions:get', async (_event, tool: string, id: string) => { + const result = await runCli(['show', `--tool=${tool}`, `--id=${id}`, '--json']) + if (result.code !== 0) { + throw new Error(result.stderr || result.stdout || 'Session not found') + } + + return JSON.parse(result.stdout) + }) + // Export sessions to a .cbz bundle ipcMain.handle('sessions:export', async (_event, opts: { tool: string diff --git a/electron-app/electron/ipc/settings.ts b/electron-app/electron/ipc/settings.ts index 5fdf61c..90ce30e 100644 --- a/electron-app/electron/ipc/settings.ts +++ b/electron-app/electron/ipc/settings.ts @@ -19,16 +19,39 @@ const store = new Store({ } }) +// The renderer may only change known keys, with the right types. cliPath and +// phpPath are executed by the main process, so they must be plain strings. +const VALIDATORS: { [K in keyof Settings]: (v: unknown) => v is Settings[K] } = { + cliPath: (v): v is string => typeof v === 'string' && v.length < 1024 && !/[\r\n]/.test(v), + phpPath: (v): v is string => typeof v === 'string' && v.length < 1024 && !/[\r\n]/.test(v), + serverUrl: (v): v is string => typeof v === 'string' && (v === '' || /^https?:\/\/[^\s]+$/i.test(v)), + token: (v): v is string => typeof v === 'string' && v.length < 512, + darkMode: (v): v is boolean => typeof v === 'boolean' +} + +function applySetting(key: string, value: unknown): void { + if (!Object.prototype.hasOwnProperty.call(VALIDATORS, key)) { + throw new Error(`Unknown setting: ${key}`) + } + + const k = key as keyof Settings + if (!VALIDATORS[k](value)) { + throw new Error(`Invalid value for setting ${key}`) + } + + store.set(k, value as Settings[typeof k]) +} + export function registerSettingsHandlers(): void { ipcMain.handle('settings:get', () => store.store) - ipcMain.handle('settings:set', (_event, key: keyof Settings, value: unknown) => { - store.set(key, value) + ipcMain.handle('settings:set', (_event, key: string, value: unknown) => { + applySetting(key, value) return store.store }) - ipcMain.handle('settings:setAll', (_event, settings: Partial) => { - Object.entries(settings).forEach(([k, v]) => store.set(k as keyof Settings, v)) + ipcMain.handle('settings:setAll', (_event, settings: Record) => { + Object.entries(settings ?? {}).forEach(([k, v]) => applySetting(k, v)) return store.store }) } diff --git a/electron-app/electron/ipc/sync.ts b/electron-app/electron/ipc/sync.ts index df21c5b..98f082e 100644 --- a/electron-app/electron/ipc/sync.ts +++ b/electron-app/electron/ipc/sync.ts @@ -8,7 +8,8 @@ export function registerSyncHandlers(): void { tool?: string project?: string }) => { - const args = ['sync:push', `--token=${opts.token}`, `--server=${opts.server}`] + // Token is passed via MOVEZ_TOKEN, never as an argument (visible in process lists) + const args = ['sync:push', `--server=${opts.server}`] if (opts.tool) args.push(`--tool=${opts.tool}`) if (opts.project) args.push(`--project=${opts.project}`) @@ -22,7 +23,8 @@ export function registerSyncHandlers(): void { lines.push(line) win?.webContents.send('sync:log', line) }, - (code) => resolve({ success: code === 0, output: lines.join('\n') }) + (code) => resolve({ success: code === 0, output: lines.join('\n') }), + { MOVEZ_TOKEN: opts.token } ) }) }) @@ -35,7 +37,7 @@ export function registerSyncHandlers(): void { fromPath?: string toPath?: string }) => { - const args = ['sync:pull', `--token=${opts.token}`, `--server=${opts.server}`, `--tool=${opts.tool}`] + const args = ['sync:pull', `--server=${opts.server}`, `--tool=${opts.tool}`] if (opts.project) args.push(`--project=${opts.project}`) if (opts.fromPath) args.push(`--from-path=${opts.fromPath}`) if (opts.toPath) args.push(`--to-path=${opts.toPath}`) @@ -50,7 +52,8 @@ export function registerSyncHandlers(): void { lines.push(line) win?.webContents.send('sync:log', line) }, - (code) => resolve({ success: code === 0, output: lines.join('\n') }) + (code) => resolve({ success: code === 0, output: lines.join('\n') }), + { MOVEZ_TOKEN: opts.token } ) }) }) diff --git a/electron-app/electron/main.ts b/electron-app/electron/main.ts index 038ecd1..f240b6a 100644 --- a/electron-app/electron/main.ts +++ b/electron-app/electron/main.ts @@ -28,7 +28,7 @@ function createWindow(): void { backgroundColor: '#0f172a', webPreferences: { preload: join(__dirname, '../preload/preload.js'), - sandbox: false, + sandbox: true, contextIsolation: true, nodeIntegration: false } diff --git a/electron-app/electron/preload.ts b/electron-app/electron/preload.ts index ac8bf0b..8e9757b 100644 --- a/electron-app/electron/preload.ts +++ b/electron-app/electron/preload.ts @@ -3,6 +3,7 @@ import { contextBridge, ipcRenderer } from 'electron' export const bridge = { // Sessions listSessions: (tool?: string) => ipcRenderer.invoke('sessions:list', tool), + getSession: (tool: string, id: string) => ipcRenderer.invoke('sessions:get', tool, id), exportSessions: (opts: unknown) => ipcRenderer.invoke('sessions:export', opts), importSessions: (opts: unknown) => ipcRenderer.invoke('sessions:import', opts), diff --git a/electron-app/src/pages/Migrate.vue b/electron-app/src/pages/Migrate.vue index 281862d..d20d0e9 100644 --- a/electron-app/src/pages/Migrate.vue +++ b/electron-app/src/pages/Migrate.vue @@ -3,7 +3,7 @@ import { ref, computed } from 'vue' import LogStream from '@/components/LogStream.vue' import ToolBadge from '@/components/ToolBadge.vue' -const TOOLS = ['cursor', 'windsurf', 'claude-code', 'codex', 'copilot-cli', 'cline', 'continue'] +import { READABLE_TOOLS as TOOLS, WRITABLE_TOOLS } from '@/tools' // Wizard state const step = ref(1) @@ -111,7 +111,7 @@ function reset() {

Select target tool