summaryrefslogtreecommitdiff
diff options
-rw-r--r--composer.lock278
-rw-r--r--run.php4
-rw-r--r--src/Commands/DownloadVendorCommand.php135
-rw-r--r--src/Commands/InitCommand.php36
-rw-r--r--src/Commands/UpdateCommand.php31
-rw-r--r--src/Services/VendorDownloader.php151
-rw-r--r--src/app.php2
-rw-r--r--tests/Commands/BackupCommandTest.php2
-rw-r--r--tests/Commands/UpdateCommandTest.php34
9 files changed, 358 insertions, 315 deletions
diff --git a/composer.lock b/composer.lock
index 42de317..7069c2e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -70,16 +70,16 @@
},
{
"name": "phpoption/phpoption",
- "version": "1.9.3",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
@@ -129,7 +129,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -141,7 +141,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:41:07+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "psr/container",
@@ -198,16 +198,16 @@
},
{
"name": "symfony/console",
- "version": "v6.4.21",
+ "version": "v6.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719"
+ "reference": "1b2813049506b39eb3d7e64aff033fd5ca26c97e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719",
- "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719",
+ "url": "https://api.github.com/repos/symfony/console/zipball/1b2813049506b39eb3d7e64aff033fd5ca26c97e",
+ "reference": "1b2813049506b39eb3d7e64aff033fd5ca26c97e",
"shasum": ""
},
"require": {
@@ -272,7 +272,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.21"
+ "source": "https://github.com/symfony/console/tree/v6.4.30"
},
"funding": [
{
@@ -284,24 +284,28 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-07T15:42:41+00:00"
+ "time": "2025-12-05T13:47:41+00:00"
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
- "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@@ -314,7 +318,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -339,7 +343,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -355,11 +359,11 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -418,7 +422,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -430,6 +434,10 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
@@ -438,16 +446,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -496,7 +504,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
@@ -508,15 +516,19 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -577,7 +589,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -589,6 +601,10 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
@@ -597,7 +613,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -658,7 +674,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -670,6 +686,10 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
@@ -678,7 +698,7 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -738,7 +758,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -750,6 +770,10 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
@@ -758,16 +782,16 @@
},
{
"name": "symfony/process",
- "version": "v6.4.20",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20"
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20",
- "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20",
+ "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
"shasum": ""
},
"require": {
@@ -799,7 +823,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.20"
+ "source": "https://github.com/symfony/process/tree/v6.4.26"
},
"funding": [
{
@@ -811,24 +835,28 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-10T17:11:00+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
- "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
@@ -846,7 +874,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -882,7 +910,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -894,24 +922,28 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2025-07-15T11:30:57+00:00"
},
{
"name": "symfony/string",
- "version": "v6.4.21",
+ "version": "v6.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "73e2c6966a5aef1d4892873ed5322245295370c6"
+ "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6",
- "reference": "73e2c6966a5aef1d4892873ed5322245295370c6",
+ "url": "https://api.github.com/repos/symfony/string/zipball/50590a057841fa6bf69d12eceffce3465b9e32cb",
+ "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb",
"shasum": ""
},
"require": {
@@ -925,7 +957,6 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/error-handler": "^5.4|^6.0|^7.0",
"symfony/http-client": "^5.4|^6.0|^7.0",
"symfony/intl": "^6.2|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -968,7 +999,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v6.4.21"
+ "source": "https://github.com/symfony/string/tree/v6.4.30"
},
"funding": [
{
@@ -980,11 +1011,15 @@
"type": "github"
},
{
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-18T15:23:29+00:00"
+ "time": "2025-11-21T18:03:05+00:00"
},
{
"name": "vlucas/phpdotenv",
@@ -1074,16 +1109,16 @@
"packages-dev": [
{
"name": "myclabs/deep-copy",
- "version": "1.13.1",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
- "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -1122,7 +1157,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -1130,20 +1165,20 @@
"type": "tidelift"
}
],
- "time": "2025-04-29T12:36:36+00:00"
+ "time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.4.0",
+ "version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -1162,7 +1197,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -1186,9 +1221,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
- "time": "2024-12-30T11:07:19+00:00"
+ "time": "2025-12-06T11:56:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -1631,16 +1666,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "10.5.46",
+ "version": "10.5.60",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d"
+ "reference": "f2e26f52f80ef77832e359205f216eeac00e320c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d",
- "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c",
+ "reference": "f2e26f52f80ef77832e359205f216eeac00e320c",
"shasum": ""
},
"require": {
@@ -1650,7 +1685,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.1",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@@ -1661,13 +1696,13 @@
"phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0",
- "sebastian/comparator": "^5.0.3",
+ "sebastian/comparator": "^5.0.4",
"sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0",
- "sebastian/exporter": "^5.1.2",
+ "sebastian/exporter": "^5.1.4",
"sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0",
- "sebastian/recursion-context": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.1",
"sebastian/type": "^4.0.0",
"sebastian/version": "^4.0.1"
},
@@ -1712,7 +1747,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60"
},
"funding": [
{
@@ -1736,7 +1771,7 @@
"type": "tidelift"
}
],
- "time": "2025-05-02T06:46:24+00:00"
+ "time": "2025-12-06T07:50:42+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1908,16 +1943,16 @@
},
{
"name": "sebastian/comparator",
- "version": "5.0.3",
+ "version": "5.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
+ "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
+ "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"shasum": ""
},
"require": {
@@ -1973,15 +2008,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2024-10-18T14:56:07+00:00"
+ "time": "2025-09-07T05:25:07+00:00"
},
{
"name": "sebastian/complexity",
@@ -2174,16 +2221,16 @@
},
{
"name": "sebastian/exporter",
- "version": "5.1.2",
+ "version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
+ "reference": "0735b90f4da94969541dac1da743446e276defa6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
+ "reference": "0735b90f4da94969541dac1da743446e276defa6",
"shasum": ""
},
"require": {
@@ -2192,7 +2239,7 @@
"sebastian/recursion-context": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -2240,15 +2287,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T07:17:12+00:00"
+ "time": "2025-09-24T06:09:11+00:00"
},
{
"name": "sebastian/global-state",
@@ -2484,23 +2543,23 @@
},
{
"name": "sebastian/recursion-context",
- "version": "5.0.0",
+ "version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712"
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -2535,15 +2594,28 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T07:05:40+00:00"
+ "time": "2025-08-10T07:50:56+00:00"
},
{
"name": "sebastian/type",
@@ -2656,16 +2728,16 @@
},
{
"name": "theseer/tokenizer",
- "version": "1.2.3",
+ "version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@@ -2694,7 +2766,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -2702,7 +2774,7 @@
"type": "github"
}
],
- "time": "2024-03-03T12:36:25+00:00"
+ "time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
diff --git a/run.php b/run.php
index 66b4721..5f88b98 100644
--- a/run.php
+++ b/run.php
@@ -25,8 +25,8 @@ $formatter->setStyle('error', new OutputFormatterStyle('red'));
// Run the command and handle errors
try {
- $output->writeln("<warn>WARNING: This CLI is in alpha testing.</warn>");
- $output->writeln("<warn>There's a high chance of issues, and the CLI API is subject to change.</warn>");
+ $output->writeln("<warn>WARNING: This CLI is in beta testing.</warn>");
+ $output->writeln("<warn>There's a chance of issues, and the CLI API is subject to change.</warn>");
$output->writeln("");
$app->run(null, $output);
diff --git a/src/Commands/DownloadVendorCommand.php b/src/Commands/DownloadVendorCommand.php
index d3b0501..5019238 100644
--- a/src/Commands/DownloadVendorCommand.php
+++ b/src/Commands/DownloadVendorCommand.php
@@ -3,14 +3,12 @@
namespace Cli\Commands;
use Cli\Services\AppLocator;
+use Cli\Services\VendorDownloader;
use Exception;
-use RecursiveDirectoryIterator;
-use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use ZipArchive;
class DownloadVendorCommand extends Command
{
@@ -28,136 +26,9 @@ class DownloadVendorCommand extends Command
{
$appDir = AppLocator::require($input->getOption('app-directory'));
- $output->writeln("<info>Checking app version...</info>");
- $version = AppLocator::getVersion($appDir);
- if (empty($version)) {
- throw new CommandError("Could not determine instance BookStack version.");
- }
- $targetChecksum = $this->getTargetChecksum($appDir);
-
- $output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>");
- $zip = $this->downloadVendorZip($version);
-
- $output->writeln("<info>Validating downloaded ZIP...</info>");
- $this->verifyZipChecksum($zip, $targetChecksum);
-
- $output->writeln("<info>Deleting existing vendor/ directory...</info>");
- try {
- $this->deleteAppVendorFiles($appDir);
- } catch (Exception $exception) {
- unlink($zip);
- throw $exception;
- }
-
- $output->writeln("<info>Extracting ZIP into BookStack instance...</info>");
- $this->extractZip($zip, $appDir);
-
- $output->writeln("<info>Cleaning up old app services...</info>");
- $cleaned = $this->cleanupAppServices($appDir);
- if (!$cleaned) {
- $output->writeln("<warning>Failed to remove exising app services file</warning>");
- }
-
- $output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>");
+ $downloader = new VendorDownloader($appDir, $output);
+ $downloader->download();
return Command::SUCCESS;
}
-
- protected function cleanupAppServices(string $appDir): bool
- {
- $filesToClear = [
- implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'services.php']),
- implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'packages.php']),
- ];
-
- $status = true;
-
- foreach ($filesToClear as $file) {
- if (file_exists($file)) {
- if (@unlink($file) === false) {
- $status = false;
- }
- }
- }
-
- return $status;
- }
-
- protected function extractZip(string $zipPath, string $appDir): void
- {
- $zip = new ZipArchive();
- $opened = $zip->open($zipPath, ZipArchive::RDONLY);
- $extracted = $zip->extractTo($appDir);
- $closed = $zip->close();
-
- unlink($zipPath);
- if (!$opened || !$extracted || !$closed) {
- throw new CommandError("Failed to extract ZIP files into {$appDir}");
- }
- }
-
- protected function deleteAppVendorFiles(string $appDir): void
- {
- $targetDir = $appDir . DIRECTORY_SEPARATOR . 'vendor';
- if (!is_dir($targetDir)) {
- return;
- }
-
- $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
- $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
- foreach($files as $file) {
- if ($file->isDir()){
- rmdir($file->getPathname());
- } else {
- unlink($file->getPathname());
- }
- }
-
- $deleted = rmdir($targetDir);
- if (!$deleted) {
- throw new CommandError("Could not delete existing app vendor directory.");
- }
- }
-
- protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
- {
- $zipChecksum = hash_file('sha256', $zipPath);
- if ($zipChecksum !== $targetChecksum) {
- unlink($zipPath);
- throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
- }
- }
-
- protected function downloadVendorZip(string $version): string
- {
- $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
- $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
-
- $targetFile = @fopen($targetUrl, 'rb');
- if ($targetFile === false) {
- throw new CommandError("Failed to download ZIP file from $targetUrl");
- }
-
- file_put_contents($tempFile, $targetFile);
-
- return $tempFile;
- }
-
- /**
- * @throws CommandError
- */
- protected function getTargetChecksum(string $appDir): string
- {
- $checksumFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'dev', 'checksums', 'vendor']);
- $checksum = '';
- if (file_exists($checksumFile)) {
- $checksum = trim(file_get_contents($checksumFile));
- }
-
- if (empty($checksum)) {
- throw new CommandError("Could not find a vendor checksum for validation.");
- }
-
- return $checksum;
- }
}
diff --git a/src/Commands/InitCommand.php b/src/Commands/InitCommand.php
index 4ebd267..80d17e9 100644
--- a/src/Commands/InitCommand.php
+++ b/src/Commands/InitCommand.php
@@ -2,11 +2,11 @@
namespace Cli\Commands;
-use Cli\Services\ComposerLocator;
use Cli\Services\EnvironmentLoader;
use Cli\Services\Paths;
use Cli\Services\ProgramRunner;
use Cli\Services\RequirementsValidator;
+use Cli\Services\VendorDownloader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -38,16 +38,9 @@ class InitCommand extends Command
$output->writeln("<info>Cloning down BookStack project to install directory...</info>");
$this->cloneBookStackViaGit($installDir);
- $output->writeln("<info>Checking composer exists...</info>");
- $composerLocator = new ComposerLocator($installDir);
- $composer = $composerLocator->getProgram();
- if (!$composer->isFound()) {
- $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
- $composerLocator->download();
- }
-
- $output->writeln("<info>Installing application dependencies using composer...</info>");
- $this->installComposerDependencies($composer, $installDir);
+ $output->writeln("<info>Downloading PHP dependencies from files.bookstackapp.com..</info>");
+ $vendorDownloader = new VendorDownloader($installDir, $output);
+ $vendorDownloader->download();
$output->writeln("<info>Creating .env file from .env.example...</info>");
copy(Paths::join($installDir, '.env.example'), Paths::join($installDir, '.env'));
@@ -84,24 +77,7 @@ class InitCommand extends Command
}
/**
- * Run composer install to download PHP dependencies.
- * @throws CommandError
- */
- protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
- {
- $errors = $composer->runCapturingStdErr([
- 'install',
- '--no-dev', '-n', '-q', '--no-progress',
- '-d', $installDir
- ]);
-
- if ($errors) {
- throw new CommandError("Failed composer install with errors:\n" . $errors);
- }
- }
-
- /**
- * Clone a new instance of BookStack to the given install folder.
+ * Clone a new instance of BookStack to the given installation folder.
* @throws CommandError
*/
protected function cloneBookStackViaGit(string $installDir): void
@@ -114,7 +90,7 @@ class InitCommand extends Command
'clone', '-q',
'--branch', 'release',
'--single-branch',
- 'https://github.com/BookStackApp/BookStack.git',
+ 'https://source.bookstackapp.com/bookstack.git',
$installDir
]);
diff --git a/src/Commands/UpdateCommand.php b/src/Commands/UpdateCommand.php
index 23ffc2d..c6fc476 100644
--- a/src/Commands/UpdateCommand.php
+++ b/src/Commands/UpdateCommand.php
@@ -4,10 +4,10 @@ namespace Cli\Commands;
use Cli\Services\AppLocator;
use Cli\Services\ArtisanRunner;
-use Cli\Services\ComposerLocator;
use Cli\Services\Paths;
use Cli\Services\ProgramRunner;
use Cli\Services\RequirementsValidator;
+use Cli\Services\VendorDownloader;
use Phar;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -35,14 +35,6 @@ class UpdateCommand extends Command
$output->writeln("<info>Checking local Git repository is active...</info>");
$this->ensureGitRepoExists($appDir);
- $output->writeln("<info>Checking composer exists...</info>");
- $composerLocator = new ComposerLocator($appDir);
- $composer = $composerLocator->getProgram();
- if (!$composer->isFound()) {
- $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
- $composerLocator->download();
- }
-
$cliPath = Phar::running(false);
$cliPreUpdateHash = $cliPath ? hash_file('sha256', $cliPath) : '';
@@ -55,8 +47,9 @@ class UpdateCommand extends Command
return Command::FAILURE;
}
- $output->writeln("<info>Installing PHP dependencies via composer...</info>");
- $this->installComposerDependencies($composer, $appDir);
+ $output->writeln("<info>Downloading PHP dependencies from files.bookstackapp.com...</info>");
+ $vendorDownloader = new VendorDownloader($appDir, $output);
+ $vendorDownloader->download();
$output->writeln("<info>Running database migrations...</info>");
$artisan = (new ArtisanRunner($appDir));
@@ -90,22 +83,6 @@ class UpdateCommand extends Command
}
}
- /**
- * @throws CommandError
- */
- protected function installComposerDependencies(ProgramRunner $composer, string $appDir): void
- {
- $errors = $composer->runCapturingStdErr([
- 'install',
- '--no-dev', '-n', '-q', '--no-progress',
- '-d', $appDir,
- ]);
-
- if ($errors) {
- throw new CommandError("Failed composer install with errors:\n" . $errors);
- }
- }
-
protected function ensureGitRepoExists(string $appDir): void
{
$expectedPath = Paths::join($appDir, '.git');
diff --git a/src/Services/VendorDownloader.php b/src/Services/VendorDownloader.php
new file mode 100644
index 0000000..389baf2
--- /dev/null
+++ b/src/Services/VendorDownloader.php
@@ -0,0 +1,151 @@
+<?php declare(strict_types=1);
+
+namespace Cli\Services;
+
+use Cli\Commands\CommandError;
+use Exception;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use Symfony\Component\Console\Output\OutputInterface;
+use ZipArchive;
+
+class VendorDownloader
+{
+ public function __construct(
+ protected string $appDir,
+ protected OutputInterface $output
+ ) {}
+
+ public function download(): void
+ {
+ $this->output->writeln("<info>Checking app version...</info>");
+ $version = AppLocator::getVersion($this->appDir);
+ if (empty($version)) {
+ throw new CommandError("Could not determine instance BookStack version.");
+ }
+ $targetChecksum = $this->getTargetChecksum();
+
+ $this->output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>");
+ $zip = $this->downloadVendorZip($version);
+
+ $this->output->writeln("<info>Validating downloaded ZIP...</info>");
+ $this->verifyZipChecksum($zip, $targetChecksum);
+
+ $this->output->writeln("<info>Deleting existing vendor/ directory...</info>");
+ try {
+ $this->deleteAppVendorFiles();
+ } catch (Exception $exception) {
+ unlink($zip);
+ throw $exception;
+ }
+
+ $this->output->writeln("<info>Extracting ZIP into BookStack instance...</info>");
+ $this->extractZip($zip);
+
+ $this->output->writeln("<info>Cleaning up old app services...</info>");
+ $cleaned = $this->cleanupAppServices();
+ if (!$cleaned) {
+ $this->output->writeln("<warning>Failed to remove exising app services file</warning>");
+ }
+
+ $this->output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>");
+ }
+
+ protected function cleanupAppServices(): bool
+ {
+ $filesToClear = [
+ implode(DIRECTORY_SEPARATOR, [$this->appDir, 'bootstrap', 'cache', 'services.php']),
+ implode(DIRECTORY_SEPARATOR, [$this->appDir, 'bootstrap', 'cache', 'packages.php']),
+ ];
+
+ $status = true;
+
+ foreach ($filesToClear as $file) {
+ if (file_exists($file)) {
+ if (@unlink($file) === false) {
+ $status = false;
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ protected function extractZip(string $zipPath): void
+ {
+ $zip = new ZipArchive();
+ $opened = $zip->open($zipPath, ZipArchive::RDONLY);
+ $extracted = $zip->extractTo($this->appDir);
+ $closed = $zip->close();
+
+ unlink($zipPath);
+ if (!$opened || !$extracted || !$closed) {
+ throw new CommandError("Failed to extract ZIP files into {$this->appDir}");
+ }
+ }
+
+ protected function deleteAppVendorFiles(): void
+ {
+ $targetDir = $this->appDir . DIRECTORY_SEPARATOR . 'vendor';
+ if (!is_dir($targetDir)) {
+ return;
+ }
+
+ $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
+ $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+ foreach($files as $file) {
+ if ($file->isDir()){
+ rmdir($file->getPathname());
+ } else {
+ unlink($file->getPathname());
+ }
+ }
+
+ $deleted = rmdir($targetDir);
+ if (!$deleted) {
+ throw new CommandError("Could not delete existing app vendor directory.");
+ }
+ }
+
+ protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
+ {
+ $zipChecksum = hash_file('sha256', $zipPath);
+ if ($zipChecksum !== $targetChecksum) {
+ unlink($zipPath);
+ throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
+ }
+ }
+
+ protected function downloadVendorZip(string $version): string
+ {
+ $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
+ $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
+
+ $targetFile = @fopen($targetUrl, 'rb');
+ if ($targetFile === false) {
+ throw new CommandError("Failed to download ZIP file from $targetUrl");
+ }
+
+ file_put_contents($tempFile, $targetFile);
+
+ return $tempFile;
+ }
+
+ /**
+ * @throws CommandError
+ */
+ protected function getTargetChecksum(): string
+ {
+ $checksumFile = implode(DIRECTORY_SEPARATOR, [$this->appDir, 'dev', 'checksums', 'vendor']);
+ $checksum = '';
+ if (file_exists($checksumFile)) {
+ $checksum = trim(file_get_contents($checksumFile));
+ }
+
+ if (empty($checksum)) {
+ throw new CommandError("Could not find a vendor checksum for validation.");
+ }
+
+ return $checksum;
+ }
+}
diff --git a/src/app.php b/src/app.php
index cef8388..26e5c53 100644
--- a/src/app.php
+++ b/src/app.php
@@ -9,7 +9,7 @@ use Cli\Commands\RestoreCommand;
use Cli\Commands\UpdateCommand;
// Setup our CLI
-$app = new Application('bookstack-system-cli', '0.3.1');
+$app = new Application('bookstack-system-cli', '0.4.0');
$app->setCatchExceptions(false);
$app->add(new BackupCommand());
diff --git a/tests/Commands/BackupCommandTest.php b/tests/Commands/BackupCommandTest.php
index 6652731..d9aad3e 100644
--- a/tests/Commands/BackupCommandTest.php
+++ b/tests/Commands/BackupCommandTest.php
@@ -159,7 +159,7 @@ class BackupCommandTest extends TestCase
public function test_backup_using_database_credentials_with_special_chars()
{
- // The user details used here is created as part of the database docker-mysql-init.sql script.
+ // The user details used here are created as part of the database docker-mysql-init.sql script.
chdir('/var/www/bookstack');
$this->assertCount(0, glob('storage/backups/bookstack-backup-*.zip'));
diff --git a/tests/Commands/UpdateCommandTest.php b/tests/Commands/UpdateCommandTest.php
index d0d0f2e..3a7973a 100644
--- a/tests/Commands/UpdateCommandTest.php
+++ b/tests/Commands/UpdateCommandTest.php
@@ -15,25 +15,6 @@ class UpdateCommandTest extends TestCase
$result->assertStdoutContains("Your BookStack instance at [/var/www/bookstack] has been updated!");
}
- public function test_composer_gets_downloaded_locally_if_not_found()
- {
- chdir('/var/www/bookstack');
-
- rename('/usr/local/bin/composer', '/usr/local/bin/hiddencomposer');
-
- $this->assertFileDoesNotExist('/var/www/bookstack/composer');
-
- $result = $this->runCommand('update');
- $result->assertSuccessfulExit();
- $result->assertStdoutContains("Composer does not exist, downloading a local copy...");
- $result->assertStdoutContains("Your BookStack instance at [/var/www/bookstack] has been updated!");
-
- $this->assertFileExists('/var/www/bookstack/composer');
- unlink('/var/www/bookstack/composer');
-
- rename('/usr/local/bin/hiddencomposer', '/usr/local/bin/composer');
- }
-
public function test_command_rejects_on_no_instance_found()
{
chdir('/home');
@@ -65,4 +46,19 @@ class UpdateCommandTest extends TestCase
$result->assertSuccessfulExit();
$result->assertStdoutContains("Your BookStack instance at [/var/www/bookstack] has been updated!");
}
+
+ public function test_vendor_folder_gets_created()
+ {
+ $this->withOwnBookStackFolder(function (string $basePath) {
+ exec("rm -rf {$basePath}/vendor");
+
+ $this->assertDirectoryDoesNotExist("{$basePath}/vendor");
+
+ $this->runCommand('update', ['--app-directory' => $basePath])
+ ->assertSuccessfulExit();
+
+ $this->assertDirectoryExists("{$basePath}/vendor");
+ $this->assertGreaterThan(20, glob("{$basePath}/vendor/composer/*"));
+ });
+ }
} \ No newline at end of file