From 99c17e733f4fe7b8ebfa7e0688797a8fc60020d8 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 11:26:37 +0100 Subject: [PATCH 01/20] chore: update CI workflow to trigger on nightly branch for pushes and pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31cd863..c45c2c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,10 @@ name: πŸ§ͺ CI on: + push: + branches: [nightly] pull_request: + branches: [nightly] workflow_dispatch: jobs: From b5732663aeedeae5bdc005b44c9d73d37dafc659 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 11:35:59 +0100 Subject: [PATCH 02/20] temp prod CI allow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c45c2c5..79fef4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: πŸ§ͺ CI on: push: - branches: [nightly] + branches: [nightly, production] pull_request: branches: [nightly] workflow_dispatch: From 8b0a702743e4e3434adadb4f9937da77e49a6907 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 11:36:07 +0100 Subject: [PATCH 03/20] test CI for status checks From 17a4441aa8c99da8224ca0fe5156cc2b8a896b28 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 11:56:25 +0100 Subject: [PATCH 04/20] refactor: simplify CI workflow configuration and improve output messages --- .github/workflows/ci.yml | 118 +++++++++++++++++----------------- .github/workflows/promote.yml | 34 ++++++++++ 2 files changed, 93 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/promote.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79fef4c..ad7da15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,59 +1,59 @@ -name: πŸ§ͺ CI - -on: - push: - branches: [nightly, production] - pull_request: - branches: [nightly] - workflow_dispatch: - -jobs: - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - node-version: [18.x, 20.x, 22.x] - fail-fast: false - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Run tests - run: npm test - - - name: Check linting - run: npm run lint - - - name: Type check - run: npm run type-check - - ci-summary: - name: CI Summary - runs-on: ubuntu-latest - needs: test - if: always() - - steps: - - name: CI Results - run: | - if [ "${{ needs.test.result }}" == "success" ]; then - echo "βœ… All CI tests passed!" - else - echo "❌ Some CI tests failed!" - exit 1 - fi \ No newline at end of file +name: CI + +on: + push: + branches: [nightly] + pull_request: + branches: [nightly] + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [18.x, 20.x, 22.x] + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Run tests + run: npm test + + - name: Check linting + run: npm run lint + + - name: Type check + run: npm run type-check + + ci-summary: + name: CI Summary + runs-on: ubuntu-latest + needs: test + if: always() + + steps: + - name: CI Results + run: | + if [ "${{ needs.test.result }}" = "success" ]; then + echo "All CI tests passed." + else + echo "Some CI tests failed." + exit 1 + fi diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml new file mode 100644 index 0000000..55aa309 --- /dev/null +++ b/.github/workflows/promote.yml @@ -0,0 +1,34 @@ +name: Nightly Promotion + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + +jobs: + promote: + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'nightly' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch origin +refs/heads/*:refs/remotes/origin/* + + - name: Merge nightly into production + run: | + git checkout -B production origin/production + git merge --no-ff origin/nightly -m "CI: promote nightly to production" + git push origin production From 79bee975d48ba96f323cfa4872ed9cd43d909d80 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 12:03:47 +0100 Subject: [PATCH 05/20] ci: update merge strategy for promoting nightly to production --- .github/workflows/promote.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 55aa309..c05559f 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -30,5 +30,5 @@ jobs: - name: Merge nightly into production run: | git checkout -B production origin/production - git merge --no-ff origin/nightly -m "CI: promote nightly to production" + git merge -X theirs --no-edit origin/nightly -m "CI: promote nightly β†’ production" git push origin production From 3ea6fd48a385319cd47c99be8e296af6f68f975f Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 12:18:13 +0100 Subject: [PATCH 06/20] ci: refactor promotion workflow to create a dedicated branch and automate PR opening --- .github/workflows/auto-approve-promotion.yml | 98 ++++++++++++++++++++ .github/workflows/promote.yml | 55 +++++++++-- 2 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/auto-approve-promotion.yml diff --git a/.github/workflows/auto-approve-promotion.yml b/.github/workflows/auto-approve-promotion.yml new file mode 100644 index 0000000..7bc8bf1 --- /dev/null +++ b/.github/workflows/auto-approve-promotion.yml @@ -0,0 +1,98 @@ +name: Auto Approve Promotion + +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + +permissions: + pull-requests: write + contents: write + +jobs: + approve: + if: > + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'promotion/nightly' && + github.event.pull_request.base.ref == 'production' + runs-on: ubuntu-latest + + steps: + - name: Approve promotion pull request + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pull_number = context.payload.pull_request.number; + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number, + }); + + const alreadyApproved = reviews.some( + (review) => + review.user?.login === "github-actions[bot]" && + review.state === "APPROVED" + ); + + if (alreadyApproved) { + core.info(`Promotion PR #${pull_number} already approved.`); + } else { + await github.rest.pulls.createReview({ + owner, + repo, + pull_number, + event: "APPROVE", + body: "Automated approval for nightly promotion.", + }); + core.info(`Approved promotion PR #${pull_number}.`); + } + + - name: Attempt auto merge + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pull_number = context.payload.pull_request.number; + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + let pr; + for (let attempt = 0; attempt < 5; attempt += 1) { + ({ data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number, + })); + + if (pr.mergeable_state && pr.mergeable_state !== "unknown") { + break; + } + + core.info(`mergeable_state is '${pr.mergeable_state}'. Waiting before retry (${attempt + 1}/5).`); + await sleep(5000); + } + + if (pr.mergeable_state !== "clean") { + core.info( + `Promotion PR #${pull_number} not ready to merge (mergeable_state=${pr.mergeable_state}).` + ); + return; + } + + await github.rest.pulls.merge({ + owner, + repo, + pull_number, + merge_method: "merge", + }); + + core.info(`Merged promotion PR #${pull_number}.`) diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index c05559f..48adf2f 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -7,14 +7,15 @@ on: - completed jobs: - promote: + prepare-promotion-pr: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'nightly' runs-on: ubuntu-latest permissions: contents: write + pull-requests: write steps: - - name: Checkout repo + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 @@ -24,11 +25,47 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Fetch all branches - run: git fetch origin +refs/heads/*:refs/remotes/origin/* - - - name: Merge nightly into production + - name: Prepare promotion branch run: | - git checkout -B production origin/production - git merge -X theirs --no-edit origin/nightly -m "CI: promote nightly β†’ production" - git push origin production + git fetch origin production nightly + git checkout origin/nightly + git checkout -B promotion/nightly + git push origin promotion/nightly --force + + - name: Open or update promotion pull request + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const headBranch = "promotion/nightly"; + const baseBranch = "production"; + + const { data: existing } = await github.rest.pulls.list({ + owner, + repo, + state: "open", + head: `${owner}:${headBranch}`, + base: baseBranch, + }); + + if (existing.length > 0) { + core.info(`Promotion PR already open: #${existing[0].number}`); + return; + } + + const { data: pr } = await github.rest.pulls.create({ + owner, + repo, + head: headBranch, + base: baseBranch, + title: "Promote nightly to production", + body: [ + "Automated promotion from `nightly` to `production`.", + "", + "Please review and merge when ready." + ].join("\n"), + }); + + core.info(`Opened promotion PR #${pr.number}`); From acf2c82571b26fbb5f258b15017e21dbd313121c Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 12:43:42 +0100 Subject: [PATCH 07/20] ci: enhance promotion workflow by generating GitHub app token for authentication --- .github/workflows/auto-approve-promotion.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-approve-promotion.yml b/.github/workflows/auto-approve-promotion.yml index 7bc8bf1..e1a3200 100644 --- a/.github/workflows/auto-approve-promotion.yml +++ b/.github/workflows/auto-approve-promotion.yml @@ -20,10 +20,18 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate promotion app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PROMOTION_APP_ID }} + private-key: ${{ secrets.PROMOTION_APP_PRIVATE_KEY }} + installation-id: ${{ secrets.PROMOTION_APP_INSTALLATION_ID }} + - name: Approve promotion pull request uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; @@ -57,7 +65,7 @@ jobs: - name: Attempt auto merge uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; From caf6ad77f92f49d57b3c4b220e41bbdd643fc44a Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 12:52:00 +0100 Subject: [PATCH 08/20] ci: update pull request triggers to include production branch --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad7da15..518e71e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,9 @@ on: push: branches: [nightly] pull_request: - branches: [nightly] + branches: + - nightly + - production workflow_dispatch: jobs: From 9ad7646b56f4b7fb1f74173dc362a777da09f3ab Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 15:26:20 +0100 Subject: [PATCH 09/20] update package-lock.json with dependency version upgrades and removals --- package-lock.json | 246 ++++++++++++++++++++++------------------------ 1 file changed, 119 insertions(+), 127 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb76ccc..7d7f68a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -607,9 +607,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -649,9 +649,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -664,19 +664,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -711,13 +714,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -731,32 +737,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -823,6 +816,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", @@ -869,14 +871,14 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", - "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -896,14 +898,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", - "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/type": "^3.0.5", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -939,10 +941,31 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", "license": "MIT", "engines": { "node": ">=18" @@ -1111,9 +1134,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", "license": "MIT", "engines": { "node": ">=18" @@ -2343,9 +2366,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2687,9 +2710,9 @@ ] }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2985,9 +3008,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3163,9 +3186,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "license": "MIT" }, "node_modules/chokidar": { @@ -3580,20 +3603,20 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3604,9 +3627,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3641,9 +3664,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3658,9 +3681,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3671,15 +3694,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3817,20 +3840,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-content-type-parse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", @@ -4343,15 +4352,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -5855,15 +5868,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-is-promise": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", @@ -7021,9 +7025,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7118,18 +7122,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", From 12e760a66a9f3e7e38e4b3ec6214cfaa7e613a13 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 16:10:34 +0100 Subject: [PATCH 10/20] be gone emojis --- src/commands/addons/deps.ts | 60 +++++----- src/commands/addons/disable.ts | 12 +- src/commands/addons/enable.ts | 12 +- src/commands/addons/index.ts | 2 +- src/commands/addons/install-deps.ts | 36 +++--- src/commands/addons/install.ts | 26 ++--- src/commands/addons/list.ts | 22 ++-- src/commands/addons/recover.ts | 56 +++++----- src/commands/addons/uninstall.ts | 12 +- src/commands/addons/update.ts | 4 +- src/commands/addons/version.ts | 112 +++++++++---------- src/commands/build/build.ts | 30 ++--- src/commands/config/config.ts | 152 +++++++++++++------------ src/commands/index.ts | 32 +++--- src/commands/init/compiler.ts | 4 +- src/commands/init/git.ts | 10 +- src/commands/init/projectStructure.ts | 4 +- src/commands/init/prompts.ts | 2 +- src/commands/init/serverDownload.ts | 8 +- src/commands/init/setup.ts | 4 +- src/commands/kill/kill.ts | 22 ++-- src/commands/run/index.ts | 20 ++-- src/commands/setup/setup.ts | 24 ++-- src/commands/start/start.ts | 40 +++---- src/core/addons/AddonDiscovery.ts | 8 +- src/core/addons/AddonInstaller.ts | 32 +++--- src/core/addons/AddonRecovery.ts | 2 +- src/core/addons/AddonRegistry.ts | 24 ++-- src/core/addons/GitHubDownloader.ts | 2 +- src/core/addons/commandResolver.ts | 30 ++--- src/core/addons/dependencyResolver.ts | 22 ++-- src/core/addons/hooks.ts | 32 +++--- src/core/addons/loader.ts | 46 ++++---- src/core/addons/manager.ts | 88 +++++++-------- src/core/addons/semver.ts | 2 +- src/core/manifest.ts | 26 ++--- src/index.ts | 42 +++---- src/utils/commandWrapper.ts | 28 ++--- src/utils/config.ts | 4 +- src/utils/logger.ts | 154 ++++++++++++++------------ src/utils/serverState.ts | 54 ++++----- src/utils/updateChecker.ts | 6 +- tests/unit/build.test.ts | 20 ++-- tests/unit/config.test.ts | 18 +-- tests/unit/init.test.ts | 29 +++-- tests/unit/install.test.ts | 17 +-- tests/unit/kill.test.ts | 20 ++-- tests/unit/logger.test.ts | 54 +++++---- tests/unit/setup.test.ts | 22 ++-- tests/unit/start.test.ts | 25 +++-- tests/unit/uninstall.test.ts | 11 +- 51 files changed, 787 insertions(+), 737 deletions(-) diff --git a/src/commands/addons/deps.ts b/src/commands/addons/deps.ts index c86eb51..fc01f92 100644 --- a/src/commands/addons/deps.ts +++ b/src/commands/addons/deps.ts @@ -24,47 +24,47 @@ export default function(program: Command): void { if (options.check) { // Check dependency status - logger.heading(`πŸ” Dependency Status: ${addonName}`); + logger.heading(`Dependency Status: ${addonName}`); const validation = addonManager.validateDependencies(addonName); if (validation.valid) { - logger.success('βœ… All dependencies are satisfied'); + logger.success('All dependencies are satisfied'); } else { - logger.warn('⚠️ Dependency issues found:'); + logger.warn('Dependency issues found:'); for (const issue of validation.issues) { - logger.warn(` β€’ ${issue}`); + logger.warn(` - ${issue}`); } } } else if (options.resolve) { // Resolve dependency tree - logger.heading(`🌳 Dependency Tree: ${addonName}`); + logger.heading(`Dependency Tree: ${addonName}`); const resolution = await dependencyResolver.resolveDependencies(addonName); - logger.info(`πŸ“¦ Resolved addons: ${resolution.resolved.join(', ')}`); + logger.info(`Resolved addons: ${resolution.resolved.join(', ')}`); if (resolution.conflicts.length > 0) { - logger.warn('⚠️ Conflicts detected:'); + logger.warn('Conflicts detected:'); for (const conflict of resolution.conflicts) { - logger.warn(` β€’ ${conflict.addon}: ${conflict.reason}`); + logger.warn(` - ${conflict.addon}: ${conflict.reason}`); } } if (resolution.versionConflicts.length > 0) { - logger.warn('⚠️ Version conflicts detected:'); + logger.warn('Version conflicts detected:'); for (const versionConflict of resolution.versionConflicts) { - logger.warn(` β€’ ${versionConflict.addon}: ${versionConflict.reason}`); + logger.warn(` - ${versionConflict.addon}: ${versionConflict.reason}`); logger.warn(` Available: ${versionConflict.availableVersion}`); logger.warn(` Required: ${versionConflict.constraint}`); } } if (resolution.missing.length > 0) { - logger.warn('❌ Missing dependencies:'); + logger.warn('Missing dependencies:'); for (const missing of resolution.missing) { - logger.warn(` β€’ ${missing}`); + logger.warn(` - ${missing}`); } } @@ -72,7 +72,7 @@ export default function(program: Command): void { const order = dependencyResolver.getInstallationOrder(resolution); if (order.length > 0) { logger.info(''); - logger.info('πŸ“‹ Recommended installation order:'); + logger.info('Recommended installation order:'); for (let i = 0; i < order.length; i++) { logger.info(` ${i + 1}. ${order[i]}`); } @@ -82,15 +82,15 @@ export default function(program: Command): void { const suggestions = dependencyResolver.suggestSolutions(resolution); if (suggestions.length > 0) { logger.info(''); - logger.info('πŸ’‘ Suggested solutions:'); + logger.info('Suggested solutions:'); for (const suggestion of suggestions) { - logger.info(` β€’ ${suggestion}`); + logger.info(` - ${suggestion}`); } } } else if (options.validate) { // Validate all dependencies - logger.heading('πŸ” Validating All Addon Dependencies'); + logger.heading('Validating All Addon Dependencies'); const addons = await addonManager.listAddons(); let validCount = 0; @@ -100,49 +100,49 @@ export default function(program: Command): void { const validation = addonManager.validateDependencies(addon.name); if (validation.valid) { validCount++; - logger.detail(`βœ… ${addon.name}`); + logger.detail(`${addon.name}`); } else { invalidCount++; - logger.warn(`❌ ${addon.name}:`); + logger.warn(`${addon.name}:`); for (const issue of validation.issues) { - logger.warn(` β€’ ${issue}`); + logger.warn(` - ${issue}`); } } } logger.info(''); - logger.info(`πŸ“Š Validation Summary:`); - logger.info(` βœ… Valid: ${validCount} addon(s)`); - logger.info(` ❌ Invalid: ${invalidCount} addon(s)`); + logger.info('Validation summary:'); + logger.success(`Valid addons: ${validCount}`); + logger.warn(`Invalid addons: ${invalidCount}`); if (invalidCount > 0) { logger.info(''); - logger.info('πŸ’‘ Use "tapi addon deps --resolve" to get solutions'); + logger.hint('Use "tapi addon deps --resolve" to get solutions'); } } else { // Show dependency information - logger.heading(`πŸ“¦ Dependencies: ${addonName}`); + logger.heading(`Dependencies: ${addonName}`); const addonInfo = addonManager.getLoader().getAddonInfo(addonName); if (!addonInfo) { - logger.error(`❌ Addon not found: ${addonName}`); + logger.error(`Addon not found: ${addonName}`); process.exit(1); } if (addonInfo.dependencies && addonInfo.dependencies.length > 0) { - logger.info('πŸ“‹ Dependencies:'); + logger.info('Dependencies:'); for (const dep of addonInfo.dependencies) { const depInfo = addonManager.getLoader().getAddonInfo(dep); if (depInfo) { - const status = depInfo.enabled ? 'βœ…' : '❌'; + const status = depInfo.enabled ? '[ENABLED]' : '[DISABLED]'; logger.info(` ${status} ${dep} (${depInfo.version})`); } else { - logger.info(` ❓ ${dep} (not installed)`); + logger.warn(` Missing addon: ${dep}`); } } } else { - logger.info('πŸ“­ No dependencies'); + logger.info('No dependencies'); } logger.info(''); @@ -153,7 +153,7 @@ export default function(program: Command): void { } } catch (error) { - logger.error(`❌ Dependency operation failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Dependency operation failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/disable.ts b/src/commands/addons/disable.ts index ceb746e..8c7cde8 100644 --- a/src/commands/addons/disable.ts +++ b/src/commands/addons/disable.ts @@ -16,7 +16,7 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading(`⏸️ Disabling addon: ${addonName}`); + logger.heading(`Disabling addon: ${addonName}`); const addonManager = getAddonManager(); @@ -25,26 +25,26 @@ export default function(program: Command): void { const addon = installedAddons.find(addon => addon.name === addonName); if (!addon || !addon.installed) { - logger.error(`❌ Addon '${addonName}' is not installed`); + logger.error(`Addon '${addonName}' is not installed`); return; } if (!addon.enabled) { - logger.warn(`⚠️ Addon '${addonName}' is already disabled`); + logger.warn(`Addon '${addonName}' is already disabled`); return; } // Disable the addon - logger.info('πŸ”„ Disabling addon...'); + logger.working('Disabling addon'); await addonManager.disableAddon(addonName); - logger.info('βœ… Addon disabled successfully!'); + logger.success('Addon disabled successfully!'); logger.info(''); logger.info('The addon is now inactive and will not run its hooks.'); logger.info('Use "tapi addon enable" to re-enable it later.'); } catch (error) { - logger.error(`❌ Failed to disable addon: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to disable addon: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/enable.ts b/src/commands/addons/enable.ts index fe5571f..3eba9da 100644 --- a/src/commands/addons/enable.ts +++ b/src/commands/addons/enable.ts @@ -16,7 +16,7 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading(`βœ… Enabling addon: ${addonName}`); + logger.heading(`Enabling addon: ${addonName}`); const addonManager = getAddonManager(); @@ -25,26 +25,26 @@ export default function(program: Command): void { const addon = installedAddons.find(addon => addon.name === addonName); if (!addon || !addon.installed) { - logger.error(`❌ Addon '${addonName}' is not installed`); + logger.error(`Addon '${addonName}' is not installed`); logger.info(`Run 'tapi addon install ${addonName}' to install it first`); return; } if (addon.enabled) { - logger.warn(`⚠️ Addon '${addonName}' is already enabled`); + logger.warn(`Addon '${addonName}' is already enabled`); return; } // Enable the addon - logger.info('πŸ”„ Enabling addon...'); + logger.working('Enabling addon'); await addonManager.enableAddon(addonName); - logger.info('βœ… Addon enabled successfully!'); + logger.success('Addon enabled successfully!'); logger.info(''); logger.info('The addon is now active and will run its hooks during project operations.'); } catch (error) { - logger.error(`❌ Failed to enable addon: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to enable addon: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/index.ts b/src/commands/addons/index.ts index e482a42..c21c3c9 100644 --- a/src/commands/addons/index.ts +++ b/src/commands/addons/index.ts @@ -57,7 +57,7 @@ export default function(program: Command): void { // Show help if no subcommand provided addonCommand.action(() => { showBanner(false); - logger.info('πŸ“¦ Addon management commands:'); + logger.info('Addon management commands:'); logger.info(''); logger.info(' install Install an addon'); logger.info(' uninstall Remove an addon'); diff --git a/src/commands/addons/install-deps.ts b/src/commands/addons/install-deps.ts index fa2ddd0..01989f9 100644 --- a/src/commands/addons/install-deps.ts +++ b/src/commands/addons/install-deps.ts @@ -18,30 +18,30 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading(`πŸ“¦ Installing Dependencies: ${addonName}`); + logger.heading(`Installing Dependencies: ${addonName}`); const addonManager = getAddonManager(); const dependencyResolver = addonManager.getDependencyResolver(); // Resolve dependencies - logger.info('πŸ” Resolving dependencies...'); + logger.working('Resolving dependencies'); const resolution = await dependencyResolver.resolveDependencies(addonName); if (resolution.missing.length === 0) { - logger.success('βœ… All dependencies are already installed'); + logger.success('All dependencies are already installed'); return; } - logger.info(`πŸ“‹ Missing dependencies: ${resolution.missing.join(', ')}`); + logger.info(`Missing dependencies: ${resolution.missing.join(', ')}`); if (options.dryRun) { logger.info(''); - logger.info('πŸ” Dry run - would install:'); + logger.info('Dry run - would install:'); for (const dep of resolution.missing) { - logger.info(` β€’ ${dep}`); + logger.info(` - ${dep}`); } logger.info(''); - logger.info('πŸ’‘ Remove --dry-run flag to actually install'); + logger.info('Remove --dry-run flag to actually install'); return; } @@ -53,44 +53,44 @@ export default function(program: Command): void { // Report results logger.info(''); if (result.installed.length > 0) { - logger.success(`βœ… Successfully installed ${result.installed.length} dependencies:`); + logger.success(`Successfully installed ${result.installed.length} dependencies:`); for (const dep of result.installed) { - logger.info(` β€’ ${dep}`); + logger.info(` - ${dep}`); } } if (result.failed.length > 0) { - logger.warn(`⚠️ Failed to install ${result.failed.length} dependencies:`); + logger.warn(`Failed to install ${result.failed.length} dependencies:`); for (const dep of result.failed) { - logger.warn(` β€’ ${dep}`); + logger.warn(` - ${dep}`); } } // Check for remaining conflicts if (resolution.conflicts.length > 0) { logger.warn(''); - logger.warn('⚠️ Dependency conflicts remain:'); + logger.warn('Dependency conflicts remain:'); for (const conflict of resolution.conflicts) { - logger.warn(` β€’ ${conflict.addon}: ${conflict.reason}`); + logger.warn(` - ${conflict.addon}: ${conflict.reason}`); } } // Final validation logger.info(''); - logger.info('πŸ” Final dependency validation...'); + logger.working('Final dependency validation'); const validation = addonManager.validateDependencies(addonName); if (validation.valid) { - logger.success('βœ… All dependencies are now satisfied!'); + logger.success('All dependencies are now satisfied!'); } else { - logger.warn('⚠️ Some dependency issues remain:'); + logger.warn('Some dependency issues remain:'); for (const issue of validation.issues) { - logger.warn(` β€’ ${issue}`); + logger.warn(` - ${issue}`); } } } catch (error) { - logger.error(`❌ Dependency installation failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Dependency installation failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/install.ts b/src/commands/addons/install.ts index cc2e1ec..6950abb 100644 --- a/src/commands/addons/install.ts +++ b/src/commands/addons/install.ts @@ -21,7 +21,7 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading(`πŸ“¦ Installing addon: ${addonName}`); + logger.heading(`Installing addon: ${addonName}`); const addonManager = getAddonManager(); @@ -35,7 +35,7 @@ export default function(program: Command): void { source = 'local'; installPath = options.local; } else if (!source) { - logger.error('❌ No source specified. Use --github or --local to specify installation source.'); + logger.error('No source specified. Use --github or --local to specify installation source.'); logger.info(''); logger.info('Examples:'); logger.info(' tapi addon install my-addon --github user/repo'); @@ -47,13 +47,13 @@ export default function(program: Command): void { installPath = addonName; } - logger.info(`πŸ“‘ Source: ${source}`); - logger.info(`πŸ“ Path: ${installPath}`); + logger.info(`Source: ${source}`); + logger.info(`Path: ${installPath}`); if (options.global) { - logger.info(`πŸ“¦ Installing to: ~/.tapi/addons/ (global)`); + logger.info(`Installing to: ~/.tapi/addons/ (global)`); } else { - logger.info(`πŸ“¦ Installing to: ./.tapi/addons/ (project-local)`); + logger.info(`Installing to: ./.tapi/addons/ (project-local)`); } const installedAddons = await addonManager.listAddons(); @@ -61,13 +61,13 @@ export default function(program: Command): void { if (existingAddon) { if (existingAddon.installed) { - logger.warn(`⚠️ Addon '${addonName}' is already installed`); + logger.warn(`Addon '${addonName}' is already installed`); logger.info('Use --force to reinstall'); return; } } - logger.info('⬇️ Downloading and installing addon...'); + logger.working('Downloading and installing addon'); await addonManager.installAddon(addonName, { source, path: installPath, @@ -75,15 +75,15 @@ export default function(program: Command): void { autoDeps: options.autoDeps || false }); - logger.info('βœ… Addon installed successfully!'); + logger.success('Addon installed successfully!'); logger.info(''); logger.info('Next steps:'); - logger.info(` β€’ Run 'tapi addon list' to see installed addons`); - logger.info(` β€’ Run 'tapi addon enable ${addonName}' to activate it`); - logger.info(` β€’ Check the addon documentation for usage instructions`); + logger.info(` - Run 'tapi addon list' to see installed addons`); + logger.info(` - Run 'tapi addon enable ${addonName}' to activate it`); + logger.info(` - Check the addon documentation for usage instructions`); } catch (error) { - logger.error(`❌ Failed to install addon: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to install addon: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/list.ts b/src/commands/addons/list.ts index ed9150f..1f75aa6 100644 --- a/src/commands/addons/list.ts +++ b/src/commands/addons/list.ts @@ -22,14 +22,14 @@ export default function(program: Command): void { const addonManager = getAddonManager(); if (options.all) { - logger.heading('πŸ“¦ All available addons:'); + logger.heading('All available addons:'); // Show installed addons const installedAddons = await addonManager.listAddons(); if (installedAddons.length > 0) { logger.info('Installed:'); for (const addon of installedAddons) { - const status = addon.enabled ? 'βœ…' : '⏸️'; + const status = addon.enabled ? '[ENABLED]' : '[DISABLED]'; const version = addon.version ? ` v${addon.version}` : ''; logger.info(` ${status} ${addon.name}${version} - ${addon.description}`); } @@ -42,18 +42,18 @@ export default function(program: Command): void { const availableAddons = await addonManager.searchAddons(''); if (availableAddons.length > 0) { for (const addon of availableAddons) { - logger.info(` πŸ“¦ ${addon.name} - ${addon.description || 'No description available'}`); + logger.info(` - ${addon.name} - ${addon.description || 'No description available'}`); } } else { logger.info(' No addons available in registry'); } } catch (error) { - logger.warn(`⚠️ Could not load available addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.warn(`Could not load available addons: ${error instanceof Error ? error.message : 'unknown error'}`); logger.info(' Run "tapi addon install " to install specific addons'); } } else { - logger.heading('πŸ“¦ Installed addons:'); + logger.heading('Installed addons:'); const installedAddons = await addonManager.listAddons(); @@ -74,11 +74,11 @@ export default function(program: Command): void { // Display addons for (const addon of filteredAddons) { - const status = addon.enabled ? 'βœ… Enabled' : '⏸️ Disabled'; + const status = addon.enabled ? 'Enabled' : 'Disabled'; const version = addon.version ? ` v${addon.version}` : ''; - logger.info(`πŸ“¦ ${addon.name}${version}`); - logger.info(` ${status}`); + logger.info(`${addon.name}${version}`); + logger.info(` Status: ${status}`); logger.info(` ${addon.description}`); if (addon.author) { @@ -97,7 +97,7 @@ export default function(program: Command): void { // Show command conflicts if any const hasConflicts = addonManager.hasCommandConflicts(); if (hasConflicts) { - logger.warn('\n⚠️ Command conflicts detected:'); + logger.warn('\nCommand conflicts detected:'); const conflicts = addonManager.getCommandConflicts(); for (const [commandName, conflictingAddons] of conflicts) { logger.warn(` ${commandName}: ${conflictingAddons.length} addons trying to override`); @@ -110,7 +110,7 @@ export default function(program: Command): void { // Show command statistics const stats = addonManager.getCommandStats(); if (stats.totalAddonCommands > 0) { - logger.info(`\nπŸ“Š Command Statistics:`); + logger.info(`\nCommand statistics:`); logger.info(` Total addon commands: ${stats.totalAddonCommands}`); logger.info(` Overridden commands: ${stats.overriddenCommands.length}`); logger.info(` New commands: ${stats.newCommands.length}`); @@ -121,7 +121,7 @@ export default function(program: Command): void { } } catch (error) { - logger.error(`❌ Failed to list addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to list addons: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/recover.ts b/src/commands/addons/recover.ts index d486d9a..5fa721f 100644 --- a/src/commands/addons/recover.ts +++ b/src/commands/addons/recover.ts @@ -18,32 +18,32 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading('πŸ”§ Addon Recovery Tool'); + logger.heading('Addon Recovery Tool'); const addonManager = getAddonManager(); if (options.clearErrors) { // Clear all error records - logger.info('🧹 Clearing all addon error records...'); + logger.info('Clearing all addon error records...'); const allErrors = addonManager.getAllAddonErrors(); for (const addonName of allErrors.keys()) { addonManager.clearAddonErrors(addonName); } - logger.success('βœ… Cleared all error records'); + logger.success('Cleared all error records'); return; } if (options.all) { // Attempt to recover all failed addons - logger.info('πŸ”„ Attempting to recover all failed addons...'); + logger.info('Attempting to recover all failed addons...'); const allErrors = addonManager.getAllAddonErrors(); const failedAddons = Array.from(allErrors.keys()); if (failedAddons.length === 0) { - logger.info('βœ… No failed addons found'); + logger.info('No failed addons found'); return; } @@ -54,7 +54,7 @@ export default function(program: Command): void { for (const addonName of failedAddons) { try { - logger.info(`πŸ”„ Attempting to recover: ${addonName}`); + logger.info(`Attempting to recover: ${addonName}`); // Try to enable the addon (this will trigger a reload) await addonManager.enableAddon(addonName); @@ -63,34 +63,34 @@ export default function(program: Command): void { addonManager.clearAddonErrors(addonName); recovered++; - logger.success(`βœ… Recovered: ${addonName}`); + logger.success(`Recovered: ${addonName}`); } catch (error) { stillFailed++; - logger.warn(`⚠️ Still failing: ${addonName} (${error instanceof Error ? error.message : 'unknown error'})`); + logger.warn(`Still failing: ${addonName} (${error instanceof Error ? error.message : 'unknown error'})`); } } - logger.info(''); - logger.info(`πŸ“Š Recovery Summary:`); - logger.info(` βœ… Recovered: ${recovered} addon(s)`); - logger.info(` ⚠️ Still failing: ${stillFailed} addon(s)`); + logger.info(''); + logger.info('Recovery summary:'); + logger.success(`Recovered: ${recovered} addon(s)`); + logger.warn(`Still failing: ${stillFailed} addon(s)`); if (stillFailed > 0) { logger.info(''); - logger.info('πŸ’‘ For persistent failures:'); - logger.info(' β€’ Check addon documentation for requirements'); - logger.info(' β€’ Try reinstalling: tapi addon uninstall && tapi addon install '); - logger.info(' β€’ Report issues to addon maintainers'); + logger.info('For persistent failures:'); + logger.info(' - Check addon documentation for requirements'); + logger.info(' - Try reinstalling: tapi addon uninstall && tapi addon install '); + logger.info(' - Report issues to addon maintainers'); } } else { // Show recovery options - logger.info('πŸ” Addon Error Analysis'); + logger.info('Addon Error Analysis'); const allErrors = addonManager.getAllAddonErrors(); if (allErrors.size === 0) { - logger.info('βœ… No addon errors found'); + logger.info('No addon errors found'); logger.info(''); logger.info('Your addon system is healthy!'); return; @@ -100,22 +100,22 @@ export default function(program: Command): void { logger.info(''); for (const [addonName, errors] of allErrors) { - logger.info(`πŸ“¦ ${addonName}:`); - for (const error of errors) { - logger.info(` ❌ ${error}`); - } + logger.info(`${addonName}:`); + for (const error of errors) { + logger.info(` - ${error}`); + } logger.info(''); } - logger.info('πŸ”§ Recovery Options:'); - logger.info(' β€’ Recover all: tapi addon recover --all'); - logger.info(' β€’ Clear errors: tapi addon recover --clear-errors'); - logger.info(' β€’ Disable problematic addons: tapi addon disable '); - logger.info(' β€’ Reinstall addons: tapi addon uninstall && tapi addon install '); + logger.info('Recovery Options:'); + logger.info(' - Recover all: tapi addon recover --all'); + logger.info(' - Clear errors: tapi addon recover --clear-errors'); + logger.info(' - Disable problematic addons: tapi addon disable '); + logger.info(' - Reinstall addons: tapi addon uninstall && tapi addon install '); } } catch (error) { - logger.error(`❌ Recovery failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Recovery failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/uninstall.ts b/src/commands/addons/uninstall.ts index f9acb3c..d6316f6 100644 --- a/src/commands/addons/uninstall.ts +++ b/src/commands/addons/uninstall.ts @@ -17,7 +17,7 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading(`πŸ—‘οΈ Uninstalling addon: ${addonName}`); + logger.heading(`Uninstalling addon: ${addonName}`); const addonManager = getAddonManager(); @@ -26,28 +26,28 @@ export default function(program: Command): void { const addon = installedAddons.find(addon => addon.name === addonName); if (!addon || !addon.installed) { - logger.error(`❌ Addon '${addonName}' is not installed`); + logger.error(`Addon '${addonName}' is not installed`); return; } // Confirm removal unless --force is used if (!options.force) { - logger.warn(`⚠️ This will permanently remove the addon '${addonName}'`); + logger.warn(`This will permanently remove the addon '${addonName}'`); logger.info('Use --force to skip confirmation'); return; } // Uninstall the addon - logger.info('πŸ—‘οΈ Removing addon...'); + logger.info('Removing addon...'); await addonManager.uninstallAddon(addonName); - logger.info('βœ… Addon uninstalled successfully!'); + logger.info('Addon uninstalled successfully!'); logger.info(''); logger.info('Note: This addon has been removed from your project.'); logger.info('Any configuration it added to pawn.json may need manual cleanup.'); } catch (error) { - logger.error(`❌ Failed to uninstall addon: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to uninstall addon: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/update.ts b/src/commands/addons/update.ts index fe85b0b..446fad5 100644 --- a/src/commands/addons/update.ts +++ b/src/commands/addons/update.ts @@ -27,7 +27,7 @@ export default function(program: Command): void { await addonManager.updateAddon(addonName); } else { // No addon specified - logger.error('❌ Please specify an addon name or use --all'); + logger.error('Please specify an addon name or use --all'); logger.info(''); logger.info('Examples:'); logger.info(' tapi addon update linter'); @@ -36,7 +36,7 @@ export default function(program: Command): void { } } catch (error) { - logger.error(`❌ Update failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Update failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/addons/version.ts b/src/commands/addons/version.ts index 4d8cac1..392ddca 100644 --- a/src/commands/addons/version.ts +++ b/src/commands/addons/version.ts @@ -23,56 +23,56 @@ export default function(program: Command): void { if (options.check) { // Parse check arguments const args = options.check.split(' '); - if (args.length !== 2) { - logger.error('❌ Usage: --check '); - process.exit(1); - } - - const [version, constraint] = args; - - logger.heading(`πŸ” Version Constraint Check`); + if (args.length !== 2) { + logger.error('Usage: --check '); + process.exit(1); + } + + const [version, constraint] = args; + + logger.heading('Version Constraint Check'); logger.info(`Version: ${version}`); logger.info(`Constraint: ${constraint}`); - if (!SemVer.isValidVersion(version)) { - logger.error(`❌ Invalid version format: ${version}`); - process.exit(1); - } - - if (!SemVer.isValidConstraint(constraint)) { - logger.error(`❌ Invalid constraint format: ${constraint}`); - process.exit(1); - } + if (!SemVer.isValidVersion(version)) { + logger.error(`Invalid version format: ${version}`); + process.exit(1); + } + + if (!SemVer.isValidConstraint(constraint)) { + logger.error(`Invalid constraint format: ${constraint}`); + process.exit(1); + } const satisfies = SemVer.satisfies(version, constraint); - if (satisfies) { - logger.success(`βœ… Version ${version} satisfies constraint ${constraint}`); - } else { - logger.warn(`❌ Version ${version} does NOT satisfy constraint ${constraint}`); - } + if (satisfies) { + logger.success(`Version ${version} satisfies constraint ${constraint}`); + } else { + logger.warn(`Version ${version} does NOT satisfy constraint ${constraint}`); + } } else if (options.compare) { // Parse compare arguments const args = options.compare.split(' '); - if (args.length !== 2) { - logger.error('❌ Usage: --compare '); - process.exit(1); - } + if (args.length !== 2) { + logger.error('Usage: --compare '); + process.exit(1); + } const [version1, version2] = args; - logger.heading(`βš–οΈ Version Comparison`); - - if (!SemVer.isValidVersion(version1)) { - logger.error(`❌ Invalid version format: ${version1}`); - process.exit(1); - } + logger.heading('Version Comparison'); - if (!SemVer.isValidVersion(version2)) { - logger.error(`❌ Invalid version format: ${version2}`); - process.exit(1); - } + if (!SemVer.isValidVersion(version1)) { + logger.error(`Invalid version format: ${version1}`); + process.exit(1); + } + + if (!SemVer.isValidVersion(version2)) { + logger.error(`Invalid version format: ${version2}`); + process.exit(1); + } const comparison = SemVer.compare(version1, version2); @@ -89,37 +89,37 @@ export default function(program: Command): void { } else if (options.validate) { // Validate version format - logger.heading(`βœ… Version Validation`); + logger.heading('Version Validation'); const isValid = SemVer.isValidVersion(options.validate); - if (isValid) { - logger.success(`βœ… Valid version format: ${options.validate}`); - } else { - logger.error(`❌ Invalid version format: ${options.validate}`); - logger.info('Expected format: major.minor.patch[-prerelease][+build]'); - logger.info('Examples: 1.0.0, 2.1.3-beta.1, 3.0.0-alpha.1+build.123'); - } + if (isValid) { + logger.success(`Valid version format: ${options.validate}`); + } else { + logger.error(`Invalid version format: ${options.validate}`); + logger.info('Expected format: major.minor.patch[-prerelease][+build]'); + logger.info('Examples: 1.0.0, 2.1.3-beta.1, 3.0.0-alpha.1+build.123'); + } } else if (options.constraint) { // Validate constraint format - logger.heading(`βœ… Constraint Validation`); + logger.heading('Constraint Validation'); const isValid = SemVer.isValidConstraint(options.constraint); - if (isValid) { - logger.success(`βœ… Valid constraint format: ${options.constraint}`); - logger.info('Supported operators: ^, ~, >=, <=, >, <, ='); - logger.info('Examples: ^1.0.0, ~2.1.0, >=1.0.0 <2.0.0'); - } else { - logger.error(`❌ Invalid constraint format: ${options.constraint}`); - logger.info('Supported operators: ^, ~, >=, <=, >, <, ='); - logger.info('Examples: ^1.0.0, ~2.1.0, >=1.0.0 <2.0.0'); - } + if (isValid) { + logger.success(`Valid constraint format: ${options.constraint}`); + logger.info('Supported operators: ^, ~, >=, <=, >, <, ='); + logger.info('Examples: ^1.0.0, ~2.1.0, >=1.0.0 <2.0.0'); + } else { + logger.error(`Invalid constraint format: ${options.constraint}`); + logger.info('Supported operators: ^, ~, >=, <=, >, <, ='); + logger.info('Examples: ^1.0.0, ~2.1.0, >=1.0.0 <2.0.0'); + } } else { // Show help - logger.heading('πŸ“‹ Semantic Versioning Utilities'); + logger.heading('Semantic Versioning Utilities'); logger.info(''); logger.info('Commands:'); logger.info(' --check Check if version satisfies constraint'); @@ -145,7 +145,7 @@ export default function(program: Command): void { } } catch (error) { - logger.error(`❌ Version operation failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Version operation failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/build/build.ts b/src/commands/build/build.ts index 3553f97..6327cca 100644 --- a/src/commands/build/build.ts +++ b/src/commands/build/build.ts @@ -66,11 +66,11 @@ export default function(program: Command): void { showBanner(false); try { - logger.heading('πŸ”¨ Building PAWN project...'); + logger.heading('Building PAWN project...'); const manifestPath = path.join(process.cwd(), '.tapi', 'pawn.json'); if (!fs.existsSync(manifestPath)) { - logger.error('❌ No pawn.json manifest found. Run "tapi init" first.'); + logger.error('No pawn.json manifest found. Run "tapi init" first.'); process.exit(1); } @@ -78,7 +78,7 @@ export default function(program: Command): void { // Handle list profiles option if (options.listProfiles) { - logger.info('πŸ“‹ Available build profiles:'); + logger.info('Available build profiles:'); if (manifest.compiler?.profiles) { for (const [name, profile] of Object.entries(manifest.compiler.profiles)) { logger.info(` ${name}: ${profile.description || 'No description'}`); @@ -102,13 +102,13 @@ export default function(program: Command): void { let compilerConfig = manifest.compiler; if (options.profile && manifest.compiler?.profiles?.[options.profile]) { const profile = manifest.compiler.profiles[options.profile]; - logger.info(`πŸ“‹ Using build profile: ${options.profile}`); + logger.info(`Using build profile: ${options.profile}`); if (profile.description) { logger.info(` ${profile.description}`); } compilerConfig = mergeBuildProfile(manifest.compiler, profile); } else if (options.profile) { - logger.error(`❌ Build profile '${options.profile}' not found in pawn.json`); + logger.error(`Build profile '${options.profile}' not found in pawn.json`); logger.info('Available profiles:'); if (manifest.compiler?.profiles) { for (const [name, profile] of Object.entries(manifest.compiler.profiles)) { @@ -124,12 +124,12 @@ export default function(program: Command): void { const outputFile = options.output || compilerConfig?.output || manifest.output; if (!inputFile) { - logger.error('❌ No input file specified. Use --input or define entry in pawn.json'); + logger.error('No input file specified. Use --input or define entry in pawn.json'); process.exit(1); } if (!fs.existsSync(inputFile)) { - logger.error(`❌ Input file not found: ${inputFile}`); + logger.error(`Input file not found: ${inputFile}`); process.exit(1); } @@ -237,12 +237,12 @@ export default function(program: Command): void { } if (!compilerPath) { - logger.error('❌ Could not find pawncc compiler. Make sure it\'s in pawno, qawno, or compiler directory.'); + logger.error('Could not find pawncc compiler. Make sure it\'s in pawno, qawno, or compiler directory.'); process.exit(1); } - logger.routine(`πŸ”§ Using compiler: ${compilerPath}`); - logger.info(`πŸ“ Compiling: ${inputFile} β†’ ${outputFile || 'default output'}`); + logger.routine(`Using compiler: ${compilerPath}`); + logger.info(`Compiling: ${inputFile} -> ${outputFile || 'default output'}`); if (logger.getVerbosity() === 'verbose') { logger.detail('Compiler arguments:'); @@ -331,7 +331,7 @@ export default function(program: Command): void { if (success) { logger.newline(); - logger.finalSuccess('βœ… Compilation successful!'); + logger.finalSuccess('Compilation successful!'); const successMatch = output.match( /Code\s*:\s*(\d+)\s*bytes\nData\s*:\s*(\d+)\s*bytes\nStack\/Heap\s*:\s*(\d+)\s*bytes\nEstimated usage\s*:\s*(\d+)\s*cells\nTotal requirements\s*:\s*(\d+)\s*bytes/ @@ -339,7 +339,7 @@ export default function(program: Command): void { if (successMatch) { logger.newline(); - logger.subheading('πŸ“Š Compilation statistics:'); + logger.subheading('Compilation statistics:'); logger.keyValue('File', `${path.basename(inputFile || '')} (${successMatch[5]} bytes)`); logger.keyValue('Code', `${successMatch[1]} bytes`); logger.keyValue('Data', `${successMatch[2]} bytes`); @@ -350,18 +350,18 @@ export default function(program: Command): void { process.exit(0); } else { logger.newline(); - logger.error('❌ Compilation failed!'); + logger.error('Compilation failed!'); process.exit(1); } }); compiler.on('error', (error) => { - logger.error(`❌ Failed to start compiler: ${error.message}`); + logger.error(`Failed to start compiler: ${error.message}`); process.exit(1); }); } catch (error) { - logger.error(`❌ Build error: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Build error: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index da193fc..afbf9c6 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -8,15 +8,15 @@ import { showBanner } from '../../utils/banner'; * Display current tapi configuration in a human-readable format. */ async function showCurrentConfig(): Promise { - logger.info('βš™οΈ Current tapi configuration:'); + logger.info('Current tapi configuration:'); const config = configManager.getFullConfig(); - logger.plain(` β€’ Default author: ${config.defaultAuthor || '(not set)'}`); - logger.plain(` β€’ Preferred editor: ${config.editor || '(not set)'}`); + logger.plain(` - Default author: ${config.defaultAuthor || '(not set)'}`); + logger.plain(` - Preferred editor: ${config.editor || '(not set)'}`); logger.plain( - ` β€’ GitHub integration: ${config.githubToken ? 'Configured' : 'Not configured'}` + ` - GitHub integration: ${config.githubToken ? 'Configured' : 'Not configured'}` ); - logger.plain(` β€’ Setup complete: ${config.setupComplete ? 'Yes' : 'No'}\n`); + logger.plain(` - Setup complete: ${config.setupComplete ? 'Yes' : 'No'}\n`); } /** @@ -31,7 +31,7 @@ async function configureAuthor(): Promise { }); configManager.setDefaultAuthor(author); - logger.success(`βœ… Default author updated to: ${author}`); + logger.success(`Default author updated to: ${author}`); } /** @@ -51,7 +51,7 @@ async function configureEditor(): Promise { })) as 'VS Code' | 'Sublime Text' | 'Other/None'; configManager.setEditor(editor); - logger.success(`βœ… Preferred editor updated to: ${editor}`); + logger.success(`Preferred editor updated to: ${editor}`); } /** @@ -78,30 +78,30 @@ async function configureGitHub(): Promise { if (token) { configManager.setGitHubToken(token); - logger.success('βœ… GitHub token updated successfully'); - } else { - logger.info('ℹ️ GitHub token update cancelled'); - } - } else if (action === 'remove') { - configManager.setGitHubToken(''); - logger.success('βœ… GitHub token removed'); - } else { - logger.info('ℹ️ GitHub token unchanged'); - } - } else { + logger.success('GitHub token updated successfully'); + } else { + logger.info('GitHub token update cancelled'); + } + } else if (action === 'remove') { + configManager.setGitHubToken(''); + logger.success('GitHub token removed'); + } else { + logger.info('GitHub token unchanged'); + } + } else { const token = await input({ message: 'Enter your GitHub personal access token (optional, press Enter to skip):', default: '', }); - if (token) { - configManager.setGitHubToken(token); - logger.success('βœ… GitHub token configured successfully'); - } else { - logger.info('ℹ️ GitHub token configuration skipped'); - } - } + if (token) { + configManager.setGitHubToken(token); + logger.success('GitHub token configured successfully'); + } else { + logger.info('GitHub token configuration skipped'); + } + } } /** @@ -114,15 +114,13 @@ async function resetConfiguration(): Promise { default: '', }); - if (confirm.toLowerCase() === 'confirm') { - configManager.reset(); - logger.success('βœ… Configuration reset to defaults'); - logger.info( - 'ℹ️ You will need to run "tapi setup" again before using tapi' - ); - } else { - logger.info('ℹ️ Configuration reset cancelled'); - } + if (confirm.toLowerCase() === 'confirm') { + configManager.reset(); + logger.success('Configuration reset to defaults'); + logger.info('You will need to run "tapi setup" again before using tapi'); + } else { + logger.info('Configuration reset cancelled'); + } } /** @@ -156,9 +154,9 @@ async function interactiveConfig(): Promise { case 'reset': await resetConfiguration(); return; // exit after reset - case 'exit': - logger.info('βœ… Configuration complete!'); - return; + case 'exit': + logger.success('Configuration complete!'); + return; } logger.plain(''); // add spacing between iterations @@ -200,50 +198,50 @@ export default function (program: Command): void { if (options.author !== undefined) { if (options.author === true) { // --author flag without value, prompt for it - await configureAuthor(); - } else { - // --author with value - configManager.setDefaultAuthor(options.author); - logger.success(`βœ… Default author set to: ${options.author}`); - } - return; - } + await configureAuthor(); + } else { + // --author with value + configManager.setDefaultAuthor(options.author); + logger.success(`Default author set to: ${options.author}`); + } + return; + } if (options.editor) { const validEditors = ['VS Code', 'Sublime Text', 'Other/None']; if (validEditors.includes(options.editor)) { - configManager.setEditor( - options.editor as 'VS Code' | 'Sublime Text' | 'Other/None' - ); - logger.success(`βœ… Preferred editor set to: ${options.editor}`); - } else { - logger.error( - `❌ Invalid editor. Valid options: ${validEditors.join(', ')}` - ); - process.exit(1); - } - return; - } - - if (options.githubToken !== undefined) { - if (options.githubToken === true) { - // --github-token flag without value, prompt for it - await configureGitHub(); - } else { - // --github-token with value - configManager.setGitHubToken(options.githubToken); - logger.success('βœ… GitHub token configured successfully'); - } - return; - } + configManager.setEditor( + options.editor as 'VS Code' | 'Sublime Text' | 'Other/None' + ); + logger.success(`Preferred editor set to: ${options.editor}`); + } else { + logger.error( + `Invalid editor. Valid options: ${validEditors.join(', ')}` + ); + process.exit(1); + } + return; + } + + if (options.githubToken !== undefined) { + if (options.githubToken === true) { + // --github-token flag without value, prompt for it + await configureGitHub(); + } else { + // --github-token with value + configManager.setGitHubToken(options.githubToken); + logger.success('GitHub token configured successfully'); + } + return; + } // no options provided, start interactive mode await interactiveConfig(); - } catch (error) { - logger.error( - `❌ Configuration failed: ${error instanceof Error ? error.message : 'unknown error'}` - ); - process.exit(1); - } - }); -} + } catch (error) { + logger.error( + `Configuration failed: ${error instanceof Error ? error.message : 'unknown error'}` + ); + process.exit(1); + } + }); +} diff --git a/src/commands/index.ts b/src/commands/index.ts index d062050..9d84775 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -20,61 +20,61 @@ import addonsCommand from './addons/index'; * @param program - Commander root program to extend. */ export function registerCommands(program: Command): void { - logger.detail('πŸ”§ Registering commands...'); + logger.detail('Registering commands...'); try { // First, initialize addon manager and load addons // This allows addons to override built-in commands - logger.detail('πŸ“¦ Initializing addon system...'); + logger.detail('Initializing addon system...'); const { getAddonManager } = require('../core/addons'); const addonManager = getAddonManager(program); // Initialize addons (this will register any addon commands) addonManager.initializeAddons().catch(() => { // Silently fail addon initialization - not critical for basic functionality - logger.detail('⚠️ Addon initialization failed, continuing without addons'); + logger.detail('Addon initialization failed, continuing without addons'); }); // Register all built-in commands statically setupCommand(program); - logger.detail('βœ… Registered setup command'); + logger.detail('Registered setup command'); buildCommand(program); - logger.detail('βœ… Registered build command'); + logger.detail('Registered build command'); configCommand(program); - logger.detail('βœ… Registered config command'); + logger.detail('Registered config command'); initCommand(program); - logger.detail('βœ… Registered init command'); + logger.detail('Registered init command'); installCommand(program); - logger.detail('βœ… Registered install command'); + logger.detail('Registered install command'); killCommand(program); - logger.detail('βœ… Registered kill command'); + logger.detail('Registered kill command'); runCommand(program); - logger.detail('βœ… Registered run command'); + logger.detail('Registered run command'); startCommand(program); - logger.detail('βœ… Registered start command'); + logger.detail('Registered start command'); program.addCommand(createUninstallCommand()); - logger.detail('βœ… Registered uninstall command'); + logger.detail('Registered uninstall command'); updateCommand(program); - logger.detail('βœ… Registered update command'); + logger.detail('Registered update command'); addonsCommand(program); - logger.detail('βœ… Registered addons command'); + logger.detail('Registered addons command'); // Now register any addon commands that were loaded addonManager.registerAddonCommands(); - logger.detail(`🎯 Total commands registered: ${program.commands.length}`); + logger.detail(`Total commands registered: ${program.commands.length}`); } catch (error) { - logger.error(`❌ Command registration failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Command registration failed: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } diff --git a/src/commands/init/compiler.ts b/src/commands/init/compiler.ts index 92b3b92..92e2cf9 100644 --- a/src/commands/init/compiler.ts +++ b/src/commands/init/compiler.ts @@ -21,7 +21,7 @@ export async function setupCompiler( compilerAnswers.downgradeQawno || false ); if (logger.getVerbosity() !== 'quiet') { - logger.success('βœ… Compiler installed'); + logger.success('Compiler installed'); } } catch { // error handled within download function @@ -32,7 +32,7 @@ export async function setupCompiler( try { await downloadopenmpStdLib(); if (logger.getVerbosity() !== 'quiet') { - logger.success('βœ… Standard library installed'); + logger.success('Standard library installed'); } } catch { // error handled within download function diff --git a/src/commands/init/git.ts b/src/commands/init/git.ts index 52fc3ff..f52754a 100644 --- a/src/commands/init/git.ts +++ b/src/commands/init/git.ts @@ -50,23 +50,23 @@ export async function initGitRepository(): Promise { logger.routine('Created initial Git commit'); } catch (commitError) { logger.warn( - '⚠️ Could not create initial commit. You may need to commit the changes manually.' + 'Could not create initial commit. You may need to commit the changes manually.' ); logger.warn( - `⚠️ Git commit error: ${commitError instanceof Error ? commitError.message : 'unknown error'}` + `Git commit error: ${commitError instanceof Error ? commitError.message : 'unknown error'}` ); } } catch (gitignoreError) { logger.warn( - `⚠️ Could not create .gitignore file: ${gitignoreError instanceof Error ? gitignoreError.message : 'unknown error'}` + `Could not create .gitignore file: ${gitignoreError instanceof Error ? gitignoreError.message : 'unknown error'}` ); } } catch (error) { logger.warn( - '⚠️ Failed to initialize Git repository. Git features will be disabled.' + 'Failed to initialize Git repository. Git features will be disabled.' ); logger.warn( - `⚠️ Git error: ${error instanceof Error ? error.message : 'unknown error'}` + `Git error: ${error instanceof Error ? error.message : 'unknown error'}` ); throw error; } diff --git a/src/commands/init/projectStructure.ts b/src/commands/init/projectStructure.ts index e543ed9..20f7c91 100644 --- a/src/commands/init/projectStructure.ts +++ b/src/commands/init/projectStructure.ts @@ -82,7 +82,7 @@ export async function setupProjectStructure( } } catch (error) { logger.error( - `❌ Failed to create README.md: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to create README.md: ${error instanceof Error ? error.message : 'unknown error'}` ); } @@ -167,6 +167,6 @@ export async function setupProjectStructure( // Summary at normal level if (logger.getVerbosity() !== 'quiet') { - logger.success('βœ… Project files and structure created'); + logger.success('Project files and structure created'); } } diff --git a/src/commands/init/prompts.ts b/src/commands/init/prompts.ts index d4097b1..7fe1369 100644 --- a/src/commands/init/prompts.ts +++ b/src/commands/init/prompts.ts @@ -144,7 +144,7 @@ export async function promptForCompilerOptions(isLegacySamp: boolean = false): P const isDowngrade = comparison < 0; if (isDowngrade) { - logger.warn(`⚠️ Version conflict detected!`); + logger.warn(`Version conflict detected!`); logger.warn(` Server package includes: ${existingVersion}`); logger.warn(` Community compiler version: ${cleanTargetVersion}`); logger.warn(` Installing community compiler would be a downgrade!`); diff --git a/src/commands/init/serverDownload.ts b/src/commands/init/serverDownload.ts index b9d04d4..af25689 100644 --- a/src/commands/init/serverDownload.ts +++ b/src/commands/init/serverDownload.ts @@ -89,7 +89,7 @@ async function downloadServer( } catch (error) { spinner.fail(); logger.error( - `❌ Failed to download server package: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to download server package: ${error instanceof Error ? error.message : 'unknown error'}` ); if (logger.getVerbosity() !== 'quiet') { logger.newline(); @@ -273,9 +273,9 @@ export async function extractServerPackage( } } catch (err) { logger.warn( - `⚠️ Could not remove existing extract directory: ${err instanceof Error ? err.message : 'unknown error'}` + `Could not remove existing extract directory: ${err instanceof Error ? err.message : 'unknown error'}` ); - logger.warn('⚠️ Proceeding anyway, cleanup may not be complete'); + logger.warn('Proceeding anyway, cleanup may not be complete'); } } @@ -462,7 +462,7 @@ export async function extractServerPackage( } } catch (error) { logger.error( - `❌ Failed to extract server package: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to extract server package: ${error instanceof Error ? error.message : 'unknown error'}` ); try { fs.unlinkSync(filePath); diff --git a/src/commands/init/setup.ts b/src/commands/init/setup.ts index cc4e5eb..2cd4ab6 100644 --- a/src/commands/init/setup.ts +++ b/src/commands/init/setup.ts @@ -214,7 +214,7 @@ async function selectFilesToOverwrite(files: string[], analysis: { prefix = '[?] '; description = 'May be overwritten'; } else { - prefix = '[βœ“] '; + prefix = '[OK] '; } return { @@ -319,7 +319,7 @@ export async function setupInitCommand(options: CommandOptions): Promise { // Check if this is a bare server package const serverPackage = detectBareServerPackage(); if (serverPackage.type && !serverPackage.hasContent) { - logger.info(`🎯 Detected bare ${serverPackage.type.toUpperCase()} server package - setting up project...`); + logger.info(`Detected bare ${serverPackage.type.toUpperCase()} server package - setting up project...`); // Skip the "directory not empty" warnings for bare server packages } diff --git a/src/commands/kill/kill.ts b/src/commands/kill/kill.ts index c5007c0..2d31121 100644 --- a/src/commands/kill/kill.ts +++ b/src/commands/kill/kill.ts @@ -19,8 +19,8 @@ export default function (program: Command): void { try { if (!options.force) { - logger.warn('⚠️ This will forcefully terminate ALL SA-MP/open.mp server processes.'); - logger.info('πŸ’‘ For normal server shutdown, use Ctrl+C in the terminal running "tapi start"'); + logger.warn('This will forcefully terminate ALL SA-MP/open.mp server processes.'); + logger.info('For normal server shutdown, use Ctrl+C in the terminal running "tapi start"'); logger.newline(); // Simple confirmation without inquirer dependency @@ -32,12 +32,12 @@ export default function (program: Command): void { }); if (response.toLowerCase() !== 'y' && response.toLowerCase() !== 'yes') { - logger.info('ℹ️ Operation cancelled'); + logger.info('Operation cancelled'); return; } } - logger.heading('πŸ’€ Force killing server processes...'); + logger.heading('Force killing server processes...'); const platform = process.platform; let killed = false; @@ -54,7 +54,7 @@ export default function (program: Command): void { reject(error); } else { if (stdout && !stdout.includes('not found')) { - logger.success(`βœ… Killed ${processName}`); + logger.success(`Killed ${processName}`); killed = true; } resolve(); @@ -62,7 +62,7 @@ export default function (program: Command): void { }); }); } catch (error) { - logger.warn(`⚠️ Could not kill ${processName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.warn(`Could not kill ${processName}: ${error instanceof Error ? error.message : 'unknown error'}`); } } } else { @@ -77,7 +77,7 @@ export default function (program: Command): void { reject(error); } else { if (error?.code !== 1) { - logger.success(`βœ… Killed processes matching ${pattern}`); + logger.success(`Killed processes matching ${pattern}`); killed = true; } resolve(); @@ -85,7 +85,7 @@ export default function (program: Command): void { }); }); } catch (error) { - logger.warn(`⚠️ Could not kill ${pattern}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.warn(`Could not kill ${pattern}: ${error instanceof Error ? error.message : 'unknown error'}`); } } } @@ -95,14 +95,14 @@ export default function (program: Command): void { if (killed) { logger.newline(); - logger.finalSuccess('βœ… Server processes terminated and state cleared'); + logger.finalSuccess('Server processes terminated and state cleared'); } else { - logger.info('ℹ️ No server processes found running'); + logger.info('No server processes found running'); } } catch (error) { logger.error( - `❌ Kill operation failed: ${error instanceof Error ? error.message : 'unknown error'}` + `Kill operation failed: ${error instanceof Error ? error.message : 'unknown error'}` ); process.exit(1); } diff --git a/src/commands/run/index.ts b/src/commands/run/index.ts index 1962076..9e81451 100644 --- a/src/commands/run/index.ts +++ b/src/commands/run/index.ts @@ -24,7 +24,7 @@ export default function (program: Command): void { try { const manifest = await loadManifest(); if (!manifest) { - logger.error('❌ No pawn.json manifest found. Run "tapi init" first.'); + logger.error('No pawn.json manifest found. Run "tapi init" first.'); process.exit(1); } @@ -34,7 +34,7 @@ export default function (program: Command): void { } if (!scriptName) { - logger.error('❌ Please specify a script name or use --list to see available scripts'); + logger.error('Please specify a script name or use --list to see available scripts'); logger.info('Usage: tapi run '); process.exit(1); } @@ -42,7 +42,7 @@ export default function (program: Command): void { await runScript(manifest, scriptName); } catch (error) { - logger.error(`❌ Failed to run script: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to run script: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } }); @@ -59,7 +59,7 @@ async function listScripts(manifest: PackageManifest): Promise { return; } - logger.info('πŸ“œ Available scripts:'); + logger.info('Available scripts:'); for (const [name, command] of Object.entries(scripts)) { logger.info(` ${name}: ${command}`); } @@ -72,7 +72,7 @@ async function runScript(manifest: PackageManifest, scriptName: string): Promise const scripts = manifest.scripts || {}; if (!(scripts as Record)[scriptName]) { - logger.error(`❌ Script "${scriptName}" not found in pawn.json`); + logger.error(`Script "${scriptName}" not found in pawn.json`); logger.info('Available scripts:'); for (const name of Object.keys(scripts)) { logger.info(` - ${name}`); @@ -81,15 +81,15 @@ async function runScript(manifest: PackageManifest, scriptName: string): Promise } const command = (scripts as Record)[scriptName]; - logger.info(`πŸš€ Running script: ${scriptName}`); - logger.info(`πŸ“ Command: ${command}`); + logger.info(`Running script: ${scriptName}`); + logger.info(`Command: ${command}`); try { // Parse the command to handle multiple commands separated by && const commands = command.split('&&').map((cmd: string) => cmd.trim()); for (const cmd of commands) { - logger.info(`▢️ Executing: ${cmd}`); + logger.info(`Executing: ${cmd}`); // Replace tapi with the actual executable path // Find the dist directory by looking up the directory tree @@ -126,10 +126,10 @@ async function runScript(manifest: PackageManifest, scriptName: string): Promise } } - logger.success(`βœ… Script "${scriptName}" completed successfully`); + logger.success(`Script "${scriptName}" completed successfully`); } catch (error) { - logger.error(`❌ Script "${scriptName}" failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Script "${scriptName}" failed: ${error instanceof Error ? error.message : 'unknown error'}`); process.exit(1); } } diff --git a/src/commands/setup/setup.ts b/src/commands/setup/setup.ts index 2cdfe3d..f937067 100644 --- a/src/commands/setup/setup.ts +++ b/src/commands/setup/setup.ts @@ -10,10 +10,10 @@ import { showBanner } from '../../utils/banner'; * @param force - When true, re-run setup even if previously completed. */ export async function setupWizard(force = false): Promise { - if (!force && configManager.isSetupComplete()) { - logger.info('ℹ️ Setup has already been completed.'); - logger.newline(); - logger.subheading('βš™οΈ Your current configuration:'); + if (!force && configManager.isSetupComplete()) { + logger.info('Setup has already been completed.'); + logger.newline(); + logger.subheading('Your current configuration:'); const config = configManager.getFullConfig(); logger.keyValue('Default author', config.defaultAuthor || '(not set)'); logger.keyValue('Preferred editor', config.editor || '(not set)'); @@ -22,13 +22,13 @@ export async function setupWizard(force = false): Promise { config.githubToken ? 'Configured' : 'Not configured' ); logger.newline(); - logger.info('πŸ’‘ To force setup to run again, use: tapi setup --force'); - logger.info('πŸ’‘ To edit individual settings, use: tapi config'); + logger.hint('To force setup to run again, use: tapi setup --force'); + logger.hint('To edit individual settings, use: tapi config'); logger.newline(); return true; } - logger.heading('πŸŽ‰ Welcome to tapi!'); + logger.heading('Welcome to tapi!'); logger.info('This one-time setup will help configure tapi for your use.'); logger.newline(); @@ -71,14 +71,14 @@ export async function setupWizard(force = false): Promise { configManager.setSetupComplete(true); logger.newline(); - logger.finalSuccess('βœ… Setup complete! You can now use tapi.'); - logger.info('πŸ’‘ To change these settings in the future, run: tapi config'); + logger.finalSuccess('Setup complete! You can now use tapi.'); + logger.hint('To change these settings in the future, run: tapi config'); return true; } catch (error) { - logger.error( - `❌ Setup failed: ${error instanceof Error ? error.message : 'unknown error'}` - ); + logger.error( + `Setup failed: ${error instanceof Error ? error.message : 'unknown error'}` + ); return false; } } diff --git a/src/commands/start/start.ts b/src/commands/start/start.ts index 15f0cd6..04256a8 100644 --- a/src/commands/start/start.ts +++ b/src/commands/start/start.ts @@ -41,7 +41,7 @@ function formatServerOutput(output: string, isError = false): void { // Component loading messages if (line.includes('Loading component')) { const componentName = line.match(/Loading component (.+?)\.dll/)?.[1] || 'Unknown'; - console.log(`${colors.blue}β†’${colors.reset} Loading ${colors.cyan}${componentName}${colors.reset} component...`); + console.log(`${colors.blue}->${colors.reset} Loading ${colors.cyan}${componentName}${colors.reset} component...`); continue; } @@ -50,7 +50,7 @@ function formatServerOutput(output: string, isError = false): void { const match = line.match(/Successfully loaded component (.+?) \((.+?)\)/); if (match) { const [, componentName, version] = match; - console.log(`${colors.green}βœ“${colors.reset} ${colors.cyan}${componentName}${colors.reset} ${colors.gray}(${version})${colors.reset}`); + console.log(`${colors.green}OK${colors.reset} ${colors.cyan}${componentName}${colors.reset} ${colors.gray}(${version})${colors.reset}`); } continue; } @@ -59,7 +59,7 @@ function formatServerOutput(output: string, isError = false): void { if (line.includes('Starting open.mp server')) { const versionMatch = line.match(/Starting open.mp server \((.+?)\)/); if (versionMatch) { - console.log(`${colors.green}${colors.bright}β†’ open.mp server ${versionMatch[1]}${colors.reset}`); + console.log(`${colors.green}${colors.bright}-> open.mp server ${versionMatch[1]}${colors.reset}`); } continue; } @@ -68,7 +68,7 @@ function formatServerOutput(output: string, isError = false): void { if (line.includes('Loaded') && line.includes('component(s)')) { const match = line.match(/Loaded (\d+) component\(s\)/); if (match) { - console.log(`${colors.green}βœ“${colors.reset} Loaded ${colors.bright}${match[1]}${colors.reset} components`); + console.log(`${colors.green}OK${colors.reset} Loaded ${colors.bright}${match[1]}${colors.reset} components`); } continue; } @@ -81,7 +81,7 @@ function formatServerOutput(output: string, isError = false): void { const time = new Date(timestamp).toLocaleTimeString(); let levelColor = colors.white; - let levelIcon = 'β€’'; + let levelIcon = '-'; switch (level.toLowerCase()) { case 'info': @@ -112,7 +112,7 @@ function formatServerOutput(output: string, isError = false): void { if (line.includes('Legacy Network started on port')) { const portMatch = line.match(/port (\d+)/); if (portMatch) { - console.log(`${colors.green}βœ“${colors.reset} Server listening on port ${colors.bright}${portMatch[1]}${colors.reset}`); + console.log(`${colors.green}OK${colors.reset} Server listening on port ${colors.bright}${portMatch[1]}${colors.reset}`); } continue; } @@ -312,7 +312,7 @@ interface StartOptions { * Launch watch mode: rebuild and restart the server when source files change. */ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): Promise { - logger.heading('πŸ”„ Starting watch mode...'); + logger.heading('Starting watch mode...'); logger.info('Press Ctrl+C to stop watching and exit'); logger.newline(); @@ -326,7 +326,7 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P // Kill existing server if running if (serverProcess && !serverProcess.killed) { - logger.info('πŸ›‘ Stopping server...'); + logger.info('Stopping server...'); serverProcess.kill('SIGTERM'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for graceful shutdown if (!serverProcess.killed) { @@ -336,7 +336,7 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P try { // Build first - logger.info('πŸ”¨ Building project...'); + logger.info('Building project...'); const buildResult = await new Promise<{ success: boolean, output: string }>((resolve) => { const buildProcess = spawn('tapi', ['build'], { stdio: ['ignore', 'pipe', 'pipe'], @@ -357,16 +357,16 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P }); if (!buildResult.success) { - logger.error('❌ Build failed, not restarting server'); + logger.error('Build failed, not restarting server'); logger.info(buildResult.output); isRestarting = false; return; } - logger.success('βœ… Build successful'); + logger.success('Build successful'); // Start server - logger.info('πŸš€ Starting server...'); + logger.info('Starting server...'); serverProcess = spawn(serverInfo.executable, [], { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] @@ -386,7 +386,7 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P } }); - logger.success('βœ… Server started in watch mode'); + logger.success('Server started in watch mode'); } catch (error) { logger.error(`Failed to start server: ${error instanceof Error ? error.message : 'unknown error'}`); } @@ -407,18 +407,18 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P ignoreInitial: true }); - watcher.on('change', async (path) => { - logger.info(`πŸ“ File changed: ${path}`); + watcher.on('change', async (path) => { + logger.info(`File changed: ${path}`); await startServer(); }); - watcher.on('add', async (path) => { - logger.info(`βž• File added: ${path}`); + watcher.on('add', async (path) => { + logger.info(`File added: ${path}`); await startServer(); }); - watcher.on('unlink', async (path) => { - logger.info(`πŸ—‘οΈ File deleted: ${path}`); + watcher.on('unlink', async (path) => { + logger.info(`File deleted: ${path}`); await startServer(); }); @@ -428,7 +428,7 @@ async function startWatchMode(serverInfo: ServerInfo, _options: StartOptions): P // Handle cleanup on exit process.on('SIGINT', () => { logger.newline(); - logger.info('πŸ›‘ Stopping watch mode...'); + logger.info('Stopping watch mode...'); if (serverProcess && !serverProcess.killed) { serverProcess.kill('SIGTERM'); diff --git a/src/core/addons/AddonDiscovery.ts b/src/core/addons/AddonDiscovery.ts index bad9047..f22e8b8 100644 --- a/src/core/addons/AddonDiscovery.ts +++ b/src/core/addons/AddonDiscovery.ts @@ -36,7 +36,7 @@ export class AddonDiscovery { } } catch (error) { logger.detail( - `⚠️ Addon discovery failed: ${error instanceof Error ? error.message : 'unknown error'}` + `Addon discovery failed: ${error instanceof Error ? error.message : 'unknown error'}` ); } } @@ -116,7 +116,7 @@ export class AddonDiscovery { } } catch (error) { logger.detail( - `⚠️ Failed to discover addon at ${addonPath}: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to discover addon at ${addonPath}: ${error instanceof Error ? error.message : 'unknown error'}` ); } } @@ -153,7 +153,7 @@ export class AddonDiscovery { }; this.loader.registerAddon(addon, addonInfo); - logger.detail(`πŸ” Discovered addon: ${addonInfo.name} from ${source}`); + logger.detail(`Discovered addon: ${addonInfo.name} from ${source}`); } /** @@ -254,7 +254,7 @@ export class AddonDiscovery { } } catch (error) { logger.detail( - `⚠️ Failed to search directory ${dirPath}: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to search directory ${dirPath}: ${error instanceof Error ? error.message : 'unknown error'}` ); } } diff --git a/src/core/addons/AddonInstaller.ts b/src/core/addons/AddonInstaller.ts index 8574b10..6bc7a8f 100644 --- a/src/core/addons/AddonInstaller.ts +++ b/src/core/addons/AddonInstaller.ts @@ -89,7 +89,7 @@ export class AddonInstaller { } await this.registry.removeAddonFromRegistry(addonName); - logger.success(`βœ… Uninstalled addon: ${addonName}`); + logger.success(`Uninstalled addon: ${addonName}`); } /** @@ -111,7 +111,7 @@ export class AddonInstaller { const [, username, repoName] = match; - logger.info(`πŸ“₯ Downloading latest version from ${username}/${repoName}...`); + logger.info(`Downloading latest version from ${username}/${repoName}...`); const currentPath = addonInfo.path; if (!currentPath) { @@ -122,7 +122,7 @@ export class AddonInstaller { if (fs.existsSync(currentPath)) { await fs.promises.rename(currentPath, backupPath); - logger.detail(`πŸ“¦ Created backup: ${backupPath}`); + logger.detail(`Created backup: ${backupPath}`); } try { @@ -154,7 +154,7 @@ export class AddonInstaller { await fs.promises.rm(backupPath, { recursive: true }); } - logger.success(`βœ… Successfully updated addon: ${addonName}`); + logger.success(`Successfully updated addon: ${addonName}`); } catch (error) { // Restore backup on failure if (fs.existsSync(backupPath)) { @@ -162,7 +162,7 @@ export class AddonInstaller { await fs.promises.rm(currentPath, { recursive: true }); } await fs.promises.rename(backupPath, currentPath); - logger.info('πŸ”„ Restored backup after failed update'); + logger.info('Restored backup after failed update'); } throw error; } @@ -172,7 +172,7 @@ export class AddonInstaller { * Update every GitHub-based addon currently installed. */ async updateAllGitHubAddons(): Promise { - logger.info('πŸ”„ Updating all addons...'); + logger.info('Updating all addons...'); const addons = this.loader.getAllAddons(); const githubAddons = addons @@ -181,7 +181,7 @@ export class AddonInstaller { .filter((i) => i.source === 'github'); if (githubAddons.length === 0) { - logger.info('πŸ“¦ No GitHub-based addons to update'); + logger.info('No GitHub-based addons to update'); return; } @@ -193,26 +193,26 @@ export class AddonInstaller { for (const addon of githubAddons) { try { - logger.info(`\nπŸ“¦ Updating: ${addon.name}`); + logger.info(`\nUpdating: ${addon.name}`); await this.updateGitHubAddon(addon.name); updated++; } catch (error) { failed++; failedAddons.push(addon.name); logger.error( - `❌ Failed to update ${addon.name}: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to update ${addon.name}: ${error instanceof Error ? error.message : 'unknown error'}` ); } } - logger.info('\nπŸ“Š Update Summary:'); - logger.info(` βœ… Successfully updated: ${updated} addon(s)`); + logger.info('\nUpdate summary:'); + logger.success(`Successfully updated: ${updated} addon(s)`); if (failed > 0) { - logger.info( - ` ❌ Failed to update: ${failed} addon(s): ${failedAddons.join(', ')}` + logger.error( + `Failed to update: ${failed} addon(s): ${failedAddons.join(', ')}` ); logger.info( - 'πŸ’‘ Try updating failed addons individually: tapi addon update ' + 'Try updating failed addons individually: tapi addon update ' ); } } @@ -239,7 +239,7 @@ export class AddonInstaller { }); await this.registry.saveToRegistry(); - logger.success(`βœ… Installed local addon: ${addon.name}@${addon.version}`); + logger.success(`Installed local addon: ${addon.name}@${addon.version}`); } /** @@ -289,6 +289,6 @@ export class AddonInstaller { }); await this.registry.saveToRegistry(); - logger.success(`βœ… Installed addon: ${addon.name}@${addon.version}`); + logger.success(`Installed addon: ${addon.name}@${addon.version}`); } } diff --git a/src/core/addons/AddonRecovery.ts b/src/core/addons/AddonRecovery.ts index 38baea6..0ae9e7d 100644 --- a/src/core/addons/AddonRecovery.ts +++ b/src/core/addons/AddonRecovery.ts @@ -61,7 +61,7 @@ export class AddonRecovery { await this.registry.disableAddonInRegistry(addonName, errorMsg); } catch (error) { logger.detail( - `⚠️ Could not auto-disable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}` + `Could not auto-disable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}` ); } } diff --git a/src/core/addons/AddonRegistry.ts b/src/core/addons/AddonRegistry.ts index f6314f3..571ec36 100644 --- a/src/core/addons/AddonRegistry.ts +++ b/src/core/addons/AddonRegistry.ts @@ -43,13 +43,13 @@ export class AddonRegistry { const errorMsg = error instanceof Error ? error.message : 'unknown error'; logger.warn( - `⚠️ Failed to load addon from registry: ${addonInfo.name} (${errorMsg})` + `Failed to load addon from registry: ${addonInfo.name} (${errorMsg})` ); failedAddons.push(addonInfo.name); } } else { logger.warn( - `⚠️ Addon path not found: ${addonInfo.name} (${addonInfo.path})` + `Addon path not found: ${addonInfo.name} (${addonInfo.path})` ); failedAddons.push(addonInfo.name); } @@ -58,21 +58,21 @@ export class AddonRegistry { // Report loading summary if (loadedAddons.length > 0) { logger.detail( - `βœ… Successfully loaded ${loadedAddons.length} addon(s): ${loadedAddons.join(', ')}` + `Successfully loaded ${loadedAddons.length} addon(s): ${loadedAddons.join(', ')}` ); } if (failedAddons.length > 0) { logger.warn( - `⚠️ Failed to load ${failedAddons.length} addon(s): ${failedAddons.join(', ')}` + `Failed to load ${failedAddons.length} addon(s): ${failedAddons.join(', ')}` ); logger.info( - 'πŸ’‘ Run "tapi addon list" to see addon status and recovery suggestions' + 'Run "tapi addon list" to see addon status and recovery suggestions' ); } } catch (error) { logger.warn( - `⚠️ Failed to load addon registry: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to load addon registry: ${error instanceof Error ? error.message : 'unknown error'}` ); } @@ -105,7 +105,7 @@ export class AddonRegistry { fs.writeFileSync(this.registryFile, JSON.stringify(registryData, null, 2)); } catch (error) { logger.warn( - `⚠️ Failed to save addon registry: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to save addon registry: ${error instanceof Error ? error.message : 'unknown error'}` ); } } @@ -144,11 +144,11 @@ export class AddonRegistry { this.registryFile, JSON.stringify(registryData, null, 2) ); - logger.detail(`πŸ”§ Automatically disabled problematic addon: ${addonName}`); + logger.detail(`Automatically disabled problematic addon: ${addonName}`); } } catch (recoveryError) { logger.detail( - `⚠️ Could not auto-disable addon ${addonName}: ${recoveryError instanceof Error ? recoveryError.message : 'unknown error'}` + `Could not auto-disable addon ${addonName}: ${recoveryError instanceof Error ? recoveryError.message : 'unknown error'}` ); } } @@ -188,7 +188,7 @@ export class AddonRegistry { } } catch (error) { logger.warn( - `⚠️ Failed to update addon in registry: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to update addon in registry: ${error instanceof Error ? error.message : 'unknown error'}` ); } } @@ -222,7 +222,7 @@ export class AddonRegistry { } } catch (error) { logger.warn( - `⚠️ Failed to remove addon from registry: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to remove addon from registry: ${error instanceof Error ? error.message : 'unknown error'}` ); } } @@ -254,7 +254,7 @@ export class AddonRegistry { return JSON.parse(registryContent); } catch (error) { logger.warn( - `⚠️ Failed to read registry: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to read registry: ${error instanceof Error ? error.message : 'unknown error'}` ); return null; } diff --git a/src/core/addons/GitHubDownloader.ts b/src/core/addons/GitHubDownloader.ts index 0058cc6..6318698 100644 --- a/src/core/addons/GitHubDownloader.ts +++ b/src/core/addons/GitHubDownloader.ts @@ -67,7 +67,7 @@ export class GitHubDownloader { try { await this.extractZip(zipPath, targetPath, repoName, branch); logger.detail( - `βœ… Successfully downloaded and extracted ${username}/${repoName}` + `Successfully downloaded and extracted ${username}/${repoName}` ); resolve(); } catch (error) { diff --git a/src/core/addons/commandResolver.ts b/src/core/addons/commandResolver.ts index d816e99..ae21228 100644 --- a/src/core/addons/commandResolver.ts +++ b/src/core/addons/commandResolver.ts @@ -26,7 +26,7 @@ export class CommandResolver implements ICommandResolver { this.handleCommandConflict(command.name, existingCommand, command); } else { this.addonCommands.set(command.name, command); - logger.detail(`πŸ”Œ Registered addon command: ${command.name} (priority: ${command.priority || 0})`); + logger.detail(`Registered addon command: ${command.name} (priority: ${command.priority || 0})`); } } @@ -38,20 +38,20 @@ export class CommandResolver implements ICommandResolver { const newPriority = newCommand.priority || 0; if (newPriority > existingPriority) { - logger.warn(`⚠️ Command conflict resolved: ${commandName} overridden by higher priority addon`); + logger.warn(`Command conflict resolved: ${commandName} overridden by higher priority addon`); this.addonCommands.set(commandName, newCommand); if (!this.commandConflicts.has(commandName)) { this.commandConflicts.set(commandName, []); } this.commandConflicts.get(commandName)!.push(existingCommand, newCommand); } else if (newPriority === existingPriority) { - logger.warn(`⚠️ Command conflict: ${commandName} already registered with same priority. Keeping first addon.`); + logger.warn(`Command conflict: ${commandName} already registered with same priority. Keeping first addon.`); if (!this.commandConflicts.has(commandName)) { this.commandConflicts.set(commandName, []); } this.commandConflicts.get(commandName)!.push(existingCommand, newCommand); } else { - logger.info(`ℹ️ Command ${commandName} already registered with higher priority. Ignoring new addon.`); + logger.info(`Command ${commandName} already registered with higher priority. Ignoring new addon.`); } } @@ -100,10 +100,10 @@ export class CommandResolver implements ICommandResolver { for (const command of addonCommands) { if (command.override) { - logger.detail(`πŸ”„ Overriding command: ${command.name}`); + logger.detail(`Overriding command: ${command.name}`); this.overrideCommand(command); } else { - logger.detail(`βž• Adding new command: ${command.name}`); + logger.detail(`Adding new command: ${command.name}`); this.addNewCommand(command); } } @@ -127,18 +127,18 @@ export class CommandResolver implements ICommandResolver { const commandOptions = args[args.length - 1] as Record; await addonCommand.handler(commandArgs, commandOptions); } catch (error) { - logger.error(`❌ Addon command ${addonCommand.name} failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Addon command ${addonCommand.name} failed: ${error instanceof Error ? error.message : 'unknown error'}`); const originalHandler = this.getOriginalCommand(addonCommand.name); if (originalHandler) { - logger.info(`πŸ”„ Falling back to original ${addonCommand.name} command...`); + logger.info(`Falling back to original ${addonCommand.name} command...`); try { await originalHandler(); } catch (fallbackError) { - logger.error(`❌ Original command also failed: ${fallbackError instanceof Error ? fallbackError.message : 'unknown error'}`); + logger.error(`Original command also failed: ${fallbackError instanceof Error ? fallbackError.message : 'unknown error'}`); process.exit(1); } } else { - logger.error(`❌ No fallback available for ${addonCommand.name}`); + logger.error(`No fallback available for ${addonCommand.name}`); process.exit(1); } } @@ -148,9 +148,9 @@ export class CommandResolver implements ICommandResolver { existingCommand.description(`${addonCommand.description} (overridden by addon)`); } - logger.detail(`βœ… Successfully overridden command: ${addonCommand.name}`); + logger.detail(`Successfully overridden command: ${addonCommand.name}`); } else { - logger.warn(`⚠️ Cannot override command ${addonCommand.name}: command not found`); + logger.warn(`Cannot override command ${addonCommand.name}: command not found`); } } @@ -167,8 +167,8 @@ export class CommandResolver implements ICommandResolver { const commandOptions = args[args.length - 1] as Record; await addonCommand.handler(commandArgs, commandOptions); } catch (error) { - logger.error(`❌ Addon command ${addonCommand.name} failed: ${error instanceof Error ? error.message : 'unknown error'}`); - logger.error(`❌ Command ${addonCommand.name} is provided by an addon and failed. Check addon status with 'tapi addon list'`); + logger.error(`Addon command ${addonCommand.name} failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Command ${addonCommand.name} is provided by an addon and failed. Check addon status with 'tapi addon list'`); process.exit(1); } }); @@ -187,7 +187,7 @@ export class CommandResolver implements ICommandResolver { } } - logger.detail(`βœ… Successfully added new command: ${addonCommand.name}`); + logger.detail(`Successfully added new command: ${addonCommand.name}`); } /** diff --git a/src/core/addons/dependencyResolver.ts b/src/core/addons/dependencyResolver.ts index 76f0f2e..ebeb4ee 100644 --- a/src/core/addons/dependencyResolver.ts +++ b/src/core/addons/dependencyResolver.ts @@ -76,7 +76,7 @@ export class DependencyResolver { */ async resolveDependencies(addonName: string, _targetVersion?: string): Promise { try { - logger.detail(`πŸ” Resolving dependencies for: ${addonName}`); + logger.detail(`Resolving dependencies for: ${addonName}`); const resolution: DependencyResolution = { resolved: [], @@ -104,12 +104,12 @@ export class DependencyResolver { // Check for version conflicts this.checkVersionConflicts(resolution, graph); - logger.detail(`βœ… Dependency resolution complete: ${resolution.resolved.length} resolved, ${resolution.conflicts.length} conflicts, ${resolution.missing.length} missing, ${resolution.versionConflicts.length} version conflicts`); + logger.detail(`Dependency resolution complete: ${resolution.resolved.length} resolved, ${resolution.conflicts.length} conflicts, ${resolution.missing.length} missing, ${resolution.versionConflicts.length} version conflicts`); return resolution; } catch (error) { - logger.error(`❌ Failed to resolve dependencies for ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to resolve dependencies for ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -325,17 +325,17 @@ export class DependencyResolver { return result; } - logger.info(`πŸ”„ Auto-installing ${resolution.missing.length} missing dependencies...`); + logger.info(`Auto-installing ${resolution.missing.length} missing dependencies...`); for (const missingDep of resolution.missing) { try { - logger.info(`πŸ“¦ Installing dependency: ${missingDep}`); + logger.info(`Installing dependency: ${missingDep}`); await this.addonManager.installAddon(missingDep, options); result.installed.push(missingDep); - logger.success(`βœ… Installed: ${missingDep}`); + logger.success(`Installed: ${missingDep}`); } catch (error) { result.failed.push(missingDep); - logger.error(`❌ Failed to install ${missingDep}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to install ${missingDep}: ${error instanceof Error ? error.message : 'unknown error'}`); } } @@ -375,9 +375,9 @@ export class DependencyResolver { suggestions.push('Resolve conflicts by:'); for (const conflict of resolution.conflicts) { if (conflict.conflict === 'circular') { - suggestions.push(` β€’ Remove circular dependency involving ${conflict.addon}`); + suggestions.push(` - Remove circular dependency involving ${conflict.addon}`); } else if (conflict.conflict === 'version') { - suggestions.push(` β€’ Update addon versions to resolve conflict: ${conflict.reason}`); + suggestions.push(` - Update addon versions to resolve conflict: ${conflict.reason}`); } } } @@ -385,11 +385,11 @@ export class DependencyResolver { if (resolution.versionConflicts.length > 0) { suggestions.push('Resolve version conflicts by:'); for (const versionConflict of resolution.versionConflicts) { - suggestions.push(` β€’ ${versionConflict.addon}: ${versionConflict.reason}`); + suggestions.push(` - ${versionConflict.addon}: ${versionConflict.reason}`); suggestions.push(` Available version: ${versionConflict.availableVersion}`); suggestions.push(` Required constraint: ${versionConflict.constraint}`); } - suggestions.push(' β€’ Update addon versions or adjust version constraints in addon configuration'); + suggestions.push(' - Update addon versions or adjust version constraints in addon configuration'); } return suggestions; diff --git a/src/core/addons/hooks.ts b/src/core/addons/hooks.ts index f6b5698..56c2b4b 100644 --- a/src/core/addons/hooks.ts +++ b/src/core/addons/hooks.ts @@ -46,7 +46,7 @@ export class HookManager { this.registerAddonHooks(addon); } - logger.detail(`πŸ”— Registered hooks for ${addons.length} addons`); + logger.detail(`Registered hooks for ${addons.length} addons`); } /** @@ -56,7 +56,7 @@ export class HookManager { for (const [hookName, hook] of Object.entries(addon.hooks)) { if (hook && typeof hook === 'function') { this.registerHook(hookName, hook); - logger.detail(` πŸ“Œ ${addon.name}: ${hookName}`); + logger.detail(` - ${addon.name}: ${hookName}`); } } } @@ -81,7 +81,7 @@ export class HookManager { return; } - logger.detail(`🎣 Executing ${handlers.length} hooks for: ${event}`); + logger.detail(`Executing ${handlers.length} hooks for: ${event}`); const failedHooks: string[] = []; @@ -90,7 +90,7 @@ export class HookManager { await handler(context); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'unknown error'; - logger.error(`❌ Hook ${event} failed: ${errorMsg}`); + logger.error(`Hook ${event} failed: ${errorMsg}`); failedHooks.push(event); // Provide recovery suggestions for hook failures @@ -102,7 +102,7 @@ export class HookManager { // Report hook execution summary if (failedHooks.length > 0) { - logger.warn(`⚠️ ${failedHooks.length} hook(s) failed during ${event} event`); + logger.warn(`${failedHooks.length} hook(s) failed during ${event} event`); } } @@ -110,24 +110,24 @@ export class HookManager { * Provide recovery suggestions for hook failures */ private provideHookRecoverySuggestions(event: string, errorMsg: string): void { - logger.info('πŸ”§ Hook recovery suggestions:'); + logger.info('Hook recovery suggestions:'); if (errorMsg.includes('Cannot read property') || errorMsg.includes('undefined')) { - logger.info(' β€’ Check that the hook context contains expected properties'); - logger.info(' β€’ Verify hook implementation matches the expected interface'); + logger.info(' - Check that the hook context contains expected properties'); + logger.info(' - Verify hook implementation matches the expected interface'); } else if (errorMsg.includes('Permission denied') || errorMsg.includes('EACCES')) { - logger.info(' β€’ Check file permissions for the operation'); - logger.info(' β€’ Ensure tapi has write access to required directories'); + logger.info(' - Check file permissions for the operation'); + logger.info(' - Ensure tapi has write access to required directories'); } else if (errorMsg.includes('ENOENT') || errorMsg.includes('not found')) { - logger.info(' β€’ Verify that required files exist before accessing them'); - logger.info(' β€’ Check file paths in hook implementation'); + logger.info(' - Verify that required files exist before accessing them'); + logger.info(' - Check file paths in hook implementation'); } else { - logger.info(' β€’ Review hook implementation for logical errors'); - logger.info(' β€’ Check addon documentation for hook usage examples'); + logger.info(' - Review hook implementation for logical errors'); + logger.info(' - Check addon documentation for hook usage examples'); } - logger.info(` β€’ Disable the problematic addon: 'tapi addon disable '`); - logger.info(` β€’ Check addon status: 'tapi addon list'`); + logger.info(` - Disable the problematic addon: 'tapi addon disable '`); + logger.info(` - Check addon status: 'tapi addon list'`); } /** diff --git a/src/core/addons/loader.ts b/src/core/addons/loader.ts index 51d13f2..5ea9ac1 100644 --- a/src/core/addons/loader.ts +++ b/src/core/addons/loader.ts @@ -73,14 +73,14 @@ export class AddonLoader { lastError = error instanceof Error ? error : new Error('Unknown error'); if (attempt < retries && this.isRetryableError(lastError)) { - logger.warn(`⚠️ Addon load attempt ${attempt} failed, retrying... (${lastError.message})`); + logger.warn(`Addon load attempt ${attempt} failed, retrying... (${lastError.message})`); await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff } } } // All retries failed - provide detailed error information - logger.error(`❌ Failed to load addon ${addonPath} after ${retries} attempts`); + logger.error(`Failed to load addon ${addonPath} after ${retries} attempts`); logger.error(`Last error: ${lastError?.message}`); // Provide recovery suggestions @@ -138,7 +138,7 @@ export class AddonLoader { await addon.activate(this.context); } - logger.detail(`βœ… Loaded addon: ${addon.name}@${addon.version}`); + logger.detail(`Loaded addon: ${addon.name}@${addon.version}`); return addon; } @@ -164,30 +164,30 @@ export class AddonLoader { * Provide recovery suggestions for failed addon loads */ private provideRecoverySuggestions(addonPath: string, error: Error | null): void { - logger.info('πŸ”§ Recovery suggestions:'); + logger.info('Recovery suggestions:'); if (error?.message.includes('Addon not found')) { - logger.info(' β€’ Verify the addon path is correct'); - logger.info(' β€’ Ensure the addon directory exists and contains valid files'); + logger.info(' - Verify the addon path is correct'); + logger.info(' - Ensure the addon directory exists and contains valid files'); } else if (error?.message.includes('Invalid addon structure')) { - logger.info(' β€’ Ensure your addon exports a class that extends TapiAddon'); - logger.info(' β€’ Check that your addon has proper constructor and methods'); + logger.info(' - Ensure your addon exports a class that extends TapiAddon'); + logger.info(' - Check that your addon has proper constructor and methods'); } else if (error?.message.includes('Cannot resolve module')) { - logger.info(' β€’ Check that all dependencies are installed'); - logger.info(' β€’ Run npm install in the addon directory'); + logger.info(' - Check that all dependencies are installed'); + logger.info(' - Run npm install in the addon directory'); } else if (error?.message.includes('SyntaxError')) { - logger.info(' β€’ Check for syntax errors in your addon code'); - logger.info(' β€’ Validate JavaScript syntax'); + logger.info(' - Check for syntax errors in your addon code'); + logger.info(' - Validate JavaScript syntax'); } else if (error?.message.includes('EACCES') || error?.message.includes('Permission denied')) { - logger.info(' β€’ Check file permissions on the addon directory'); - logger.info(' β€’ Ensure tapi has read access to the addon files'); + logger.info(' - Check file permissions on the addon directory'); + logger.info(' - Ensure tapi has read access to the addon files'); } else { - logger.info(' β€’ Check addon documentation for setup requirements'); - logger.info(' β€’ Verify addon compatibility with current tapi version'); + logger.info(' - Check addon documentation for setup requirements'); + logger.info(' - Verify addon compatibility with current tapi version'); } - logger.info(` β€’ Run 'tapi addon list' to see working addons`); - logger.info(` β€’ Try reinstalling the addon: 'tapi addon uninstall && tapi addon install '`); + logger.info(` - Run 'tapi addon list' to see working addons`); + logger.info(` - Try reinstalling the addon: 'tapi addon uninstall && tapi addon install '`); } /** @@ -206,10 +206,10 @@ export class AddonLoader { } this.addons.delete(name); - logger.info(`βœ… Unloaded addon: ${name}`); + logger.info(`Unloaded addon: ${name}`); } catch (error) { - logger.error(`❌ Failed to unload addon ${name}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to unload addon ${name}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -291,15 +291,15 @@ export class AddonLoader { await this.loadAddon(addonFilePath); } catch (error) { - logger.warn(`⚠️ Failed to load addon from ${addonDir}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.warn(`Failed to load addon from ${addonDir}: ${error instanceof Error ? error.message : 'unknown error'}`); } } } - logger.info(`πŸ“¦ Loaded ${this.addons.size} addons`); + logger.info(`Loaded ${this.addons.size} addons`); } catch (error) { - logger.error(`❌ Failed to load addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to load addons: ${error instanceof Error ? error.message : 'unknown error'}`); } } diff --git a/src/core/addons/manager.ts b/src/core/addons/manager.ts index ca753db..f2b1317 100644 --- a/src/core/addons/manager.ts +++ b/src/core/addons/manager.ts @@ -100,10 +100,10 @@ export class AddonManager { this.addonsLoaded = true; if (addons.length > 0) { - logger.detail(`πŸ“¦ Loaded ${addons.length} addon${addons.length === 1 ? '' : 's'}`); + logger.detail(`Loaded ${addons.length} addon${addons.length === 1 ? '' : 's'}`); } } catch (_error) { - logger.warn(`⚠️ Failed to initialize addons: ${_error instanceof Error ? _error.message : 'unknown error'}`); + logger.warn(`Failed to initialize addons: ${_error instanceof Error ? _error.message : 'unknown error'}`); } } @@ -159,7 +159,7 @@ export class AddonManager { try { await this.registry.saveToRegistry(); } catch (error) { - logger.warn(`⚠️ Failed to save addon registry: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.warn(`Failed to save addon registry: ${error instanceof Error ? error.message : 'unknown error'}`); } } @@ -181,27 +181,27 @@ export class AddonManager { } }, installPackage: async (packageName: string) => { - logger.info(`πŸ“¦ Installing package: ${packageName}`); + logger.info(`Installing package: ${packageName}`); try { await this.installAddon(packageName); - logger.info(`βœ… Successfully installed package: ${packageName}`); + logger.info(`Successfully installed package: ${packageName}`); } catch (error) { - logger.error(`❌ Failed to install package ${packageName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to install package ${packageName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } }, uninstallPackage: async (packageName: string) => { - logger.info(`πŸ—‘οΈ Uninstalling package: ${packageName}`); + logger.info(`Uninstalling package: ${packageName}`); try { await this.uninstallAddon(packageName); - logger.info(`βœ… Successfully uninstalled package: ${packageName}`); + logger.info(`Successfully uninstalled package: ${packageName}`); } catch (error) { - logger.error(`❌ Failed to uninstall package ${packageName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to uninstall package ${packageName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } }, build: async (input: string, options?: Record) => { - logger.info(`πŸ”¨ Building: ${input}`); + logger.info(`Building: ${input}`); try { const { spawn } = await import('child_process'); const path = await import('path'); @@ -255,7 +255,7 @@ export class AddonManager { return new Promise((resolve, reject) => { compiler.on('close', (code) => { if (code === 0) { - logger.info(`βœ… Build successful: ${outputFile}`); + logger.info(`Build successful: ${outputFile}`); resolve(); } else { reject(new Error(`Build failed with exit code ${code}`)); @@ -268,12 +268,12 @@ export class AddonManager { }); } catch (error) { - logger.error(`❌ Build failed: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Build failed: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } }, startServer: async (config?: Record): Promise => { - logger.info('πŸš€ Starting server...'); + logger.info('Starting server...'); try { const { spawn } = await import('child_process'); const path = await import('path'); @@ -297,15 +297,15 @@ export class AddonManager { spawn(serverPath, args, { stdio: 'inherit' }); - logger.info('βœ… Server started successfully'); + logger.info('Server started successfully'); } catch (error) { - logger.error(`❌ Failed to start server: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to start server: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } }, stopServer: async () => { - logger.info('πŸ›‘ Stopping server...'); + logger.info('Stopping server...'); try { const { exec } = await import('child_process'); const { promisify } = await import('util'); @@ -319,9 +319,9 @@ export class AddonManager { await execAsync('pkill -f open.mp-server'); } - logger.info('βœ… Server stopped successfully'); + logger.info('Server stopped successfully'); } catch (error) { - logger.error(`❌ Failed to stop server: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to stop server: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } }, @@ -455,12 +455,12 @@ export class AddonManager { // Optionally handle dependencies after main install if (autoDeps) { - logger.info('πŸ”„ Checking and installing dependencies automatically...'); + logger.info('Checking and installing dependencies automatically...'); try { const resolution = await this.resolveDependencies(addonName); if (resolution.missing.length > 0) { logger.info( - `πŸ“‹ Found ${resolution.missing.length} missing dependencies: ${resolution.missing.join(', ')}` + `Found ${resolution.missing.length} missing dependencies: ${resolution.missing.join(', ')}` ); const autoInstallResult = await this.dependencyResolver.autoInstallDependencies( resolution, @@ -468,26 +468,26 @@ export class AddonManager { ); if (autoInstallResult.installed.length > 0) { logger.success( - `βœ… Auto-installed ${autoInstallResult.installed.length} dependencies: ${autoInstallResult.installed.join(', ')}` + `Auto-installed ${autoInstallResult.installed.length} dependencies: ${autoInstallResult.installed.join(', ')}` ); } if (autoInstallResult.failed.length > 0) { logger.warn( - `⚠️ Failed to auto-install ${autoInstallResult.failed.length} dependencies: ${autoInstallResult.failed.join(', ')}` + `Failed to auto-install ${autoInstallResult.failed.length} dependencies: ${autoInstallResult.failed.join(', ')}` ); } } else { - logger.info('βœ… All dependencies already satisfied'); + logger.info('All dependencies already satisfied'); } } catch (error) { logger.warn( - `⚠️ Auto-dependency installation failed: ${error instanceof Error ? error.message : 'unknown error'}` + `Auto-dependency installation failed: ${error instanceof Error ? error.message : 'unknown error'}` ); } } } catch (error) { logger.error( - `❌ Failed to install addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}` + `Failed to install addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}` ); throw error; } @@ -501,7 +501,7 @@ export class AddonManager { await this.installer.uninstallAddon(addonName, Boolean(_options.global)); } catch (_error) { logger.error( - `❌ Failed to uninstall addon ${addonName}: ${_error instanceof Error ? _error.message : 'unknown error'}` + `Failed to uninstall addon ${addonName}: ${_error instanceof Error ? _error.message : 'unknown error'}` ); throw _error; } @@ -532,7 +532,7 @@ export class AddonManager { return addonInfos; } catch (error) { - logger.error(`❌ Failed to list addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to list addons: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -553,10 +553,10 @@ export class AddonManager { // Addon is already loaded, just need to register hooks this.hookManager.registerAddons([addon]); - logger.success(`βœ… Enabled addon: ${addonName}`); + logger.success(`Enabled addon: ${addonName}`); } catch (error) { - logger.error(`❌ Failed to enable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to enable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -571,10 +571,10 @@ export class AddonManager { await this.loader.unloadAddon(addonName); - logger.success(`βœ… Disabled addon: ${addonName}`); + logger.success(`Disabled addon: ${addonName}`); } catch (error) { - logger.error(`❌ Failed to disable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to disable addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -584,7 +584,7 @@ export class AddonManager { */ async searchAddons(query: string, _limit: number = 10): Promise { try { - logger.detail(`πŸ” Searching for addons: "${query}"`); + logger.detail(`Searching for addons: "${query}"`); const allAddons = this.loader.getAllAddons(); const searchResults: AddonInfo[] = []; @@ -631,7 +631,7 @@ export class AddonManager { return searchResults.slice(0, _limit); } catch (error) { - logger.error(`❌ Failed to search addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to search addons: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -669,12 +669,12 @@ export class AddonManager { throw new Error(`Addon not found: ${addonName}`); } - logger.info(`πŸ“¦ Addon: ${addonInfo.name}`); + logger.info(`Addon: ${addonInfo.name}`); logger.info(` Version: ${addonInfo.version}`); logger.info(` Description: ${addonInfo.description}`); logger.info(` Author: ${addonInfo.author}`); logger.info(` License: ${addonInfo.license}`); - logger.info(` Status: ${addonInfo.enabled ? 'βœ… enabled' : '❌ disabled'}`); + logger.info(` Status: ${addonInfo.enabled ? 'enabled' : 'disabled'}`); if (addonInfo.dependencies.length > 0) { logger.info(` Dependencies: ${addonInfo.dependencies.join(', ')}`); @@ -687,7 +687,7 @@ export class AddonManager { } } catch (error) { - logger.error(`❌ Failed to get addon info for ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to get addon info for ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -700,7 +700,7 @@ export class AddonManager { await this.ensureAddonsLoaded(); await this.installer.updateGitHubAddon(addonName); } catch (error) { - logger.error(`❌ Failed to update addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to update addon ${addonName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -717,7 +717,7 @@ export class AddonManager { try { await this.installer.updateAllGitHubAddons(); } catch (error) { - logger.error(`❌ Failed to update addons: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to update addons: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } @@ -808,18 +808,18 @@ export class AddonManager { const stats = this.commandResolver.getStats(); if (stats.totalAddonCommands > 0) { - logger.info(`πŸ”Œ Registered ${stats.totalAddonCommands} addon commands`); + logger.info(`Registered ${stats.totalAddonCommands} addon commands`); if (stats.overriddenCommands.length > 0) { - logger.info(`πŸ”„ Overridden commands: ${stats.overriddenCommands.join(', ')}`); + logger.info(`Overridden commands: ${stats.overriddenCommands.join(', ')}`); } if (stats.newCommands.length > 0) { - logger.info(`βž• New commands: ${stats.newCommands.join(', ')}`); + logger.info(`New commands: ${stats.newCommands.join(', ')}`); } } } catch (error) { - logger.error(`❌ Failed to register addon commands: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to register addon commands: ${error instanceof Error ? error.message : 'unknown error'}`); } } @@ -951,7 +951,7 @@ export class AddonManager { if (addon.commands) { for (const command of addon.commands) { if (command.name === commandName) { - logger.detail(`πŸ”Œ Running addon command: ${commandName} from ${addon.name}`); + logger.detail(`Running addon command: ${commandName} from ${addon.name}`); await command.handler(args, options); commandFound = true; return; @@ -965,7 +965,7 @@ export class AddonManager { } } catch (error) { - logger.error(`❌ Failed to run addon command ${commandName}: ${error instanceof Error ? error.message : 'unknown error'}`); + logger.error(`Failed to run addon command ${commandName}: ${error instanceof Error ? error.message : 'unknown error'}`); throw error; } } diff --git a/src/core/addons/semver.ts b/src/core/addons/semver.ts index a7eac6d..53d2547 100644 --- a/src/core/addons/semver.ts +++ b/src/core/addons/semver.ts @@ -95,7 +95,7 @@ export class SemVer { // Handle simple constraints return this.satisfiesSimple(version, parsedConstraint); } catch { - logger.warn(`⚠️ Invalid version constraint: ${constraint}`); + logger.warn(`Invalid version constraint: ${constraint}`); return false; } } diff --git a/src/core/manifest.ts b/src/core/manifest.ts index 9242714..1876e55 100644 --- a/src/core/manifest.ts +++ b/src/core/manifest.ts @@ -123,13 +123,13 @@ export async function generatePackageManifest(options: { JSON.stringify(manifest, null, 2) ); - logger.info('βœ… Created pawn.json manifest file'); + logger.success('Created pawn.json manifest file'); logger.detail(`Manifest created at: ${manifestPath}`); } catch (error) { - logger.error( - `❌ Failed to create manifest file: ${error instanceof Error ? error.message : 'unknown error'}` - ); - throw error; // Re-throw to let the caller handle it + logger.error( + `Failed to create manifest file: ${error instanceof Error ? error.message : 'unknown error'}` + ); + throw error; // Re-throw to let the caller handle it } } @@ -144,16 +144,16 @@ export async function loadManifest(): Promise { } const manifestPath = path.join(tapiDir, 'pawn.json'); if (!fs.existsSync(manifestPath)) { - logger.warn('⚠️ No pawn.json manifest found in the current directory'); + logger.warn('No pawn.json manifest found in the current directory'); return null; } const data = await fs.promises.readFile(manifestPath, 'utf8'); return JSON.parse(data) as PackageManifest; } catch (error) { - logger.error( - `❌ Failed to read manifest file: ${error instanceof Error ? error.message : 'unknown error'}` - ); + logger.error( + `Failed to read manifest file: ${error instanceof Error ? error.message : 'unknown error'}` + ); return null; } } @@ -190,12 +190,12 @@ export async function updateManifest( JSON.stringify(updatedManifest, null, 2) ); - logger.info('βœ… Updated pawn.json manifest file'); + logger.success('Updated pawn.json manifest file'); return true; } catch (error) { - logger.error( - `❌ Failed to update manifest file: ${error instanceof Error ? error.message : 'unknown error'}` - ); + logger.error( + `Failed to update manifest file: ${error instanceof Error ? error.message : 'unknown error'}` + ); return false; } } diff --git a/src/index.ts b/src/index.ts index b24cb3d..fc78a64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,20 +52,20 @@ async function main() { if (isFirstRun || isSetupCommand || isHelpCommand) { showBanner(true); - } - - if (isFirstRun && !isHelpCommand && !isVersionCommand && !isSetupCommand) { - logger.info('πŸŽ‰ This appears to be your first time using tapi.'); - logger.info("Let's configure some basic settings before proceeding."); - logger.newline(); - - const setupComplete = await setupWizard(false); - - if (!setupComplete) { - logger.error('❌ Setup must be completed before using tapi.'); - process.exit(1); - } - } + } + + if (isFirstRun && !isHelpCommand && !isVersionCommand && !isSetupCommand) { + logger.info('This appears to be your first time using tapi.'); + logger.info("Let's configure some basic settings before proceeding."); + logger.newline(); + + const setupComplete = await setupWizard(false); + + if (!setupComplete) { + logger.error('Setup must be completed before using tapi.'); + process.exit(1); + } + } program.parse(process.argv); @@ -80,10 +80,10 @@ async function main() { }); } } - -main().catch((err) => { - logger.error( - `❌ Fatal error: ${err instanceof Error ? err.message : 'unknown error'}` - ); - process.exit(1); -}); + +main().catch((err) => { + logger.error( + `Fatal error: ${err instanceof Error ? err.message : 'unknown error'}` + ); + process.exit(1); +}); diff --git a/src/utils/commandWrapper.ts b/src/utils/commandWrapper.ts index 5227f43..ee6fb96 100644 --- a/src/utils/commandWrapper.ts +++ b/src/utils/commandWrapper.ts @@ -12,13 +12,13 @@ export async function withErrorHandling( ): Promise { try { return await fn(); - } catch (error) { - logger.error( - `❌ Failed to ${action}: ${error instanceof Error ? error.message : 'unknown error'}` - ); - process.exit(1); - } -} + } catch (error) { + logger.error( + `Failed to ${action}: ${error instanceof Error ? error.message : 'unknown error'}` + ); + process.exit(1); + } +} /** * Wraps command execution with error handling and optional cleanup @@ -43,10 +43,10 @@ export async function withErrorHandlingAndCleanup( `Cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : 'unknown error'}` ); } - } - logger.error( - `❌ Failed to ${action}: ${error instanceof Error ? error.message : 'unknown error'}` - ); - process.exit(1); - } -} + } + logger.error( + `Failed to ${action}: ${error instanceof Error ? error.message : 'unknown error'}` + ); + process.exit(1); + } +} diff --git a/src/utils/config.ts b/src/utils/config.ts index 343c2e2..8e92adc 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -50,7 +50,7 @@ export class ConfigManager { return {}; } catch (error) { logger.error( - `❌ Error loading config: ${error instanceof Error ? error.message : 'unknown error'}` + `Error loading config: ${error instanceof Error ? error.message : 'unknown error'}` ); return {}; } @@ -68,7 +68,7 @@ export class ConfigManager { fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2)); } catch (error) { logger.error( - `❌ Error saving config: ${error instanceof Error ? error.message : 'unknown error'}` + `Error saving config: ${error instanceof Error ? error.message : 'unknown error'}` ); } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 93ec971..f38e2b1 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,7 +1,8 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; - +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import chalk from 'chalk'; + /** * Internal set of active log file streams used for file logging. */ @@ -15,10 +16,40 @@ interface LogStreams { * and optional log file persistence. */ class Logger { - private logToFile: boolean = false; - private logStreams: LogStreams = {}; - private verbosity: 'normal' | 'verbose' | 'quiet' = 'normal'; - + private logToFile: boolean = false; + private logStreams: LogStreams = {}; + private verbosity: 'normal' | 'verbose' | 'quiet' = 'normal'; + + private readonly levelConfig = { + success: { label: '[SUCCESS]', color: chalk.green, consoleMethod: 'log' as const }, + error: { label: '[ERROR]', color: chalk.red, consoleMethod: 'error' as const }, + info: { label: '[INFO]', color: chalk.blue, consoleMethod: 'log' as const }, + routine: { label: '[STEP]', color: chalk.cyan, consoleMethod: 'log' as const }, + detail: { label: '[DETAIL]', color: chalk.gray, consoleMethod: 'log' as const }, + warn: { label: '[WARN]', color: chalk.yellow, consoleMethod: 'warn' as const }, + hint: { label: '[HINT]', color: chalk.cyan, consoleMethod: 'log' as const }, + link: { label: '[LINK]', color: chalk.cyan, consoleMethod: 'log' as const }, + working: { label: '[WORKING]', color: chalk.cyan, consoleMethod: 'log' as const }, + }; + + private emit( + level: keyof Logger['levelConfig'], + message: string, + options: { requireVerbosity?: 'verbose' } = {} + ) { + const { requireVerbosity } = options; + const config = this.levelConfig[level]; + const shouldLog = + this.verbosity !== 'quiet' && + (requireVerbosity ? this.verbosity === requireVerbosity : true); + + if (shouldLog) { + console[config.consoleMethod](`${config.color(config.label)} ${message}`); + } + + this.writeToFile(level, `${config.label} ${message}`); + } + /** * Enable writing log output to disk, either at a custom path or within the default logs directory. * @@ -126,62 +157,51 @@ class Logger { * Log a success message. */ success(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`βœ“ ${message}`); - } - this.writeToFile('success', message); - } - + this.emit('success', message); + } + /** * Log an error message. */ error(message: string) { - if (this.verbosity !== 'quiet') { - console.error(`βœ— ${message}`); - } - this.writeToFile('error', message); - } - + this.emit('error', message); + } + /** * Log an informational message. */ info(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`${message}`); - } - this.writeToFile('info', message); - } - + this.emit('info', message); + } + /** * Log a routine status message (prefixed with an arrow). */ routine(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`β†’ ${message}`); - } - this.writeToFile('routine', message); - } - + this.emit('routine', message); + } + /** * Log a detailed message when in verbose mode. */ detail(message: string) { - if (this.verbosity === 'verbose') { - console.log(` ${message}`); - } - this.writeToFile('detail', message); - } - + this.emit('detail', message, { requireVerbosity: 'verbose' }); + } + /** * Log a warning message. */ warn(message: string) { - if (this.verbosity !== 'quiet') { - console.warn(`${message}`); - } - this.writeToFile('warn', message); - } - + this.emit('warn', message); + } + + /** + * Log a helpful hint message. + */ + hint(message: string) { + this.emit('hint', message); + } + /** * Log a message with no prefix. */ @@ -207,12 +227,12 @@ class Logger { * Log a formatted heading surrounded by === markers. */ heading(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`\n=== ${message} ===`); - } - this.writeToFile('heading', message); - } - + if (this.verbosity !== 'quiet') { + console.log(`\n${chalk.bold(`=== ${message} ===`)}`); + } + this.writeToFile('heading', message); + } + /** * Log a formatted subheading surrounded by --- markers. */ @@ -228,10 +248,10 @@ class Logger { * Log a final success message preceded by a newline. */ finalSuccess(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`\n${message}`); - } - this.writeToFile('finalSuccess', message); + if (this.verbosity !== 'quiet') { + console.log(`\n${chalk.green.bold(message)}`); + } + this.writeToFile('finalSuccess', message); } // List methods @@ -252,10 +272,7 @@ class Logger { * Log a message with ellipsis for ongoing work. */ working(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`${message}...`); - } - this.writeToFile('working', message); + this.emit('working', `${message}...`); } // Key-value display @@ -275,18 +292,15 @@ class Logger { * Log a shell command (prefixed with `$`). */ command(message: string) { - if (this.verbosity !== 'quiet') { - console.log(`$ ${message}`); - } - this.writeToFile('command', message); - } - - link(url: string) { - if (this.verbosity !== 'quiet') { - console.log(`πŸ”— ${url}`); - } - this.writeToFile('link', url); - } + if (this.verbosity !== 'quiet') { + console.log(`${chalk.magenta('$')} ${message}`); + } + this.writeToFile('command', message); + } + + link(url: string) { + this.emit('link', url); + } // Clean up streams when done close() { diff --git a/src/utils/serverState.ts b/src/utils/serverState.ts index 200384b..5cf25ea 100644 --- a/src/utils/serverState.ts +++ b/src/utils/serverState.ts @@ -17,43 +17,43 @@ export interface ServerState { * Persist the provided server state so the process can be recovered/resumed. */ export function saveServerState(state: ServerState): void { - try { - configManager.saveServerState(state); - logger.detail('Server state saved'); - } catch (error) { - logger.error( - `❌ Failed to save server state: ${error instanceof Error ? error.message : 'unknown error'}` - ); - } -} + try { + configManager.saveServerState(state); + logger.detail('Server state saved'); + } catch (error) { + logger.error( + `Failed to save server state: ${error instanceof Error ? error.message : 'unknown error'}` + ); + } +} /** * Retrieve the last known server state from config. */ export function loadServerState(): ServerState { - try { - return configManager.getServerState() || {}; - } catch (error) { - logger.warn( - `⚠️ Failed to load server state: ${error instanceof Error ? error.message : 'unknown error'}` - ); - return {}; - } -} + try { + return configManager.getServerState() || {}; + } catch (error) { + logger.warn( + `Failed to load server state: ${error instanceof Error ? error.message : 'unknown error'}` + ); + return {}; + } +} /** * Remove any saved server state information. */ export function clearServerState(): void { - try { - configManager.clearServerState(); - logger.detail('Server state cleared'); - } catch (error) { - logger.warn( - `⚠️ Failed to clear server state: ${error instanceof Error ? error.message : 'unknown error'}` - ); - } -} + try { + configManager.clearServerState(); + logger.detail('Server state cleared'); + } catch (error) { + logger.warn( + `Failed to clear server state: ${error instanceof Error ? error.message : 'unknown error'}` + ); + } +} /** * Determine whether the stored server process is still active. diff --git a/src/utils/updateChecker.ts b/src/utils/updateChecker.ts index 0542c88..c9a77a3 100644 --- a/src/utils/updateChecker.ts +++ b/src/utils/updateChecker.ts @@ -88,9 +88,9 @@ export async function showUpdateNotification(): Promise { // Check for updates (uses daily cache) const result = await checkForUpdates(true); - if (result.hasUpdate && result.latestVersion) { - logger.info(''); - logger.info(`πŸ“¦ Update available: tapi ${result.latestVersion}`); + if (result.hasUpdate && result.latestVersion) { + logger.info(''); + logger.info(`Update available: tapi ${result.latestVersion}`); logger.info('Run "tapi update" to upgrade'); logger.info(`Release notes: ${result.releaseUrl}`); logger.info(''); diff --git a/tests/unit/build.test.ts b/tests/unit/build.test.ts index 77c155e..bb76a61 100644 --- a/tests/unit/build.test.ts +++ b/tests/unit/build.test.ts @@ -3,15 +3,17 @@ import * as path from 'path'; import { createTempDir } from '../setup'; jest.mock('../../src/utils/logger', () => ({ - logger: { - heading: jest.fn(), - error: jest.fn(), - success: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - detail: jest.fn(), - } -})); + logger: { + heading: jest.fn(), + error: jest.fn(), + success: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + detail: jest.fn(), + working: jest.fn(), + hint: jest.fn(), + } +})); jest.mock('../../src/utils/banner', () => ({ showBanner: jest.fn(), diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 8c03a8a..e7f44ae 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -1,11 +1,13 @@ jest.mock('../../src/utils/logger', () => ({ - logger: { - info: jest.fn(), - plain: jest.fn(), - success: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - } + logger: { + info: jest.fn(), + plain: jest.fn(), + success: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + hint: jest.fn(), + working: jest.fn(), + } })); jest.mock('../../src/utils/banner', () => ({ @@ -146,4 +148,4 @@ describe('Config Command Utilities', () => { expect(maskedToken).toBe('Not set'); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/init.test.ts b/tests/unit/init.test.ts index 304f1e1..53234ea 100644 --- a/tests/unit/init.test.ts +++ b/tests/unit/init.test.ts @@ -16,16 +16,25 @@ jest.mock('ora', () => { jest.mock('cli-progress'); jest.mock('https'); -jest.mock('../../src/utils/logger', () => ({ - logger: { - getVerbosity: jest.fn(() => 'normal'), - info: jest.fn(), - error: jest.fn(), - success: jest.fn(), - warn: jest.fn(), - detail: jest.fn(), - } -})); +jest.mock('../../src/utils/logger', () => ({ + logger: { + getVerbosity: jest.fn(() => 'normal'), + info: jest.fn(), + error: jest.fn(), + success: jest.fn(), + warn: jest.fn(), + detail: jest.fn(), + heading: jest.fn(), + subheading: jest.fn(), + list: jest.fn(), + keyValue: jest.fn(), + working: jest.fn(), + command: jest.fn(), + hint: jest.fn(), + link: jest.fn(), + routine: jest.fn(), + } +})); jest.mock('../../src/utils/banner', () => ({ showBanner: jest.fn(), diff --git a/tests/unit/install.test.ts b/tests/unit/install.test.ts index d450382..5413c0d 100644 --- a/tests/unit/install.test.ts +++ b/tests/unit/install.test.ts @@ -15,13 +15,14 @@ jest.mock('../../src/utils/logger', () => ({ info: jest.fn(), error: jest.fn(), routine: jest.fn(), - success: jest.fn(), - working: jest.fn(), - detail: jest.fn(), - warn: jest.fn(), - plain: jest.fn(), - } -})); + success: jest.fn(), + working: jest.fn(), + detail: jest.fn(), + warn: jest.fn(), + plain: jest.fn(), + hint: jest.fn(), + } +})); jest.mock('../../src/utils/githubHandler', () => ({ fetchRepoDefaultBranch: jest.fn(), @@ -230,4 +231,4 @@ describe('Install Command Utilities', () => { expect(Array.isArray(filteredResources)).toBe(true); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/kill.test.ts b/tests/unit/kill.test.ts index c70a664..c7a5366 100644 --- a/tests/unit/kill.test.ts +++ b/tests/unit/kill.test.ts @@ -1,14 +1,16 @@ // Mock dependencies jest.mock('../../src/utils/logger', () => ({ - logger: { - warn: jest.fn(), - info: jest.fn(), - error: jest.fn(), - success: jest.fn(), - newline: jest.fn(), - detail: jest.fn(), - } -})); + logger: { + warn: jest.fn(), + info: jest.fn(), + error: jest.fn(), + success: jest.fn(), + newline: jest.fn(), + detail: jest.fn(), + working: jest.fn(), + hint: jest.fn(), + } +})); jest.mock('../../src/utils/banner', () => ({ showBanner: jest.fn(), diff --git a/tests/unit/logger.test.ts b/tests/unit/logger.test.ts index d50dbd6..50c4e09 100644 --- a/tests/unit/logger.test.ts +++ b/tests/unit/logger.test.ts @@ -1,14 +1,17 @@ -import { logger } from '../../src/utils/logger'; - -describe('Logger', () => { - let consoleSpy: jest.SpyInstance; - - beforeEach(() => { - // Spy on console methods to test log output - consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - jest.spyOn(console, 'error').mockImplementation(); - jest.spyOn(console, 'warn').mockImplementation(); - }); +import { logger } from '../../src/utils/logger'; + +const stripAnsi = (value: string): string => + value.replace(/\u001b\[[0-9;]*m/g, ''); + +describe('Logger', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + // Spy on console methods to test log output + consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); + }); afterEach(() => { jest.restoreAllMocks(); @@ -106,15 +109,18 @@ describe('Logger', () => { expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Operation completed!')); }); - test('should log working messages with ellipsis', () => { - logger.working('Processing'); - expect(consoleSpy).toHaveBeenCalledWith('Processing...'); - }); - - test('should log commands with dollar sign prefix', () => { - logger.command('npm install'); - expect(consoleSpy).toHaveBeenCalledWith('$ npm install'); - }); + test('should log working messages with ellipsis', () => { + logger.working('Processing'); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Processing...')); + }); + + test('should log commands with dollar sign prefix', () => { + logger.command('npm install'); + const commandCall = consoleSpy.mock.calls.find(([arg]) => + typeof arg === 'string' && stripAnsi(arg).includes('$ npm install') + ); + expect(commandCall).toBeDefined(); + }); test('should log routine messages', () => { logger.routine('Routine operation'); @@ -126,10 +132,10 @@ describe('Logger', () => { expect(consoleSpy).toHaveBeenCalledWith('Plain message'); }); - test('should log links with link emoji', () => { - logger.link('https://example.com'); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('https://example.com')); - }); + test('should log links with formatted prefix', () => { + logger.link('https://example.com'); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('https://example.com')); + }); test('should create newlines', () => { logger.newline(); diff --git a/tests/unit/setup.test.ts b/tests/unit/setup.test.ts index 0729ea3..55d091f 100644 --- a/tests/unit/setup.test.ts +++ b/tests/unit/setup.test.ts @@ -1,14 +1,16 @@ jest.mock('../../src/utils/logger', () => ({ - logger: { - heading: jest.fn(), - info: jest.fn(), - subheading: jest.fn(), - keyValue: jest.fn(), - newline: jest.fn(), - success: jest.fn(), - error: jest.fn(), - } -})); + logger: { + heading: jest.fn(), + info: jest.fn(), + subheading: jest.fn(), + keyValue: jest.fn(), + newline: jest.fn(), + success: jest.fn(), + error: jest.fn(), + hint: jest.fn(), + warn: jest.fn(), + } +})); jest.mock('../../src/utils/banner', () => ({ showBanner: jest.fn(), diff --git a/tests/unit/start.test.ts b/tests/unit/start.test.ts index 446661e..3693a29 100644 --- a/tests/unit/start.test.ts +++ b/tests/unit/start.test.ts @@ -6,16 +6,21 @@ import { createTempDir } from '../setup'; jest.mock('child_process'); jest.mock('chokidar'); jest.mock('../../src/utils/logger', () => ({ - logger: { - heading: jest.fn(), - info: jest.fn(), - error: jest.fn(), - success: jest.fn(), - warn: jest.fn(), - detail: jest.fn(), - newline: jest.fn(), - } -})); + logger: { + heading: jest.fn(), + info: jest.fn(), + error: jest.fn(), + success: jest.fn(), + warn: jest.fn(), + detail: jest.fn(), + newline: jest.fn(), + working: jest.fn(), + hint: jest.fn(), + link: jest.fn(), + command: jest.fn(), + routine: jest.fn(), + } +})); jest.mock('../../src/utils/banner', () => ({ showBanner: jest.fn(), diff --git a/tests/unit/uninstall.test.ts b/tests/unit/uninstall.test.ts index 26787b6..e41255e 100644 --- a/tests/unit/uninstall.test.ts +++ b/tests/unit/uninstall.test.ts @@ -8,7 +8,16 @@ import { createUninstallCommand } from '../../src/commands/uninstall/uninstall'; // Mock dependencies jest.mock('fs'); jest.mock('os'); -jest.mock('../../src/utils/logger'); +jest.mock('../../src/utils/logger', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + success: jest.fn(), + hint: jest.fn(), + working: jest.fn(), + }, +})); jest.mock('@inquirer/prompts'); const mockFs = fs as jest.Mocked; From 8d8fa3f3ca9b41f0e50981dcddc3a732e6d3dc7b Mon Sep 17 00:00:00 2001 From: itsneufox Date: Fri, 17 Oct 2025 16:43:29 +0100 Subject: [PATCH 11/20] enhance compiler setup with standard library detection and improved logging verbosity --- src/commands/init/compiler.ts | 129 ++++++++++------ src/commands/init/git.ts | 44 +++--- src/commands/init/projectStructure.ts | 4 +- src/commands/init/prompts.ts | 60 +++++--- src/commands/init/serverDownload.ts | 211 ++++++++++++++------------ src/commands/init/setup.ts | 114 +++++++++----- src/commands/init/utils.ts | 46 +++--- tests/unit/setup.test.ts | 2 + 8 files changed, 367 insertions(+), 243 deletions(-) diff --git a/src/commands/init/compiler.ts b/src/commands/init/compiler.ts index 92e2cf9..3c8771c 100644 --- a/src/commands/init/compiler.ts +++ b/src/commands/init/compiler.ts @@ -1,11 +1,41 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as https from 'https'; -import simpleGit from 'simple-git'; -import { logger } from '../../utils/logger'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as https from 'https'; +import simpleGit from 'simple-git'; +import { logger } from '../../utils/logger'; import { CompilerAnswers } from './types'; import { downloadFileWithProgress } from './serverDownload'; +const STD_LIB_SENTINELS = ['a_samp.inc', 'open.mp.inc', 'omp.inc', 'open.mp']; + +/** + * Detect whether a standard library already exists in common include locations. + */ +export function hasExistingStandardLibrary(): boolean { + const candidates = [ + path.join(process.cwd(), 'qawno', 'include'), + path.join(process.cwd(), 'compiler', 'include'), + path.join(process.cwd(), 'includes'), + ]; + + for (const dir of candidates) { + if (!fs.existsSync(dir)) continue; + try { + const files = fs.readdirSync(dir); + const hasStdLib = files.some((file) => { + return STD_LIB_SENTINELS.includes(file.toLowerCase()); + }); + if (hasStdLib) { + return true; + } + } catch { + // Ignore unreadable directories + } + } + + return false; +} + /** * Install the compiler and standard library based on setup answers. */ @@ -20,9 +50,9 @@ export async function setupCompiler( compilerAnswers.installCompilerFolder || false, compilerAnswers.downgradeQawno || false ); - if (logger.getVerbosity() !== 'quiet') { - logger.success('Compiler installed'); - } + if (logger.getVerbosity() === 'verbose') { + logger.success('Compiler installed'); + } } catch { // error handled within download function } @@ -31,8 +61,8 @@ export async function setupCompiler( if (compilerAnswers.downloadStdLib) { try { await downloadopenmpStdLib(); - if (logger.getVerbosity() !== 'quiet') { - logger.success('Standard library installed'); + if (logger.getVerbosity() === 'verbose') { + logger.success('Standard library installed'); } } catch { // error handled within download function @@ -219,21 +249,22 @@ export async function downloadopenmpStdLib( } // Check if standard library files already exist - const files = fs.readdirSync(includesDir); - const hasStdLibFiles = files.some( - (file) => - file === 'a_samp.inc' || file === 'open.mp' || file.endsWith('.inc') - ); - - if (hasStdLibFiles) { - logger.info( - `Standard library files already exist in ${includesDirName}, skipping download` - ); - logger.info( - 'If you want to update the standard library, please remove existing .inc files first' - ); - return; - } + const files = fs.readdirSync(includesDir); + const hasStdLibFiles = files.some((file) => + STD_LIB_SENTINELS.includes(file.toLowerCase()) + ); + + if (hasStdLibFiles) { + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Standard library files already exist in ${includesDirName}, skipping download` + ); + logger.detail( + 'If you want to update the standard library, please remove existing .inc files first' + ); + } + return; + } const git = simpleGit(); await git.clone( @@ -360,9 +391,9 @@ export async function extractCompilerPackage( // Handle qawno const qawnoDir = path.join(process.cwd(), 'qawno'); if (!keepQawno && fs.existsSync(qawnoDir)) { - if (logger.getVerbosity() === 'verbose') { - logger.routine('Removing existing qawno directory'); - } + if (logger.getVerbosity() === 'verbose') { + logger.detail('Removing existing qawno directory'); + } fs.rmSync(qawnoDir, { recursive: true, force: true }); if (logger.getVerbosity() === 'verbose') { logger.detail('Existing qawno directory removed'); @@ -385,7 +416,9 @@ export async function extractCompilerPackage( ); installations.push('qawno/ (updated)'); } else { - logger.routine('Preserving existing qawno/ compiler'); + if (logger.getVerbosity() === 'verbose') { + logger.detail('Preserving existing qawno/ compiler'); + } installations.push('qawno/ (preserved)'); } } @@ -403,12 +436,12 @@ export async function extractCompilerPackage( installations.push('compiler/'); } - // Show installation summary - if (logger.getVerbosity() !== 'quiet') { - logger.newline(); - logger.subheading('Compiler installation summary:'); - logger.keyValue('Result', installations.join(', ')); - } + // Show installation summary when verbosity is high + if (logger.getVerbosity() === 'verbose') { + logger.newline(); + logger.subheading('Compiler installation summary:'); + logger.keyValue('Result', installations.join(', ')); + } } catch (error) { logger.error( `Failed to extract compiler package: ${error instanceof Error ? error.message : 'unknown error'}` @@ -513,19 +546,19 @@ async function installCompilerFiles( } if (overwrite || copiedFiles > 0) { - if (logger.getVerbosity() === 'verbose') { - logger.routine( - `Installed ${copiedFiles} compiler files to ${targetDescription}` - ); - } - } - if (skippedFiles > 0) { - if (logger.getVerbosity() === 'verbose') { - logger.routine( - `Preserved ${skippedFiles} existing files in ${targetDescription}` - ); - } - } + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Installed ${copiedFiles} compiler files to ${targetDescription}` + ); + } + } + if (skippedFiles > 0) { + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Preserved ${skippedFiles} existing files in ${targetDescription}` + ); + } + } return copiedFiles; } diff --git a/src/commands/init/git.ts b/src/commands/init/git.ts index f52754a..98d7769 100644 --- a/src/commands/init/git.ts +++ b/src/commands/init/git.ts @@ -34,27 +34,31 @@ export async function initGitRepository(): Promise { ); } - fs.writeFileSync( - path.join(process.cwd(), '.gitignore'), - gitignoreContent.trim() - ); - logger.routine( - 'Created .gitignore file with common PAWN-specific entries' - ); - - try { - await git.add('.'); - await git.commit('Initial commit: Initialize project structure', { - '--no-gpg-sign': null, - }); - logger.routine('Created initial Git commit'); + fs.writeFileSync( + path.join(process.cwd(), '.gitignore'), + gitignoreContent.trim() + ); + if (logger.getVerbosity() === 'verbose') { + logger.detail( + 'Created .gitignore file with common PAWN-specific entries' + ); + } + + try { + await git.add('.'); + await git.commit('Initial commit: Initialize project structure', { + '--no-gpg-sign': null, + }); + if (logger.getVerbosity() === 'verbose') { + logger.detail('Created initial Git commit'); + } } catch (commitError) { - logger.warn( - 'Could not create initial commit. You may need to commit the changes manually.' - ); - logger.warn( - `Git commit error: ${commitError instanceof Error ? commitError.message : 'unknown error'}` - ); + logger.warn( + 'Could not create initial commit. You may need to commit the changes manually.' + ); + logger.warn( + `Git commit error: ${commitError instanceof Error ? commitError.message : 'unknown error'}` + ); } } catch (gitignoreError) { logger.warn( diff --git a/src/commands/init/projectStructure.ts b/src/commands/init/projectStructure.ts index 20f7c91..0e5484c 100644 --- a/src/commands/init/projectStructure.ts +++ b/src/commands/init/projectStructure.ts @@ -165,8 +165,8 @@ export async function setupProjectStructure( } } - // Summary at normal level - if (logger.getVerbosity() !== 'quiet') { + // Summary at verbose level + if (logger.getVerbosity() === 'verbose') { logger.success('Project files and structure created'); } } diff --git a/src/commands/init/prompts.ts b/src/commands/init/prompts.ts index 7fe1369..ebcf1b4 100644 --- a/src/commands/init/prompts.ts +++ b/src/commands/init/prompts.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { input, select, confirm } from '@inquirer/prompts'; import { configManager } from '../../utils/config'; import { logger } from '../../utils/logger'; -import { getLatestCompilerVersion } from './compiler'; +import { getLatestCompilerVersion, hasExistingStandardLibrary } from './compiler'; import { CommandOptions, InitialAnswers, CompilerAnswers } from './types'; /** @@ -90,24 +90,31 @@ export async function promptForCompilerOptions(isLegacySamp: boolean = false): P }) : true; - let compilerVersion = 'latest'; - let keepQawno = true; - let installCompilerFolder = false; - let useCompilerFolder = false; - let downloadStdLib = true; - let downgradeQawno = false; - - if (!downloadCompiler) { - // If not downloading, just ask about stdlib - downloadStdLib = await confirm({ - message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, - default: true, - }); - return { - downloadCompiler, - compilerVersion, - keepQawno, - downgradeQawno, + const stdLibAlreadyPresent = hasExistingStandardLibrary(); + let compilerVersion = 'latest'; + let keepQawno = true; + let installCompilerFolder = false; + let useCompilerFolder = false; + let downloadStdLib = !stdLibAlreadyPresent; + let downgradeQawno = false; + + if (!downloadCompiler) { + if (!stdLibAlreadyPresent) { + downloadStdLib = await confirm({ + message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, + default: true, + }); + } else if (logger.getVerbosity() !== 'quiet') { + logger.hint( + 'Standard library detected. Skipping download β€” remove existing includes if you need a fresh copy.' + ); + downloadStdLib = false; + } + return { + downloadCompiler, + compilerVersion, + keepQawno, + downgradeQawno, installCompilerFolder, useCompilerFolder, downloadStdLib, @@ -212,10 +219,17 @@ export async function promptForCompilerOptions(isLegacySamp: boolean = false): P }); } - downloadStdLib = await confirm({ - message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, - default: true, - }); + if (!stdLibAlreadyPresent) { + downloadStdLib = await confirm({ + message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, + default: true, + }); + } else if (logger.getVerbosity() !== 'quiet') { + logger.hint( + 'Standard library detected. Skipping download β€” remove existing includes if you need a fresh copy.' + ); + downloadStdLib = false; + } return { downloadCompiler, diff --git a/src/commands/init/serverDownload.ts b/src/commands/init/serverDownload.ts index af25689..ecbf9e9 100644 --- a/src/commands/init/serverDownload.ts +++ b/src/commands/init/serverDownload.ts @@ -1,17 +1,22 @@ import * as fs from 'fs'; import * as path from 'path'; import * as https from 'https'; -import * as cliProgress from 'cli-progress'; +import * as cliProgress from 'cli-progress'; import { logger } from '../../utils/logger'; import { createSpinner } from './utils'; +export type ServerInstallationSummary = { + executable: string; + config: string; +}; + /** * Download and extract the latest open.mp server build for the project. */ export async function downloadopenmpServer( versionInput: string, directories: string[] -): Promise { +): Promise { return downloadServer(versionInput, directories, false); } @@ -21,21 +26,24 @@ export async function downloadopenmpServer( export async function downloadSampServer( versionInput: string, directories: string[] -): Promise { +): Promise { return downloadServer(versionInput, directories, true); } - -async function downloadServer( - versionInput: string, - directories: string[], - isLegacySamp: boolean -): Promise { - const serverType = isLegacySamp ? 'SA-MP' : 'open.mp'; - const spinner = createSpinner(`Fetching latest ${serverType} version...`); - - try { - const version = versionInput === 'latest' - ? (isLegacySamp ? await getLatestSampVersion() : await getLatestopenmpVersion()) + +async function downloadServer( + versionInput: string, + directories: string[], + isLegacySamp: boolean +): Promise { + const serverType = isLegacySamp ? 'SA-MP' : 'open.mp'; + const verbosity = logger.getVerbosity(); + const isQuiet = verbosity === 'quiet'; + const isVerbose = verbosity === 'verbose'; + const spinner = createSpinner(`Fetching latest ${serverType} version...`); + + try { + const version = versionInput === 'latest' + ? (isLegacySamp ? await getLatestSampVersion() : await getLatestopenmpVersion()) : versionInput; spinner.succeed(`Found ${serverType} version ${version}`); @@ -63,42 +71,50 @@ async function downloadServer( downloadUrl = `https://github.com/openmultiplayer/open.mp/releases/download/${version}/open.mp-linux-x86.tar.gz`; filename = 'open.mp-linux-x86.tar.gz'; } else { - throw new Error(`Unsupported platform: ${platform}`); - } - } - - if (logger.getVerbosity() !== 'quiet') { - logger.routine(`Downloading server package...`); - } - await downloadFileWithProgress(downloadUrl, filename); - - const extractSpinner = createSpinner('Extracting server package...'); - await extractServerPackage(path.join(process.cwd(), filename), directories); - extractSpinner.succeed(); - - if (logger.getVerbosity() !== 'quiet') { - logger.finalSuccess('Server installation complete!'); - logger.keyValue( - 'Server executable', - platform === 'win32' - ? (isLegacySamp ? 'samp-server.exe' : 'omp-server.exe') - : (isLegacySamp ? 'samp-server' : 'omp-server') - ); - logger.keyValue('Configuration', isLegacySamp ? 'server.cfg' : 'config.json'); - } - } catch (error) { - spinner.fail(); - logger.error( - `Failed to download server package: ${error instanceof Error ? error.message : 'unknown error'}` - ); + throw new Error(`Unsupported platform: ${platform}`); + } + } + + if (!isQuiet) { + logger.working('Downloading server package'); + } + await downloadFileWithProgress(downloadUrl, filename); + + const extractSpinner = createSpinner('Extracting server package...'); + await extractServerPackage(path.join(process.cwd(), filename), directories); + extractSpinner.succeed(); + + const summary: ServerInstallationSummary = { + executable: + platform === 'win32' + ? isLegacySamp + ? 'samp-server.exe' + : 'omp-server.exe' + : isLegacySamp + ? 'samp-server' + : 'omp-server', + config: isLegacySamp ? 'server.cfg' : 'config.json', + }; + + if (isVerbose) { + logger.detail(`Server package installed (${summary.executable})`); + logger.detail(`Configuration file: ${summary.config}`); + } + + return summary; + } catch (error) { + spinner.fail(); + logger.error( + `Failed to download server package: ${error instanceof Error ? error.message : 'unknown error'}` + ); if (logger.getVerbosity() !== 'quiet') { logger.newline(); logger.subheading('Manual download:'); logger.link('https://github.com/openmultiplayer/open.mp/releases'); } - throw error; - } -} + throw error; + } +} /** * Download a file from GitHub (handling redirects) while showing a progress bar. @@ -107,33 +123,34 @@ export async function downloadFileWithProgress( url: string, filename: string ): Promise { - return new Promise((resolve, reject) => { - const filePath = path.join(process.cwd(), filename); - const file = fs.createWriteStream(filePath); - - const progressBar = new cliProgress.SingleBar({ - format: - 'Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} KB', - barCompleteChar: 'β–ˆ', - barIncompleteChar: 'β–‘', - hideCursor: true, - }); - - let receivedBytes = 0; - let totalBytes = 0; - - const req = https - .get(url, { timeout: 10000 }, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - if (response.headers.location) { - logger.routine( - `Following redirect to ${response.headers.location}` - ); - req.destroy(); - - const redirectReq = https - .get( - response.headers.location, + return new Promise((resolve, reject) => { + const filePath = path.join(process.cwd(), filename); + const file = fs.createWriteStream(filePath); + + const progressBar = new cliProgress.SingleBar({ + format: + 'Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} KB', + barCompleteChar: 'β–ˆ', + barIncompleteChar: 'β–‘', + hideCursor: true, + }); + + let receivedBytes = 0; + let totalBytes = 0; + const verbose = logger.getVerbosity() === 'verbose'; + + const req = https + .get(url, { timeout: 10000 }, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + if (response.headers.location) { + if (verbose) { + logger.detail(`Following redirect to ${response.headers.location}`); + } + req.destroy(); + + const redirectReq = https + .get( + response.headers.location, { timeout: 10000 }, (redirectResponse) => { if (redirectResponse.headers['content-length']) { @@ -153,16 +170,18 @@ export async function downloadFileWithProgress( } }); - file.on('finish', () => { - progressBar.stop(); - file.close(); - redirectReq.destroy(); - logger.routine(`Server package downloaded to ${filename}`); - resolve(); - }); - } - ) - .on('error', (err) => { + file.on('finish', () => { + progressBar.stop(); + file.close(); + redirectReq.destroy(); + if (verbose) { + logger.detail(`Server package downloaded to ${filename}`); + } + resolve(); + }); + } + ) + .on('error', (err) => { progressBar.stop(); file.close(); fs.unlink(filePath, () => {}); @@ -184,15 +203,17 @@ export async function downloadFileWithProgress( } }); - file.on('finish', () => { - progressBar.stop(); - file.close(); - req.destroy(); - logger.routine(`Server package downloaded to ${filename}`); - resolve(); - }); - } else { - progressBar.stop(); + file.on('finish', () => { + progressBar.stop(); + file.close(); + req.destroy(); + if (verbose) { + logger.detail(`Server package downloaded to ${filename}`); + } + resolve(); + }); + } else { + progressBar.stop(); file.close(); req.destroy(); fs.unlink(filePath, () => {}); @@ -279,9 +300,9 @@ export async function extractServerPackage( } } - if (logger.getVerbosity() === 'verbose') { - logger.routine(`Creating temporary extract directory at ${extractDir}`); - } + if (logger.getVerbosity() === 'verbose') { + logger.detail(`Creating temporary extract directory at ${extractDir}`); + } fs.mkdirSync(extractDir, { recursive: true }); if (filePath.endsWith('.zip')) { diff --git a/src/commands/init/setup.ts b/src/commands/init/setup.ts index 2cd4ab6..d005ab7 100644 --- a/src/commands/init/setup.ts +++ b/src/commands/init/setup.ts @@ -7,7 +7,11 @@ import { promptForInitialOptions, promptForCompilerOptions } from './prompts'; import { confirm, select, checkbox } from '@inquirer/prompts'; import { setupProjectStructure } from './projectStructure'; import { setupCompiler } from './compiler'; -import { downloadopenmpServer, downloadSampServer } from './serverDownload'; +import { + downloadopenmpServer, + downloadSampServer, + ServerInstallationSummary, +} from './serverDownload'; import { cleanupGamemodeFiles, cleanupFiles, createSpinner } from './utils'; interface ConflictResolution { @@ -482,6 +486,26 @@ export async function setupInitCommand(options: CommandOptions): Promise { detectedInitGit = fs.existsSync(path.join(process.cwd(), '.git')); } + const verbosity = logger.getVerbosity(); + const isQuiet = verbosity === 'quiet'; + const isVerbose = verbosity === 'verbose'; + + const announceStep = (step: number, label: string) => { + if (isQuiet) return; + logger.working(`Step ${step}/5: ${label}`); + }; + + const completeStep = (message: string) => { + if (isQuiet) return; + logger.success(message); + }; + + const shareDetail = (message: string) => { + if (isVerbose) { + logger.detail(message); + } + }; + try { // Auto-detect server type from package, or use command line option const isLegacySamp = serverPackage.type === 'samp' ? true : @@ -495,29 +519,26 @@ export async function setupInitCommand(options: CommandOptions): Promise { logger.heading(`Initializing new ${serverTypeText} project...`); } - // Show initialization progress overview - logger.info('Initialization Progress:'); - logger.info(' [1/5] Project configuration'); - logger.info(' [2/5] Directory structure setup'); - logger.info(' [3/5] Compiler configuration'); - logger.info(' [4/5] Server package setup'); - logger.info(' [5/5] Final setup and cleanup'); - logger.newline(); + if (!isQuiet) { + logger.info('Starting initialization (5 steps)...'); + if (isVerbose) { + logger.detail('Steps: configuration β†’ project files β†’ compiler β†’ server config β†’ cleanup'); + } + } // Step 1: Project Configuration - logger.routine('[1/5] Gathering project configuration...'); + announceStep(1, 'Project configuration'); const initialAnswers = await promptForInitialOptions({ ...options, name: detectedName || options.name, initGit: detectedInitGit, }); - logger.success('Project configuration complete'); - logger.newline(); + completeStep('Configuration saved'); // Step 2: Directory Structure Setup - logger.routine('[2/5] Creating directory structure...'); + announceStep(2, 'Preparing project files'); await setupProjectStructure(initialAnswers, isLegacySamp); - logger.success('Directory structure created'); + let serverInstallSummary: ServerInstallationSummary | undefined; configManager.setEditor(initialAnswers.editor); @@ -537,22 +558,31 @@ export async function setupInitCommand(options: CommandOptions): Promise { 'plugins', 'scriptfiles', ]; - if (isLegacySamp) { - await downloadSampServer('latest', directories); - } else { - await downloadopenmpServer('latest', directories); - } + serverInstallSummary = isLegacySamp + ? await downloadSampServer('latest', directories) + : await downloadopenmpServer('latest', directories); } catch { // Error handling inside downloadopenmpServer } } + const projectFilesMessage = (() => { + if (!initialAnswers.downloadServer) { + return 'Project files ready'; + } + if (serverInstallSummary?.executable) { + return `Project files ready (server: ${serverInstallSummary.executable})`; + } + return 'Project files ready (server download skipped)'; + })(); + completeStep(projectFilesMessage); + // Step 3: Compiler Configuration - logger.routine('[3/5] Setting up PAWN compiler...'); + announceStep(3, 'Configuring compiler tools'); let compilerAnswers: CompilerAnswers; if (options.skipCompiler) { - logger.routine(' Skipping compiler setup (--skip-compiler)'); + shareDetail('Skipping compiler setup (--skip-compiler)'); compilerAnswers = { downloadCompiler: false, compilerVersion: 'latest', @@ -582,12 +612,12 @@ export async function setupInitCommand(options: CommandOptions): Promise { }); } await setupCompiler(compilerAnswers); - logger.success('Compiler configuration complete'); + completeStep('Compiler tools configured'); // Step 4: Server Configuration - logger.routine('[4/5] Updating server configuration...'); + announceStep(4, 'Updating server configuration'); await updateServerConfiguration(initialAnswers.name, isLegacySamp); - logger.success('Server configuration updated'); + completeStep('Server configuration updated'); const answers = { ...initialAnswers, @@ -595,7 +625,7 @@ export async function setupInitCommand(options: CommandOptions): Promise { }; // Step 5: Final Setup and Cleanup - logger.routine('[5/5] Finalizing project setup...'); + announceStep(5, 'Final cleanup'); setTimeout(() => { const cleanupSpinner = createSpinner('Performing final cleanup...'); @@ -719,25 +749,29 @@ async function updateServerConfiguration(projectName: string, isLegacySamp: bool * Display a friendly summary of next steps after initialization completes. */ function showSuccessInfo(answers: InitialAnswers & CompilerAnswers): void { - logger.success('Project initialization complete!'); - logger.newline(); - logger.finalSuccess('Your project is ready to go!'); + logger.finalSuccess('Project initialization complete!'); + + const verbosity = logger.getVerbosity(); + if (verbosity === 'quiet') { + return; + } - if (logger.getVerbosity() !== 'quiet') { + const projectFile = `${answers.projectType === 'gamemode' ? 'gamemodes/' : answers.projectType === 'filterscript' ? 'filterscripts/' : 'includes/'}${answers.name}.${answers.projectType === 'library' ? 'inc' : 'pwn'}`; + + if (verbosity === 'verbose') { logger.newline(); logger.subheading('Project Structure Created:'); - const projectFile = `${answers.projectType === 'gamemode' ? 'gamemodes/' : answers.projectType === 'filterscript' ? 'filterscripts/' : 'includes/'}${answers.name}.${answers.projectType === 'library' ? 'inc' : 'pwn'}`; logger.list([ `${projectFile} - Your main ${answers.projectType} file`, 'gamemodes/ - Server gamemodes directory', - 'filterscripts/ - Server filterscripts directory', + 'filterscripts/ - Server filterscripts directory', 'includes/ - Custom include files', 'plugins/ - Server plugins directory', 'scriptfiles/ - Server data files', ...(answers.editor === 'VS Code' ? ['.vscode/ - VS Code configuration'] : []), ...(answers.initGit ? ['.git/ - Git repository initialized'] : []), ]); - + logger.newline(); logger.subheading('Quick Start Commands:'); logger.list([ @@ -752,7 +786,7 @@ function showSuccessInfo(answers: InitialAnswers & CompilerAnswers): void { ] : []), ]); - + if (answers.initGit) { logger.newline(); logger.subheading('Git Repository:'); @@ -764,7 +798,17 @@ function showSuccessInfo(answers: InitialAnswers & CompilerAnswers): void { } logger.newline(); - logger.info('Need help? Run "tapi --help" for available commands'); - logger.info('Documentation: https://github.com/your-org/tapi'); + logger.hint('Need help? Run "tapi --help" for available commands'); + logger.hint('Documentation: https://github.com/your-org/tapi'); + return; + } + + logger.newline(); + logger.hint(`Edit your main script: ${projectFile}`); + logger.hint('Build: tapi build'); + logger.hint('Start server: tapi start'); + if (answers.editor === 'VS Code') { + logger.hint('VS Code build task: Ctrl+Shift+B'); } + logger.hint('Need help? Run "tapi --help" or visit the docs.'); } diff --git a/src/commands/init/utils.ts b/src/commands/init/utils.ts index 4d447c3..e576368 100644 --- a/src/commands/init/utils.ts +++ b/src/commands/init/utils.ts @@ -108,10 +108,12 @@ export function cleanupGamemodeFiles(workingFile: string): void { } } - if (removedCount > 0) { - logger.routine( - `Cleaned up gamemodes directory (removed ${removedCount} files)` - ); + if (removedCount > 0) { + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Cleaned up gamemodes directory (removed ${removedCount} files)` + ); + } } } catch (err) { logger.warn( @@ -296,11 +298,11 @@ export async function downloadFileWithProgress( .get(url, { timeout: 10000 }, (response: IncomingMessage) => { if (response.statusCode === 302 || response.statusCode === 301) { if (response.headers.location) { - if (logger.getVerbosity() === 'verbose') { - logger.routine( - `Following redirect to ${response.headers.location}` - ); - } + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Following redirect to ${response.headers.location}` + ); + } req.destroy(); @@ -327,12 +329,14 @@ export async function downloadFileWithProgress( }); file.on('finish', () => { - progressBar.stop(); - file.close(); - redirectReq.destroy(); - logger.routine(`File downloaded to ${filename}`); - resolve(); - }); + progressBar.stop(); + file.close(); + redirectReq.destroy(); + if (logger.getVerbosity() === 'verbose') { + logger.detail(`File downloaded to ${filename}`); + } + resolve(); + }); } ) .on('error', (err: Error) => { @@ -358,11 +362,13 @@ export async function downloadFileWithProgress( }); file.on('finish', () => { - progressBar.stop(); - file.close(); - req.destroy(); - logger.routine(`File downloaded to ${filename}`); - resolve(); + progressBar.stop(); + file.close(); + req.destroy(); + if (logger.getVerbosity() === 'verbose') { + logger.detail(`File downloaded to ${filename}`); + } + resolve(); }); } else { progressBar.stop(); diff --git a/tests/unit/setup.test.ts b/tests/unit/setup.test.ts index 55d091f..d892e83 100644 --- a/tests/unit/setup.test.ts +++ b/tests/unit/setup.test.ts @@ -9,6 +9,8 @@ jest.mock('../../src/utils/logger', () => ({ error: jest.fn(), hint: jest.fn(), warn: jest.fn(), + working: jest.fn(), + detail: jest.fn(), } })); From 4470981c9609c13a5ddea4f703fcef9edfc7460d Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 10:59:10 +0100 Subject: [PATCH 12/20] refactor: all README --- README.md | 237 ++++++++------- src/commands/addons/README.md | 264 +++-------------- src/commands/build/README.md | 456 ++--------------------------- src/commands/config/README.md | 372 ++---------------------- src/commands/init/README.md | 488 ++++++++----------------------- src/commands/install/README.md | 499 +++---------------------------- src/commands/kill/README.md | 172 +---------- src/commands/setup/README.md | 394 +------------------------ src/commands/start/README.md | 516 ++------------------------------- src/commands/update/README.md | 343 +--------------------- 10 files changed, 437 insertions(+), 3304 deletions(-) diff --git a/README.md b/README.md index 910c785..14a0b82 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,134 @@ -# tapi - -A command-line tool that doesn't suck for SA-MP and open.mp development. - -## What's this? - -Tired of wrestling with PAWN compiler setups, server configurations, and the general pain of SA-MP/open.mp development? Yeah, me too. That's why tapi exists. - -It's basically a CLI that handles all the boring stuff so you can focus on actually writing code. - -## Why should I care? - -- **No more manual setup hell** - `tapi init` and you're coding in seconds -- **Server runs in your terminal** - like a real dev server, Ctrl+C to stop -- **Actually works cross-platform** - Windows, Linux, macOS, whatever -- **Doesn't hijack your workflow** - uses standard server configs, no lock-in -- **Clean output** - no spam unless you want it (`--verbose`) - -## Getting started - -```bash -# Set it up (just once) -tapi setup - -# Make a project -mkdir my-gamemode && cd my-gamemode -tapi init - -# Code, build, run -tapi build -tapi start # Ctrl+C to stop, that's it -``` - -## Commands - -| Command | What it does | Docs | -|---------|--------------|------| -| `setup` | First-time configuration | [πŸ“–](src/commands/setup/README.md) | -| `init` | Create new projects | [πŸ“–](src/commands/init/README.md) | -| `build` | Compile your PAWN code | [πŸ“–](src/commands/build/README.md) | -| `start` | Run the server | [πŸ“–](src/commands/start/README.md) | -| `config` | Change settings | [πŸ“–](src/commands/config/README.md) | -| `install` | Grab packages from GitHub | [πŸ“–](src/commands/install/README.md) | -| `kill` | Emergency cleanup | [πŸ“–](src/commands/kill/README.md) | - -## Global options - -These work with any command: - -| Option | What it does | -|--------|--------------| -| `-v, --verbose` | Show detailed debug output | -| `-q, --quiet` | Minimize console output (show only progress bars) | -| `--log-to-file` | Save logs to file for debugging | - -## Daily workflow - -```bash -# Start project -tapi init - -# Work on code... -# (edit your .pwn files) - -# Test it -tapi build -tapi start # server runs right here in terminal - -# Install some library -tapi install openmultiplayer/omp-stdlib - -# Debug something? Save logs to file -tapi --log-to-file build --verbose - -# Back to coding... -``` - -## What you get - -When you run `init`, you get a complete setup: -- Server (SA-MP or open.mp, your choice) -- PAWN compiler that actually works -- Proper folder structure -- VS Code integration if you want it -- Git repo with sensible .gitignore -- No weird custom configs - just standard server files - -## Requirements - -- Node.js (recent version) -- That's it - -## Contributing - -Found a bug? Got an idea? Cool, let's talk. Check the command docs to see what needs work. - -## Status - -This is real software that works, but it's still evolving. Don't put it on your production server (yet), but it's great for development. - ---- - -*Stop fighting with tooling, start writing gamemodes.* \ No newline at end of file +# tapi + +A companion CLI for SA-MP and open.mp projects that finally treats PAWN development like modern tooling. + +## Highlights + +- **One-command project bootstrap** – `tapi init` scaffolds folders, compiler, server binaries, git, editor tasks. +- **Batteries-included build & run** – `tapi build` and `tapi start` reuse your existing configs; no magic wrappers. +- **Cross-platform by default** – Windows, Linux, macOS; same commands, same output. +- **Extensible via workflows** – drop `.tapi/workflows/init*.yml` in your repo to automate prompts, downloads, or company defaults. +- **Verbose when you need it, quiet otherwise** – consistent logging with `--verbose`, `--quiet`, and file logging. + +## Install + +> npm package availability is still in progress. For now, clone the repo or link the CLI locally. + +```bash +# clone & link for local development +git clone https://github.com/itsneufox/tapi.git +cd tapi +npm install +npm run build +npm run dev # equivalent to `npm link` +``` + +After linking you can run `tapi` from any directory. + +## Quick start + +```bash +# set up global preferences once +$ tapi setup + +# start a new project +dir my-gamemode && cd my-gamemode +$ tapi init + +# code, rebuild, run +$ tapi build +$ tapi start # Ctrl+C to stop +``` + +## Commands + +| Command | Purpose | Docs | +|---------|---------|------| +| `setup` | First-time configuration | [πŸ“–](src/commands/setup/README.md) | +| `init` | Create projects or reuse workflows | [πŸ“–](src/commands/init/README.md) | +| `build` | Compile PAWN sources | [πŸ“–](src/commands/build/README.md) | +| `start` | Launch the server (watch mode supported) | [πŸ“–](src/commands/start/README.md) | +| `config` | Update saved preferences | [πŸ“–](src/commands/config/README.md) | +| `install` | Fetch addons/packages | [πŸ“–](src/commands/install/README.md) | +| `kill` | Forcefully stop running servers | [πŸ“–](src/commands/kill/README.md) | + +Global flags available everywhere: `--verbose`, `--quiet`, and `--log-to-file`. + +## Workflows & presets + +Automate init (and future commands) with workflow files located at: + +``` +.tapi/workflows/init.yml # default +.tapi/workflows/init-team.yml # invoked via `tapi init --preset team` +.tapi/workflows/init/team.yml # alternative naming +``` + +See [Init presets](docs/init-presets.md) for the full schema, placeholder list, and automation examples (custom download URLs, chained defaults, non-interactive runs). + +```yaml +# .tapi/workflows/init-team.yml +description: Team defaults for staging servers +project: + name: ${folder} + description: "${user}'s staging project" + initGit: true + downloadServer: true +compiler: + downloadCompiler: true + compilerDownloadUrl: "https://internal.example.com/compiler-${platform}.zip?token=${env:TAPI_TOKEN}" + downloadStdLib: true + stdLibDownloadUrl: "https://internal.example.com/includes.zip" +options: + skipCompiler: false +acceptPreset: true +``` + +### Secrets + +`tapi` does **not** load `.env` files automatically. Export any environment variables referenced in workflows yourself: + +```bash +# bash / zsh +export TAPI_TOKEN=secret + +# PowerShell +$Env:TAPI_TOKEN = 'secret' +``` + +## Project layout + +`init` creates a ready-to-roll workspace: + +``` +my-project/ +β”œβ”€β”€ .tapi/ # tapi metadata (manifest, workflows) +β”œβ”€β”€ compiler/ # optional community compiler +β”œβ”€β”€ gamemodes/ # gamemode sources (.pwn / .amx) +β”œβ”€β”€ filterscripts/ # filterscript sources +β”œβ”€β”€ includes/ # project-specific includes +β”œβ”€β”€ plugins/ +β”œβ”€β”€ scriptfiles/ +β”œβ”€β”€ qawno/ # bundled compiler from server package +β”œβ”€β”€ config.json # open.mp server config +β”œβ”€β”€ omp-server.exe/.sh # server binaries +└── .vscode/ # VS Code tasks & settings (optional) +``` + +## Additional docs + +- [Init command details](src/commands/init/README.md) +- [Workflows & presets](docs/init-presets.md) +- [Command catalogue](src/commands) + +## Contributing + +Bugs, ideas, PRs are welcome. Check the command-specific READMEs for current limitations and TODOs, or open an issue to discuss larger changes. + +## Status + +`tapi` is production-quality for development workflows and actively evolving. For production servers, use at your own discretion and pin versions. + +--- + +*Stop fighting your tooling. Build gamemodes faster.* diff --git a/src/commands/addons/README.md b/src/commands/addons/README.md index 04de05a..4f6cd4e 100644 --- a/src/commands/addons/README.md +++ b/src/commands/addons/README.md @@ -1,223 +1,41 @@ -# Addon Commands Implementation - -This directory contains the implementation of tapi's addon management commands. - -## Overview - -The addon commands allow users to install, manage, and use tapi addons - extensions that can modify `pawn.json`, add custom commands, and hook into tapi's lifecycle. - -## Commands - -### `index.ts` - Main Command Registration -- Registers the `addon` command group -- Shows help when no subcommand is provided -- Coordinates all addon subcommands - -### `install.ts` - Addon Installation -**Command:** `tapi addon install ` - -**Features:** -- Install addons from multiple sources (npm, GitHub, local) -- Support for global and local installation -- Duplicate installation detection -- Clear installation feedback - -**Options:** -- `-g, --global` - Install globally for all projects -- `-s, --source ` - Specify installation source -- `--github ` - Install from GitHub (user/repo) -- `--local ` - Install from local path - -**Examples:** -```bash -tapi addon install linter -tapi addon install linter --global -tapi addon install --github username/repo -tapi addon install --local ./my-addon -``` - -### `uninstall.ts` - Addon Removal -**Command:** `tapi addon uninstall ` - -**Features:** -- Remove installed addons -- Force removal option -- Installation status checking -- Cleanup notifications - -**Options:** -- `-f, --force` - Force removal without confirmation - -**Examples:** -```bash -tapi addon uninstall linter -tapi addon uninstall linter --force -``` - -### `list.ts` - Addon Listing -**Command:** `tapi addon list` - -**Features:** -- List installed addons with details -- Filter by status (enabled/disabled) -- Show all available addons -- Display addon metadata (version, author, license) - -**Options:** -- `-a, --all` - Show all addons (installed and available) -- `-e, --enabled` - Show only enabled addons -- `-d, --disabled` - Show only disabled addons - -**Examples:** -```bash -tapi addon list -tapi addon list --all -tapi addon list --enabled -``` - -### `enable.ts` - Addon Activation -**Command:** `tapi addon enable ` - -**Features:** -- Enable disabled addons -- Installation status verification -- Activation confirmation - -**Examples:** -```bash -tapi addon enable linter -``` - -### `disable.ts` - Addon Deactivation -**Command:** `tapi addon disable ` - -**Features:** -- Disable enabled addons -- Installation status verification -- Deactivation confirmation - -**Examples:** -```bash -tapi addon disable linter -``` - -## Integration with Addon System - -These commands integrate with the core addon system: - -### AddonManager Integration -- Uses `getAddonManager()` to access addon functionality -- Calls methods like `installAddon()`, `uninstallAddon()`, `listAddons()` -- Handles addon lifecycle management - -### Error Handling -- Graceful error handling with user-friendly messages -- Proper exit codes for scripting -- Detailed error reporting - -### User Experience -- Clear progress indicators -- Helpful success messages -- Next steps guidance -- Consistent command interface - -## Command Flow - -### Installation Flow -1. Parse command options and addon name -2. Determine installation source and path -3. Check for existing installation -4. Call AddonManager.installAddon() -5. Provide success feedback and next steps - -### Listing Flow -1. Get addon list from AddonManager -2. Apply filters (enabled/disabled/all) -3. Format and display addon information -4. Show summary statistics - -### Enable/Disable Flow -1. Verify addon is installed -2. Check current status -3. Call AddonManager.enableAddon()/disableAddon() -4. Provide confirmation - -## Error Handling - -All commands implement consistent error handling: - -```typescript -try { - // Command logic -} catch (error) { - logger.error(`❌ Failed to [operation]: ${error instanceof Error ? error.message : 'unknown error'}`); - process.exit(1); -} -``` - -## User Feedback - -Commands provide clear feedback: - -- **Progress indicators** - Show what's happening -- **Success messages** - Confirm completion -- **Next steps** - Guide users on what to do next -- **Error messages** - Explain what went wrong - -## Installation Locations - -Commands handle different installation scenarios: - -### Global Installation (`--global`) -- Installs to `~/.tapi/addons/` -- Available to all projects -- Registry in `~/.tapi/addons.json` - -### Local Installation (default) -- Installs to `./.tapi/addons/` -- Project-specific -- Registry in `~/.tapi/addons.json` - -### npm Installation -- Discovers addons in `./node_modules/tapi-*` -- Automatic discovery and activation - -## Testing - -To test the addon commands: - -```bash -# Test installation -tapi addon install --local ./examples/addons/greeter-addon - -# Test listing -tapi addon list - -# Test enable/disable -tapi addon disable greeter -tapi addon enable greeter - -# Test uninstall -tapi addon uninstall greeter --force -``` - -## Dependencies - -These commands depend on: - -- **AddonManager** - Core addon functionality -- **Logger** - User feedback and error reporting -- **Banner** - Consistent UI display -- **Commander.js** - Command-line interface - -## Code Style - -Commands follow these patterns: - -- **Consistent structure** - All commands follow similar patterns -- **Error handling** - Proper try/catch with user-friendly messages -- **User feedback** - Clear progress and success indicators -- **Option validation** - Validate command options -- **Exit codes** - Proper exit codes for scripting - - +# `addon` command group + +Manage tapi addons (install, update, list, enable/disable). Addons extend the CLI with extra commands, hooks, and project scaffolding. + +Addons live alongside workflows; see [`src/core/addons`](../../core/addons) for architecture details. + +## Subcommands + +| Command | Purpose | +|---------|---------| +| `tapi addon install ` | Install an addon from npm, GitHub, or a local path | +| `tapi addon uninstall ` | Remove an addon | +| `tapi addon list` | Show installed addons (and status flags) | +| `tapi addon enable ` | Enable a previously installed addon | +| `tapi addon disable ` | Disable an addon (without uninstalling) | +| `tapi addon recover` | Diagnose or clear addon error state | +| `tapi addon deps` | Inspect/resolve addon dependencies | +| `tapi addon install-deps` | Install missing dependencies | +| `tapi addon update [name]` | Update one or all addons | +| `tapi addon version` | Semver utilities (check, compare, validate) | + +> Run `tapi addon --help` for the latest list; new subcommands may be added over time. + +## Installation sources + +- **npm** (default): `tapi addon install addon-name` +- **GitHub**: `tapi addon install --github user/repo` +- **Local path**: `tapi addon install --local ./path` +- **Scoped workflows**: use `--source` to point at custom registries. + +Global installs (`--global`) make an addon available to every project. + +## Interaction with the addon manager + +All commands use the addon manager (`src/core/addons`) which: + +- Tracks installed addons (local + global) +- Handles enable/disable state +- Runs lifecycle hooks during `init`, `build`, etc. + +Errors are presented with actionable messages and non-zero exit codes for scripting. diff --git a/src/commands/build/README.md b/src/commands/build/README.md index ff2ad2e..c3e4abb 100644 --- a/src/commands/build/README.md +++ b/src/commands/build/README.md @@ -1,12 +1,6 @@ -# `build` Command +# `build` -Compile your PAWN code using the PAWN compiler with intelligent include path detection and optimized settings for open.mp development. - -## Overview - -The `build` command compiles your PAWN source code into AMX bytecode that can be executed by the open.mp server. It automatically detects include directories, applies optimized compiler settings, and provides clear error reporting. - -## Usage +Compile PAWN sources into AMX bytecode using the configuration in `pawn.json`. ```bash tapi build [options] @@ -14,449 +8,47 @@ tapi build [options] ## Options -| Option | Description | Default | -|--------|-------------|---------| -| `-i, --input ` | Input .pwn file to compile | From pawn.json | -| `-o, --output ` | Output .amx file | From pawn.json | -| `-d, --debug ` | Debug level (1-3) | 3 | -| `-p, --profile ` | Use specific build profile | Default profile | -| `--list-profiles` | List available build profiles | false | - -### Global Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-v, --verbose` | Show detailed debug output | false | -| `-q, --quiet` | Minimize console output (show only progress bars) | false | -| `--log-to-file` | Save logs to file for debugging | false | - -## Features - -### Intelligent Include Detection - -The build command automatically finds and includes the following directories in order of priority: - -1. **pawno/include** - SA-MP includes (highest priority) -2. **qawno/include** - open.mp includes (second priority) -3. **compiler/include** - Community compiler includes (third priority) -4. **Custom includes** - User-defined include paths from pawn.json - -### Configuration-Driven - -The build process uses settings from your `pawn.json` manifest file: - -```json -{ - "compiler": { - "input": "gamemodes/my-gamemode.pwn", - "output": "gamemodes/my-gamemode.amx", - "includes": ["includes", "gamemodes"], - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+"], - "profiles": { - "test": { - "description": "Testing profile with verbose debugging", - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+"] - }, - "prod": { - "description": "Production profile with optimized settings", - "options": ["-d1", "-O1"] - } - } - } -} -``` - -### Optimized Compilation - -Pre-configured compiler options optimized for open.mp development: - -- **`-d3`**: Maximum debug information -- **`-;+`**: Enable semicolon warnings -- **`-(+`**: Enable parenthesis warnings -- **`-\\+`**: Enable backslash warnings -- **`-Z+`**: Enable zero-division warnings - -### Error Reporting - -Clear, formatted error messages with file and line numbers: - -``` -gamemodes/my-gamemode.pwn(15) : error 001: expected token: ";", but found "}" -gamemodes/my-gamemode.pwn(23) : warning 215: expression has no effect -``` - -## Compilation Process - -### 1. Configuration Loading -- Loads `pawn.json` manifest file -- Validates project configuration -- Determines input/output files - -### 2. Include Path Detection -- Scans for existing include directories -- Prioritizes include paths based on availability -- Validates include directory existence +| Flag | Description | Default | +|------|-------------|---------| +| `-i, --input ` | Source `.pwn` to compile | `compiler.input` | +| `-o, --output ` | Output `.amx` path | `compiler.output` | +| `-d, --debug <0-3>` | Debug info level | `3` | +| `-p, --profile ` | Use build profile | first profile / defaults | +| `--list-profiles` | Show available profiles | β€” | -### 3. Compiler Execution -- Constructs compiler command with all options -- Executes pawncc with proper arguments -- Captures and processes compiler output +Global flags (`--verbose`, `--quiet`, `--log-to-file`) also apply. -### 4. Error Processing -- Parses compiler output for errors and warnings -- Formats error messages for readability -- Provides file and line number context +### Include priority -## Examples +1. `pawno/include` +2. `qawno/include` +3. `compiler/include` +4. Paths from `compiler.includes` -### Basic Compilation -```bash -$ tapi build - -=== Building PAWN project... === -β„Ή Input file: gamemodes/my-gamemode.pwn -β„Ή Output file: gamemodes/my-gamemode.amx -β„Ή Debug level: 3 -β„Ή Added include directory: qawno/include -β„Ή Added include directory: includes -β„Ή Added include directory: gamemodes -β„Ή Compiler options: -d3 -;+ -(+ -\\+ -Z+ +### Default compiler flags -Compiling gamemodes/my-gamemode.pwn... -βœ“ Compilation successful! - Output: gamemodes/my-gamemode.amx - Size: 45.2 KB ``` - -### Custom Input/Output -```bash -$ tapi build -i filterscripts/anticheat.pwn -o filterscripts/anticheat.amx - -=== Building PAWN project... === -β„Ή Input file: filterscripts/anticheat.pwn -β„Ή Output file: filterscripts/anticheat.amx -β„Ή Debug level: 3 -β„Ή Added include directory: qawno/include -β„Ή Added include directory: includes - -Compiling filterscripts/anticheat.pwn... -βœ“ Compilation successful! - Output: filterscripts/anticheat.amx - Size: 12.8 KB +-d3 -;+ -(+ -\+ -Z+ ``` -### Debug Level Control -```bash -$ tapi build -d 1 +Override via profiles or CLI. -=== Building PAWN project... === -β„Ή Input file: gamemodes/my-gamemode.pwn -β„Ή Output file: gamemodes/my-gamemode.amx -β„Ή Debug level: 1 -β„Ή Added include directory: qawno/include -β„Ή Added include directory: includes +### Profiles -Compiling gamemodes/my-gamemode.pwn... -βœ“ Compilation successful! - Output: gamemodes/my-gamemode.amx - Size: 38.7 KB (smaller due to reduced debug info) -``` - -### Verbose Output -```bash -$ tapi build --verbose - -=== Building PAWN project... === -β„Ή Input file: gamemodes/my-gamemode.pwn -β„Ή Output file: gamemodes/my-gamemode.amx -β„Ή Debug level: 3 -β„Ή Added include directory: qawno/include -β„Ή Added include directory: includes -β„Ή Added include directory: gamemodes -β„Ή Compiler options: -d3 -;+ -(+ -\\+ -Z+ -β„Ή Full command: qawno/pawncc.exe -igamemodes/my-gamemode.pwn -oqawno/include -iincludes -igamemodes -d3 -;+ -(+ -\\+ -Z+ gamemodes/my-gamemode.pwn - -Compiling gamemodes/my-gamemode.pwn... -βœ“ Compilation successful! - Output: gamemodes/my-gamemode.amx - Size: 45.2 KB -``` - -## Build Profiles - -Build profiles allow you to define different compiler configurations for different build scenarios (development, testing, production, etc.). - -### Using Build Profiles - -#### List Available Profiles -```bash -$ tapi build --list-profiles - -Available build profiles: - test - Testing profile with verbose debugging - prod - Production profile with optimized settings - debug - Debug profile with maximum debug information -``` - -#### Build with a Specific Profile -```bash -$ tapi build --profile prod - -=== Building PAWN project... === -β„Ή Using build profile: prod -β„Ή Input file: gamemodes/my-gamemode.pwn -β„Ή Output file: gamemodes/my-gamemode.amx -β„Ή Profile options: -d1 -O1 - -Compiling gamemodes/my-gamemode.pwn... -βœ“ Compilation successful! - Output: gamemodes/my-gamemode.amx - Size: 32.1 KB (optimized) -``` - -#### Profile-Specific Input/Output -```bash -$ tapi build --profile debug -i gamemodes/debug.pwn -o gamemodes/debug.amx - -=== Building PAWN project... === -β„Ή Using build profile: debug -β„Ή Input file: gamemodes/debug.pwn -β„Ή Output file: gamemodes/debug.amx -β„Ή Profile options: -d3 -;+ -(+ -\\+ -Z+ -v - -Compiling gamemodes/debug.pwn... -βœ“ Compilation successful! - Output: gamemodes/debug.amx - Size: 48.7 KB (maximum debug info) -``` - -### Creating Custom Profiles - -Add profiles to your `pawn.json` file: +Profiles live under `compiler.profiles` in `pawn.json`: ```json { "compiler": { - "input": "gamemodes/main.pwn", - "output": "gamemodes/main.amx", - "includes": ["includes", "gamemodes"], - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+"], "profiles": { - "dev": { - "description": "Development profile with full debugging", - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+", "-v"] - }, - "test": { - "description": "Testing profile with balanced settings", - "options": ["-d2", "-;+", "-(+"] - }, + "dev": { "options": ["-d3"] }, "prod": { - "description": "Production profile with optimizations", - "options": ["-d1", "-O1", "-O2"] - }, - "release": { - "description": "Release profile with custom paths", - "input": "gamemodes/release.pwn", - "output": "dist/gamemode.amx", - "includes": ["includes", "gamemodes", "release/includes"], - "options": ["-d1", "-O1"] + "options": ["-d0", "-O1"], + "output": "dist/my-gamemode.amx" } } } } ``` -### Profile Configuration Options - -Each profile can override any base compiler setting: - -| Option | Description | Example | -|--------|-------------|---------| -| `input` | Input .pwn file path | `"gamemodes/custom.pwn"` | -| `output` | Output .amx file path | `"dist/custom.amx"` | -| `includes` | Array of include directories | `["includes", "custom"]` | -| `options` | Array of compiler options | `["-d1", "-O1"]` | -| `description` | Human-readable description | `"Production build"` | - -### Profile Inheritance - -Profiles inherit from the base compiler configuration and only override specified options: - -```json -{ - "compiler": { - "input": "gamemodes/main.pwn", - "output": "gamemodes/main.amx", - "includes": ["includes", "gamemodes"], - "options": ["-d3"], - "profiles": { - "fast": { - "description": "Fast build with minimal debug info", - "options": ["-d1"] // Overrides base -d3, inherits includes - }, - "verbose": { - "description": "Verbose build with extra includes", - "includes": ["includes", "gamemodes", "debug/includes"], - "options": ["-d3", "-v"] // Inherits input/output, adds -v - } - } - } -} -``` - -## Error Handling - -### Common Compilation Errors - -#### Missing Include Files -``` -gamemodes/my-gamemode.pwn(1) : fatal error 100: cannot read from file: "a_samp.inc" -``` -**Solution**: Ensure qawno/include directory exists and contains required includes - -#### Syntax Errors -``` -gamemodes/my-gamemode.pwn(15) : error 001: expected token: ";", but found "}" -``` -**Solution**: Check syntax at the specified line number - -#### Undefined Symbols -``` -gamemodes/my-gamemode.pwn(23) : error 017: undefined symbol "MAX_PLAYERS" -``` -**Solution**: Define constants in pawn.json or include proper header files - -#### Include Path Issues -``` -β„Ή Skipped non-existent include directory: custom/includes -``` -**Solution**: Remove non-existent include paths from pawn.json - -### Error Recovery - -The build command provides helpful error information: - -1. **File and line numbers** for precise error location -2. **Error codes** for quick reference -3. **Context information** about include paths and compiler options -4. **Suggestions** for common error resolution - -## Configuration - -### pawn.json Compiler Settings - -```json -{ - "compiler": { - "input": "gamemodes/my-gamemode.pwn", - "output": "gamemodes/my-gamemode.amx", - "includes": [ - "includes", - "gamemodes", - "custom/path" - ], - "options": [ - "-d3", - "-;+", - "-(+", - "-\\+", - "-Z+", - "-v" - ], - "profiles": { - "test": { - "description": "Testing profile with verbose debugging", - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+"] - } - } - } -} -``` - -### Include Directory Priority - -1. **User-defined includes** (from pawn.json) -2. **pawno/include** (SA-MP includes) -3. **qawno/include** (open.mp includes) -4. **compiler/include** (Community compiler includes) - -## Debug Levels - -### Level 1 (Minimal) -- Basic debug information -- Smaller output file size -- Faster compilation - -### Level 2 (Standard) -- Standard debug information -- Balanced file size and debugging capability - -### Level 3 (Maximum) - Default -- Full debug information -- Largest output file size -- Best debugging experience - -## Performance Tips - -### Optimize Compilation Speed -- Use debug level 1 for production builds -- Minimize include directories -- Use specific input files instead of relying on defaults - -### Reduce Output Size -- Use debug level 1 -- Remove unused includes -- Optimize PAWN code - -### Improve Error Detection -- Use debug level 3 during development -- Enable all compiler warnings -- Use verbose mode for detailed information - -## Integration - -### VS Code Integration -When using VS Code, the build command integrates with: -- **Build tasks** (Ctrl+Shift+B) -- **Error reporting** in Problems panel -- **Quick fixes** and suggestions - -### Continuous Integration -The build command is suitable for CI/CD pipelines: -```bash -# In CI script -tapi build --quiet -if [ $? -eq 0 ]; then - echo "Build successful" -else - echo "Build failed" - exit 1 -fi -``` - -## Troubleshooting - -### Common Issues - -#### "No pawn.json manifest found" -**Cause**: Not in a tapi project directory -**Solution**: Run `tapi init` to create a project - -#### "Input file not found" -**Cause**: Specified input file doesn't exist -**Solution**: Check file path or use `--input` to specify correct file - -#### "Compiler not found" -**Cause**: PAWN compiler not installed -**Solution**: Run `tapi init` to install compiler - -#### "Include directory not found" -**Cause**: Include path doesn't exist -**Solution**: Remove non-existent paths from pawn.json or create directories - -### Getting Help - -- **Verbose mode**: Use `--verbose` for detailed compiler information -- **Check configuration**: Verify pawn.json settings -- **Validate includes**: Ensure include directories exist and contain required files -- **Review errors**: Check error messages for specific issues and line numbers +Use `tapi build --profile prod` or list them with `tapi build --list-profiles`. diff --git a/src/commands/config/README.md b/src/commands/config/README.md index bb5e989..a5a6bbb 100644 --- a/src/commands/config/README.md +++ b/src/commands/config/README.md @@ -1,353 +1,19 @@ -# `config` Command - -Manage tapi user preferences and settings through an interactive configuration interface. - -## Overview - -The `config` command provides an interactive way to manage tapi user preferences, including default author information, preferred editor settings, GitHub integration, and other configuration options. It offers a user-friendly interface for viewing and modifying settings without manually editing configuration files. - -## Usage - -```bash -tapi config [options] -``` - -## Features - -### Interactive Configuration Management - -- **Current Settings Display**: View all current configuration values -- **Individual Setting Updates**: Modify specific settings one at a time -- **GitHub Integration**: Configure GitHub token for package installation -- **Editor Preferences**: Set preferred code editor for project setup -- **Default Author**: Configure default author name for new projects - -### Configuration Categories - -1. **User Information**: Default author name for projects -2. **Editor Preferences**: Preferred code editor (VS Code, Sublime Text, etc.) -3. **GitHub Integration**: Personal access token for package installation -4. **System Settings**: Setup completion status and other system preferences - -### Persistent Storage - -- **Automatic Saving**: Changes are saved immediately -- **Cross-Session Persistence**: Settings persist between command sessions -- **Backup Support**: Configuration can be reset to defaults -- **Validation**: Input validation and error handling - -## Configuration Options - -### Default Author -Set your name to be used as the default author for new projects. - -**Location**: `~/.tapi/preferences.json` -**Key**: `defaultAuthor` -**Example**: `"Developer Name"` - -### Preferred Editor -Choose your preferred code editor for project setup and integration. - -**Options**: -- **VS Code**: Full integration with tasks, debugging, and IntelliSense -- **Sublime Text**: Basic configuration and syntax highlighting -- **Other/None**: No editor-specific setup - -**Location**: `~/.tapi/preferences.json` -**Key**: `editor` -**Example**: `"VS Code"` - -### GitHub Integration -Configure GitHub personal access token for package installation. - -**Purpose**: Enables installation of packages from private repositories and increases API rate limits -**Location**: `~/.tapi/preferences.json` -**Key**: `githubToken` -**Example**: `"ghp_xxxxxxxxxxxxxxxxxxxx"` - -### Setup Status -Track whether initial setup has been completed. - -**Location**: `~/.tapi/preferences.json` -**Key**: `setupComplete` -**Example**: `true` - -## Examples - -### View Current Configuration -```bash -$ tapi config - -Current tapi configuration: -β€’ Default author: Developer -β€’ Preferred editor: VS Code -β€’ GitHub integration: Configured -β€’ Setup complete: Yes - -What would you like to configure? -βœ” Select an option β€Ί Default author -βœ” Enter your default author name: New Developer -βœ“ Default author updated to: New Developer -``` - -### Configure GitHub Integration -```bash -$ tapi config - -Current tapi configuration: -β€’ Default author: Developer -β€’ Preferred editor: VS Code -β€’ GitHub integration: Not configured -β€’ Setup complete: Yes - -What would you like to configure? -βœ” Select an option β€Ί GitHub integration -βœ” Enter your GitHub personal access token: **************** -βœ“ GitHub token configured successfully -``` - -### Update Editor Preference -```bash -$ tapi config - -Current tapi configuration: -β€’ Default author: Developer -β€’ Preferred editor: Sublime Text -β€’ GitHub integration: Configured -β€’ Setup complete: Yes - -What would you like to configure? -βœ” Select an option β€Ί Preferred editor -βœ” Which code editor do you use most for PAWN development? VS Code -βœ“ Preferred editor updated to: VS Code -``` - -### Reset Configuration -```bash -$ tapi config - -Current tapi configuration: -β€’ Default author: Developer -β€’ Preferred editor: VS Code -β€’ GitHub integration: Configured -β€’ Setup complete: Yes - -What would you like to configure? -βœ” Select an option β€Ί Reset all configuration -⚠️ This will reset ALL configuration to defaults. Type "confirm" to proceed: confirm -βœ“ Configuration reset to defaults -``` - -## Configuration File Structure - -### preferences.json -```json -{ - "defaultAuthor": "Developer Name", - "editor": "VS Code", - "githubToken": "ghp_xxxxxxxxxxxxxxxxxxxx", - "setupComplete": true -} -``` - -### File Locations - -#### Windows -``` -C:\Users\\.tapi\preferences.json -``` - -#### Linux/macOS -``` -~/.tapi/preferences.json -``` - -## Interactive Menu Options - -### Main Menu -``` -Current tapi configuration: -β€’ Default author: Developer -β€’ Preferred editor: VS Code -β€’ GitHub integration: Configured -β€’ Setup complete: Yes - -What would you like to configure? -βœ” Select an option β€Ί [Use arrow keys to navigate] - β€’ Default author - β€’ Preferred editor - β€’ GitHub integration - β€’ Reset all configuration - β€’ Exit -``` - -### GitHub Integration Submenu -``` -GitHub token is already configured. What would you like to do? -βœ” Select an option β€Ί [Use arrow keys to navigate] - β€’ Update the token - β€’ Remove the token - β€’ Keep current token -``` - -### Editor Selection -``` -Which code editor do you use most for PAWN development? -βœ” Select an option β€Ί [Use arrow keys to navigate] - β€’ Visual Studio Code (recommended) - β€’ Sublime Text - β€’ Other/None -``` - -## Configuration Impact - -### Default Author -- **Used in**: `tapi init` command -- **Affects**: Project manifest (pawn.json) author field -- **Default**: Empty (prompts user during init) - -### Preferred Editor -- **Used in**: `tapi init` command -- **Affects**: Editor-specific file generation (.vscode/, etc.) -- **Default**: VS Code - -### GitHub Integration -- **Used in**: `tapi install` command -- **Affects**: Package installation from GitHub repositories -- **Default**: Not configured - -### Setup Status -- **Used in**: `tapi setup` command -- **Affects**: Whether setup wizard runs automatically -- **Default**: false - -## Security Considerations - -### GitHub Token Storage -- **Location**: Local configuration file -- **Permissions**: User read/write only -- **Scope**: Minimal required permissions -- **Security**: Store securely, don't share - -### Token Permissions -Recommended GitHub token permissions: -- **repo**: Access to private repositories -- **read:packages**: Download packages -- **No admin permissions**: Minimal scope for security - -### Token Management -```bash -# View current token status -tapi config - -# Update token -tapi config -# Select "GitHub integration" β†’ "Update the token" - -# Remove token -tapi config -# Select "GitHub integration" β†’ "Remove the token" -``` - -## Error Handling - -### Common Issues - -#### Configuration File Corruption -```bash -βœ— Failed to load configuration - Error: Invalid JSON format - Solution: Configuration file will be reset to defaults -``` - -#### Permission Issues -```bash -βœ— Failed to save configuration - Error: EACCES: permission denied - Solution: Check file permissions or run as administrator -``` - -#### Invalid Input -```bash -βœ— Invalid input provided - Error: Author name cannot be empty - Solution: Provide a valid author name -``` - -### Recovery Options - -1. **Reset Configuration**: Use "Reset all configuration" option -2. **Manual Edit**: Edit preferences.json file directly -3. **Delete File**: Remove preferences.json to start fresh -4. **Re-run Setup**: Use `tapi setup --force` - -## Integration with Other Commands - -### init Command -- **Default Author**: Pre-fills author field in interactive prompts -- **Editor Preference**: Determines which editor files to generate -- **Setup Status**: Affects whether setup wizard runs - -### install Command -- **GitHub Token**: Used for package installation from GitHub -- **Rate Limits**: Increases API rate limits for authenticated requests -- **Private Repos**: Enables access to private repositories - -### setup Command -- **Setup Status**: Tracks whether initial setup is complete -- **Configuration**: Uses current settings as defaults - -## Best Practices - -### Configuration Management -1. **Set Default Author**: Configure your name for convenience -2. **Choose Editor**: Select your preferred editor for integration -3. **Configure GitHub**: Set up token for package installation -4. **Regular Updates**: Review and update settings periodically - -### Security -1. **Token Security**: Keep GitHub token secure and private -2. **Minimal Permissions**: Use tokens with minimal required permissions -3. **Regular Rotation**: Rotate GitHub tokens periodically -4. **Local Storage**: Be aware that tokens are stored locally - -### Workflow Integration -1. **Team Consistency**: Use consistent settings across team members -2. **Project Standards**: Align with project coding standards -3. **Editor Integration**: Leverage editor-specific features -4. **Package Management**: Use GitHub integration for dependencies - -## Troubleshooting - -### Common Issues - -#### "Configuration file not found" -**Cause**: First-time usage or file corruption -**Solution**: Run `tapi setup` to create initial configuration - -#### "Invalid GitHub token" -**Cause**: Token expired or has insufficient permissions -**Solution**: Generate new token with proper permissions - -#### "Permission denied" -**Cause**: Insufficient file system permissions -**Solution**: Check directory permissions or run as administrator - -#### "Editor not supported" -**Cause**: Selected editor doesn't have integration support -**Solution**: Choose supported editor or use "Other/None" - -### Getting Help - -- **View current settings**: Run `tapi config` to see all settings -- **Reset configuration**: Use "Reset all configuration" option -- **Manual editing**: Edit preferences.json file directly -- **Re-run setup**: Use `tapi setup --force` to reconfigure - -### Recovery Steps - -1. **Check current settings**: `tapi config` -2. **Identify problematic setting**: Look for error messages -3. **Reset specific setting**: Use config command to update -4. **Reset all settings**: Use "Reset all configuration" option -5. **Re-run setup**: `tapi setup --force` if needed +# `config` + +Inspect or change tapi’s stored preferences (author name, editor, GitHub token). + +```bash +tapi config [flags] +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--show` | Print current settings | +| `--author [name]` | Set default author (omit value to prompt) | +| `--editor ` | Set preferred editor (`VS Code`, `Sublime Text`, `Other/None`) | +| `--github-token [token]` | Add / update / remove GitHub token | +| `--reset` | Reset configuration to defaults | + +Running without flags launches an interactive menu that lets you tweak values one by one. Settings live in `~/.tapi/config.json` and feed into `tapi init`, `tapi install`, etc. diff --git a/src/commands/init/README.md b/src/commands/init/README.md index e9e60c8..a6c1054 100644 --- a/src/commands/init/README.md +++ b/src/commands/init/README.md @@ -1,373 +1,115 @@ -# `init` Command - -Initialize a new open.mp project with proper directory structure, server files, and development environment setup. - -## Overview - -The `init` command is the primary way to create new open.mp projects. It sets up everything you need to start developing PAWN code for open.mp servers, including: - -- Project directory structure -- open.mp server package -- PAWN compiler and standard library -- VS Code integration (optional) -- Git repository (optional) -- Project configuration files - -## Usage - -```bash -tapi init [options] -``` - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-n, --name ` | Project name | Current directory name | -| `-d, --description ` | Project description | - | -| `-a, --author ` | Project author | From user preferences | -| `-q, --quiet` | Minimize console output | false | -| `--skip-compiler` | Skip compiler setup and use default settings | false | -| `-v, --verbose` | Show detailed debug output | false | - -## Interactive Prompts - -The `init` command uses interactive prompts to gather project information: - -### 1. Project Information -- **Project name**: Name of your project (used for files and directories) -- **Project description**: Brief description of what your project does -- **Author**: Your name (defaults to user preference) - -### 2. Project Type -Choose from: -- **gamemode**: Main server gamemode (most common) -- **filterscript**: Server-side script for specific functionality -- **library**: Include library for other projects - -### 3. Editor Setup -- **VS Code**: Full integration with tasks, debugging, and IntelliSense -- **Sublime Text**: Basic configuration -- **Other/None**: No editor-specific setup - -### 4. Git Repository -- **Yes**: Initialize Git repository with .gitignore -- **No**: Skip Git setup - -### 5. Server Package -- **Yes**: Download and extract open.mp server package -- **No**: Skip server setup (manual setup required) - -### 6. Compiler Setup -- **Download community compiler**: Install latest community PAWN compiler -- **Compiler version**: Specific version or "latest" -- **Installation location**: qawno/ (replace server's) or compiler/ (separate) -- **Standard library**: Download open.mp standard library - -## Smart Version Conflict Detection - -The `init` command intelligently handles compiler version conflicts: - -### How It Works -1. **Detects existing compiler**: Checks if qawno/ directory exists -2. **Compares versions**: Compares server package compiler vs community compiler -3. **Presents options**: Offers appropriate choices based on version comparison - -### Version Conflict Scenarios - -#### Downgrade Detected (Server > Community) -``` -⚠️ Version conflict detected! - Server package includes: 3.10.11 - Community compiler version: 3.10.10 - Installing community compiler would be a downgrade! - -How would you like to handle this version conflict? -βœ” Select an option β€Ί Keep server's compiler (3.10.11) - recommended -``` - -**Options:** -- **Keep server's compiler**: Preserve the newer version (recommended) -- **Replace with community compiler**: Downgrade to older version (not recommended) -- **Install both**: Keep server's and install community in compiler/ folder - -#### Upgrade Available (Server < Community) -``` -Server has 3.10.10, community compiler is 3.10.11. Replace server's compiler? -βœ” Yes/No β€Ί Yes -``` - -#### Same Version (No Conflict) -``` -βœ“ Compiler versions match (3.10.11). Keeping existing compiler. -``` - -## Project Structure Created - -``` -project-name/ -β”œβ”€β”€ gamemodes/ # Gamemode source files -β”‚ └── project-name.pwn # Main gamemode file -β”œβ”€β”€ filterscripts/ # Filterscript source files -β”œβ”€β”€ includes/ # Include files -β”œβ”€β”€ plugins/ # Server plugins -β”œβ”€β”€ scriptfiles/ # Server data files -β”œβ”€β”€ qawno/ # PAWN compiler files -β”‚ β”œβ”€β”€ pawncc.exe # Compiler executable -β”‚ β”œβ”€β”€ pawnc.dll # Compiler library -β”‚ └── include/ # Standard library -β”œβ”€β”€ compiler/ # Community compiler (if installed separately) -β”œβ”€β”€ omp-server.exe # open.mp server executable -β”œβ”€β”€ config.json # Server configuration -β”œβ”€β”€ .tapi/ # tapi configuration -β”‚ └── pawn.json # Project manifest -└── .vscode/ # VS Code configuration (if selected) - β”œβ”€β”€ tasks.json # Build tasks - └── settings.json # Editor settings -``` - -## Configuration Files - -### pawn.json (Project Manifest) -```json -{ - "name": "my-gamemode", - "version": "1.0.0", - "description": "A new open.mp gamemode", - "author": "Developer", - "license": "MIT", - "entry": "gamemodes/my-gamemode.pwn", - "output": "gamemodes/my-gamemode.amx", - "compiler": { - "input": "gamemodes/my-gamemode.pwn", - "output": "gamemodes/my-gamemode.amx", - "includes": ["includes", "gamemodes"], - "constants": { - "MAX_PLAYERS": 50, - "DEBUG": 1 - }, - "options": ["-d3", "-;+", "-(+", "-\\+", "-Z+"] - } -} -``` - -### config.json (Server Configuration) -```json -{ - "hostname": "open.mp Server", - "language": "English", - "maxplayers": 50, - "port": 7777, - "rcon_password": "changeme", - "password": "", - "announce": true, - "chatlogging": true, - "weburl": "https://open.mp", - "onfoot_rate": 30, - "incar_rate": 30, - "weapon_rate": 30, - "stream_distance": 300.0, - "stream_rate": 1000, - "maxnpc": 0, - "logtimeformat": "[%H:%M:%S]", - "language": "English", - "rcon": true, - "rcon_password": "changeme", - "password": "", - "admin_password": "changeme", - "announce": true, - "chatlogging": true, - "weburl": "https://open.mp", - "onfoot_rate": 30, - "incar_rate": 30, - "weapon_rate": 30, - "stream_distance": 300.0, - "stream_rate": 1000, - "maxnpc": 0, - "logtimeformat": "[%H:%M:%S]", - "plugins": [], - "filterscripts": [], - "pawn": { - "main_scripts": ["gamemodes/my-gamemode"], - "auto_reload": true - } -} -``` - -## Error Handling - -### Interruption Recovery -If you press Ctrl+C during the initialization process, tapi will: -1. **Detect the interruption** -2. **Use sensible defaults** for remaining options -3. **Continue the process** with default settings -4. **Complete initialization** successfully - -### Common Error Scenarios - -#### Network Issues -``` -βœ— Failed to download server package - Error: Network timeout - Solution: Check your internet connection and try again -``` - -#### Permission Issues -``` -βœ— Failed to create directory: gamemodes/ - Error: EACCES: permission denied - Solution: Check directory permissions or run as administrator -``` - -#### Disk Space Issues -``` -βœ— Failed to extract server package - Error: ENOSPC: no space left on device - Solution: Free up disk space and try again -``` - -## Verbosity Levels - -### Normal Mode (Default) -```bash -tapi init -``` -- Clean, minimal output -- Progress bars for downloads -- Essential success messages only - -### Quiet Mode -```bash -tapi init --quiet -``` -- Minimal output -- Only critical messages and progress bars -- Perfect for automated scripts - -### Verbose Mode -```bash -tapi init --verbose -``` -- Detailed logging -- File operation details -- Debug information -- Redirect URLs and technical details - -## Examples - -### Basic Project Initialization -```bash -$ tapi init - -=== Initializing new open.mp project... === -βœ” Project name: my-gamemode -βœ” Project description: A new open.mp gamemode -βœ” Author: Developer -βœ” Project type: gamemode -βœ” Which editor are you using? VS Code -βœ” Initialize Git repository? Yes -βœ” Add open.mp server package? Yes - ---- Setting up your project... --- -β„Ή Created pawn.json manifest file -βœ” VS Code configuration created -βœ“ Project files and structure created -βœ” Found open.mp version v1.4.0.2779 -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 29984/29984 KB -βœ” Copied 13 server files to project -βœ” Extracting server package... - -πŸŽ‰ Server installation complete! - Server executable: omp-server.exe - Configuration: config.json - -βœ” Download community pawn compiler? Yes -βœ” Enter the compiler version (or "latest" for the latest version): latest -βœ” Install community compiler in compiler/ folder? No -βœ” Download open.mp standard library? Yes - ---- Compiler installation summary: --- - Result: qawno/ (preserved) -βœ“ Compiler installed -βœ“ Standard library installed -βœ” Server configuration updated -βœ” Cleanup complete - -πŸŽ‰ Project initialized successfully! -``` - -### Quick Setup with Options -```bash -$ tapi init --name my-server --description "My awesome server" --author "CoolDev" --quiet - -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 29984/29984 KB -πŸŽ‰ Project initialized successfully! -``` - -### Skip Compiler Setup -```bash -$ tapi init --skip-compiler - -=== Initializing new open.mp project... === -βœ” Project name: my-gamemode -βœ” Project description: A new open.mp gamemode -βœ” Author: Developer -βœ” Project type: gamemode -βœ” Which editor are you using? VS Code -βœ” Initialize Git repository? Yes -βœ” Add open.mp server package? Yes - ---- Setting up your project... --- -β„Ή Created pawn.json manifest file -βœ” VS Code configuration created -βœ“ Project files and structure created -βœ” Found open.mp version v1.4.0.2779 -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 29984/29984 KB -βœ” Copied 13 server files to project -βœ” Extracting server package... - -πŸŽ‰ Server installation complete! - Server executable: omp-server.exe - Configuration: config.json - -β„Ή Skipping compiler setup. Using default settings. -βœ” Server configuration updated -βœ” Cleanup complete - -πŸŽ‰ Project initialized successfully! -``` - -## Next Steps - -After running `tapi init`, you can: - -1. **Edit your code**: Open the generated .pwn file in your editor -2. **Build your project**: Run `tapi build` to compile -3. **Start the server**: Run `tapi start` to launch the server -4. **Install packages**: Use `tapi install` to add dependencies - -## Troubleshooting - -### Common Issues - -#### "No pawn.json manifest found" -**Cause**: Not in a tapi project directory -**Solution**: Run `tapi init` to create a new project - -#### "Server executable not found" -**Cause**: Server package wasn't downloaded or extracted properly -**Solution**: Re-run `tapi init` or manually download server files - -#### "Compiler version conflict" -**Cause**: Server package has different compiler version than community -**Solution**: Choose appropriate option in the version conflict prompt - -#### "Permission denied" -**Cause**: Insufficient permissions to create files/directories -**Solution**: Run as administrator or check directory permissions - -### Getting Help - -- **Verbose mode**: Use `--verbose` for detailed error information -- **Clean slate**: Delete project directory and re-run `tapi init` -- **Manual setup**: Use `--skip-compiler` to bypass problematic steps +# `init` + +Bootstrap an open.mp/SA-MP project (folders, server binaries, compiler, git, editor tasks) in one go. + +## Command summary + +```bash +tapi init [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `-n, --name ` | Project name | Current directory name | +| `-d, --description ` | Project description | β€” | +| `-a, --author ` | Author shown in manifests | Configured user name | +| `-q, --quiet` | Minimal console output | `false` | +| `-v, --verbose` | Extra diagnostics | `false` | +| `--skip-compiler` | Skip compiler & stdlib setup | `false` | +| `--legacy-samp` | Prefer SA-MP assets over open.mp | `false` | +| `--preset ` | Load answers from `.tapi/workflows/init-.yml` (or full path) | β€” | +| `--accept-preset` | Trust preset defaults, still prompt for missing values | `false` | +| `--non-interactive` | No prompts; fails if data is missing | `false` | + +Global flags (`--log-to-file`, etc.) are also honoured. + +## Workflows & presets + +Automate init by storing configuration in `.tapi/workflows/` (repo) or `~/.tapi/workflows/` (user). Files are discovered in this order: + +1. `--preset ` β†’ `.tapi/workflows/init-.yml` or `.tapi/workflows/init/.yml` +2. `.tapi/workflows/init.yml` +3. User defaults `~/.tapi/workflows/init-*.yml` + +Example: + +```yaml +# .tapi/workflows/init-team.yml +project: + name: ${folder} + description: "${user}'s staging sandbox" + initGit: true + downloadServer: true +compiler: + downloadCompiler: true + compilerVersion: latest + compilerDownloadUrl: "https://internal.example.com/compiler-${platform}.zip?token=${env:TAPI_TOKEN}" + downloadStdLib: true + stdLibDownloadUrl: "https://internal.example.com/includes.zip" +options: + skipCompiler: false +acceptPreset: true +``` + +See the [Workflows guide](../../../docs/init-presets.md) for the full schema, placeholders, and advanced automation (inheritance, custom downloads, secrets). + +### Secrets + +`tapi` does not load `.env` automatically. Export environment variables in your shell or CI job before invoking the CLI: + +```bash +export TAPI_TOKEN=secret-value +``` + +## Interactive flow (default mode) + +Unless `--non-interactive` is supplied, `init` walks you through: + +1. **Project details** – name, description, author +2. **Project type** – gamemode / filterscript / library +3. **Editor integration** – VS Code tasks/debug configs, or skip +4. **Git setup** – create repo + `.gitignore` (optional) +5. **Server package** – download open.mp (or SA-MP when `--legacy-samp`) +6. **Compiler** – choose between bundled or community compiler, handle version conflicts, decide whether to install the stdlib + +Workflows pre-fill these answers; missing values still trigger prompts unless `--non-interactive` or `acceptPreset` is used. + +## Non-interactive mode + +Use `--non-interactive` when automating init (CI/CD, templates). Requirements: + +- Every required field must be provided via CLI flags, config defaults, or workflow. +- Download errors or missing values cause the command to exit with a non-zero status. +- Conflicting files abort automatically (no prompts are shown), so start from an empty directory or only safe files. + +## Custom download sources + +Workflows may override the compiler and standard library URLs. Supported archive formats: `.zip`, `.tar.gz`, `.tgz`. Placeholders: + +- `${platform}` β†’ `windows` or `linux` +- `${env:VAR}` β†’ environment variables (for tokens, switches) + +If the archive contains an `include/` folder, it is copied; otherwise the root contents are used. + +## Output layout + +``` +project/ +β”œβ”€β”€ .tapi/ # tapi metadata (manifest, workflows) +β”œβ”€β”€ compiler/ # optional community compiler (separate installs) +β”œβ”€β”€ qawno/ # compiler shipped with the server package +β”œβ”€β”€ gamemodes/ # main PAWN sources (.pwn / .amx) +β”œβ”€β”€ filterscripts/ +β”œβ”€β”€ includes/ +β”œβ”€β”€ plugins/ +β”œβ”€β”€ scriptfiles/ +β”œβ”€β”€ config.json # open.mp server configuration +β”œβ”€β”€ omp-server.* # server binaries +└── .vscode/ # tasks & settings (when VS Code selected) +``` + +## Related commands + +- [`tapi build`](../build/README.md) – compiles the sources defined in `pawn.json` +- [`tapi start`](../start/README.md) – runs the configured server with watch mode +- [`tapi config`](../config/README.md) – tweak stored defaults used by `init` diff --git a/src/commands/install/README.md b/src/commands/install/README.md index a29a08d..c7cc121 100644 --- a/src/commands/install/README.md +++ b/src/commands/install/README.md @@ -1,466 +1,33 @@ -# `install` Command - -Install packages from GitHub repositories with dependency management and cross-platform support. - -## Overview - -The `install` command downloads and installs PAWN packages from GitHub repositories. It supports various repository references (branches, tags, commits), handles dependencies, and provides cross-platform compatibility for different operating systems. - -## Usage - -```bash -tapi install [options] -``` - -## Repository Format - -``` -owner/repository[@branch|@tag|@commit] -``` - -### Examples -- `openmultiplayer/omp-stdlib` - Latest default branch -- `owner/repo@develop` - Specific branch -- `owner/repo@v1.0.0` - Specific tag -- `owner/repo@abc1234` - Specific commit - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--dependencies` | Install dependencies recursively | false | - -### Global Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-v, --verbose` | Show detailed debug output | false | -| `-q, --quiet` | Minimize console output (show only progress bars) | false | -| `--log-to-file` | Save logs to file for debugging | false | - -## Features - -### GitHub Integration -- **Direct Repository Access**: Install packages directly from GitHub -- **Authentication Support**: Use GitHub token for private repositories -- **Rate Limit Management**: Respects GitHub API rate limits -- **Repository Validation**: Checks for valid repository structure - -### Version Control Support -- **Branch References**: Install from specific branches -- **Tag References**: Install from specific version tags -- **Commit References**: Install from specific commits -- **Default Branch**: Uses repository's default branch when no reference specified - -### Dependency Management -- **Recursive Installation**: Install package dependencies automatically -- **Dependency Resolution**: Handles complex dependency chains -- **Conflict Detection**: Identifies and reports dependency conflicts -- **Circular Dependency Prevention**: Prevents infinite dependency loops - -### Cross-Platform Support -- **Windows**: Native Windows file handling -- **Linux**: Unix-compatible file operations -- **macOS**: macOS-specific optimizations -- **Platform Detection**: Automatic platform-specific file selection - -### Package Validation -- **pawn.json Validation**: Ensures package has proper manifest -- **Structure Validation**: Verifies correct package structure -- **File Integrity**: Validates downloaded files -- **Compatibility Check**: Ensures package compatibility - -## Installation Process - -### 1. Repository Parsing -- Parses repository URL and reference -- Validates repository format -- Determines installation target - -### 2. Repository Information Fetching -- Fetches repository metadata from GitHub -- Validates repository existence and access -- Retrieves pawn.json manifest information - -### 3. Package Validation -- Validates pawn.json structure -- Checks for required fields -- Verifies package compatibility - -### 4. File Download and Installation -- Downloads package files -- Extracts and installs to appropriate directories -- Handles platform-specific file selection - -### 5. Dependency Resolution -- Identifies package dependencies -- Recursively installs dependencies -- Resolves dependency conflicts - -## Examples - -### Basic Package Installation -```bash -$ tapi install openmultiplayer/omp-stdlib - -=== Installing package: openmultiplayer/omp-stdlib === -β„Ή Repository: https://github.com/openmultiplayer/omp-stdlib -β„Ή Reference: main (default branch) -β„Ή Platform: windows - -Fetching repository information... -βœ“ Repository information fetched successfully - -Package details: -β€’ Name: omp-stdlib -β€’ Version: 1.0.0 -β€’ Description: open.mp Standard Library -β€’ Dependencies: None - -Downloading package files... -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 245/245 KB - -Installing package... -βœ“ Package installed successfully - Location: includes/omp-stdlib/ - Files: 15 files installed -``` - -### Install Specific Version -```bash -$ tapi install openmultiplayer/omp-stdlib@v1.2.0 - -=== Installing package: openmultiplayer/omp-stdlib@v1.2.0 === -β„Ή Repository: https://github.com/openmultiplayer/omp-stdlib -β„Ή Reference: v1.2.0 (tag) -β„Ή Platform: windows - -Fetching repository information... -βœ“ Repository information fetched successfully - -Package details: -β€’ Name: omp-stdlib -β€’ Version: 1.2.0 -β€’ Description: open.mp Standard Library v1.2.0 -β€’ Dependencies: None - -Downloading package files... -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 248/248 KB - -Installing package... -βœ“ Package installed successfully - Location: includes/omp-stdlib/ - Files: 16 files installed -``` - -### Install with Dependencies -```bash -$ tapi install owner/advanced-gamemode --dependencies - -=== Installing package: owner/advanced-gamemode === -β„Ή Repository: https://github.com/owner/advanced-gamemode -β„Ή Reference: main (default branch) -β„Ή Platform: windows -β„Ή Dependencies: enabled - -Fetching repository information... -βœ“ Repository information fetched successfully - -Package details: -β€’ Name: advanced-gamemode -β€’ Version: 2.1.0 -β€’ Description: Advanced gamemode with features -β€’ Dependencies: mysql, sscanf, streamer - -Installing dependencies... -=== Installing dependency: mysql === -βœ“ mysql installed successfully - -=== Installing dependency: sscanf === -βœ“ sscanf installed successfully - -=== Installing dependency: streamer === -βœ“ streamer installed successfully - -Downloading package files... -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 1024/1024 KB - -Installing package... -βœ“ Package installed successfully - Location: gamemodes/advanced-gamemode/ - Files: 25 files installed - Dependencies: 3 packages installed -``` - -### Install from Specific Branch -```bash -$ tapi install owner/experimental-feature@develop - -=== Installing package: owner/experimental-feature@develop === -β„Ή Repository: https://github.com/owner/experimental-feature -β„Ή Reference: develop (branch) -β„Ή Platform: windows - -Fetching repository information... -βœ“ Repository information fetched successfully - -Package details: -β€’ Name: experimental-feature -β€’ Version: 0.5.0-dev -β€’ Description: Experimental features (development version) -β€’ Dependencies: None - -Downloading package files... -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 156/156 KB - -Installing package... -βœ“ Package installed successfully - Location: includes/experimental-feature/ - Files: 8 files installed -``` - -### Verbose Installation -```bash -$ tapi install openmultiplayer/omp-stdlib --verbose - -=== Installing package: openmultiplayer/omp-stdlib === -β„Ή Repository: https://github.com/openmultiplayer/omp-stdlib -β„Ή Reference: main (default branch) -β„Ή Platform: windows -β„Ή GitHub API: Using authenticated requests - -Fetching repository information... -β„Ή API endpoint: https://api.github.com/repos/openmultiplayer/omp-stdlib -β„Ή Rate limit: 5000/5000 remaining -βœ“ Repository information fetched successfully - -Package details: -β€’ Name: omp-stdlib -β€’ Version: 1.0.0 -β€’ Description: open.mp Standard Library -β€’ Dependencies: None -β€’ Include path: includes/ -β€’ Files: 15 files - -Downloading package files... -β„Ή Download URL: https://github.com/openmultiplayer/omp-stdlib/archive/main.zip -β„Ή File size: 245 KB -Downloading [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100% | ETA: 0s | 245/245 KB - -Installing package... -β„Ή Extracting archive... -β„Ή Creating directory: includes/omp-stdlib/ -β„Ή Copying files... - βœ“ omp-stdlib.inc - βœ“ omp-stdlib-core.inc - βœ“ omp-stdlib-utils.inc - βœ“ README.md - βœ“ LICENSE -βœ“ Package installed successfully - Location: includes/omp-stdlib/ - Files: 15 files installed -``` - -## Package Structure - -### Required Files -``` -package-name/ -β”œβ”€β”€ pawn.json # Package manifest (required) -β”œβ”€β”€ README.md # Package documentation -β”œβ”€β”€ LICENSE # License information -└── [package files] # Actual package files -``` - -### pawn.json Manifest -```json -{ - "name": "package-name", - "version": "1.0.0", - "description": "Package description", - "author": "Author Name", - "license": "MIT", - "repository": "owner/repository", - "include_path": "includes/", - "dependencies": [ - "dependency1", - "dependency2" - ], - "files": [ - "package.inc", - "README.md", - "LICENSE" - ] -} -``` - -## Dependency Management - -### Dependency Resolution -1. **Parse Dependencies**: Extract dependencies from pawn.json -2. **Check Conflicts**: Identify version conflicts -3. **Install Dependencies**: Recursively install required packages -4. **Validate Installation**: Ensure all dependencies are properly installed - -### Dependency Conflicts -```bash -=== Installing package: owner/conflicting-package === -⚠️ Dependency conflict detected: - Required: mysql@v2.0.0 - Installed: mysql@v1.5.0 - - Resolution: Installing mysql@v2.0.0 (upgrade) -``` - -### Circular Dependencies -```bash -=== Installing package: owner/circular-package === -βœ— Circular dependency detected: - package-a β†’ package-b β†’ package-c β†’ package-a - - Solution: Review dependency chain and remove circular reference -``` - -## Platform Support - -### Windows -- **File Extensions**: .exe, .dll, .inc -- **Path Separators**: Backslashes (\) -- **Archive Formats**: .zip, .tar.gz - -### Linux -- **File Extensions**: Binary files, .inc -- **Path Separators**: Forward slashes (/) -- **Archive Formats**: .tar.gz, .zip - -### macOS -- **File Extensions**: Binary files, .inc -- **Path Separators**: Forward slashes (/) -- **Archive Formats**: .tar.gz, .zip - -## Error Handling - -### Common Installation Errors - -#### Repository Not Found -```bash -βœ— Repository not found: owner/nonexistent-repo - Error: 404 Not Found - Solution: Check repository name and access permissions -``` - -#### Invalid Reference -```bash -βœ— Invalid reference: v999.999.999 - Error: Tag not found - Solution: Check available tags or use valid reference -``` - -#### Network Issues -```bash -βœ— Failed to download package - Error: Network timeout - Solution: Check internet connection and try again -``` - -#### Permission Issues -```bash -βœ— Failed to install package - Error: EACCES: permission denied - Solution: Check directory permissions or run as administrator -``` - -#### Dependency Conflicts -```bash -βœ— Dependency conflict: mysql@v2.0.0 conflicts with mysql@v1.5.0 - Error: Version mismatch - Solution: Resolve version conflicts or use compatible versions -``` - -### Error Recovery - -The install command provides: - -1. **Clear error messages** with specific causes -2. **Helpful suggestions** for resolution -3. **Partial cleanup** on installation failures -4. **Retry mechanisms** for transient errors - -## GitHub Integration - -### Authentication -- **Personal Access Token**: Use GitHub token for private repositories -- **Rate Limits**: Increased limits for authenticated requests -- **Private Access**: Access to private repositories with proper permissions - -### Token Configuration -```bash -# Configure GitHub token -tapi config -# Select "GitHub integration" β†’ "Update the token" -``` - -### Rate Limit Management -```bash -β„Ή GitHub API rate limit: 4500/5000 remaining -β„Ή Rate limit resets in: 45 minutes -``` - -## Best Practices - -### Package Installation -1. **Use Specific Versions**: Install from tags for stability -2. **Check Dependencies**: Review package dependencies before installation -3. **Validate Packages**: Ensure packages have proper pawn.json manifests -4. **Backup Projects**: Backup projects before installing packages - -### Dependency Management -1. **Minimize Dependencies**: Only install necessary packages -2. **Version Pinning**: Use specific versions for critical dependencies -3. **Regular Updates**: Keep packages updated for security and features -4. **Conflict Resolution**: Resolve dependency conflicts promptly - -### Security Considerations -1. **Source Verification**: Install from trusted repositories -2. **Token Security**: Keep GitHub tokens secure and private -3. **Package Validation**: Review package contents before installation -4. **Access Control**: Use minimal required permissions for tokens - -## Troubleshooting - -### Common Issues - -#### "Repository not found" -**Cause**: Invalid repository name or insufficient permissions -**Solution**: Check repository name and GitHub token configuration - -#### "Invalid reference" -**Cause**: Branch, tag, or commit doesn't exist -**Solution**: Check available references in the repository - -#### "Network timeout" -**Cause**: Slow internet connection or GitHub API issues -**Solution**: Check internet connection and try again later - -#### "Permission denied" -**Cause**: Insufficient file system permissions -**Solution**: Check directory permissions or run as administrator - -#### "Dependency conflict" -**Cause**: Incompatible package versions -**Solution**: Resolve version conflicts or use compatible versions - -### Getting Help - -- **Verbose mode**: Use `--verbose` for detailed installation information -- **Check repository**: Verify repository exists and is accessible -- **Validate token**: Ensure GitHub token has proper permissions -- **Review dependencies**: Check package dependencies and conflicts - -### Recovery Steps - -1. **Check repository**: Verify repository name and access -2. **Validate reference**: Check available branches, tags, and commits -3. **Configure token**: Set up GitHub token for private repositories -4. **Resolve conflicts**: Address dependency conflicts -5. **Retry installation**: Attempt installation again +# `install` + +Install reusable project packages from GitHub repositories (zip downloads). + +```bash +tapi install [options] +``` + +## Options + +| Flag | Meaning | +|------|---------| +| `-g, --global` | Install into `~/.tapi/packages` instead of the project | +| `--github ` | Explicit GitHub repo (defaults to the positional `` argument) | +| `--ref ` | Install a specific Git ref | +| `--force` | Overwrite existing files | + +Packages are extracted under `.tapi/packages/` and merged into your project. Dependencies declared in `pawn.json` are installed recursively. + +## Examples + +```bash +# latest default branch +tapi install openmultiplayer/omp-stdlib + +# install a tag +tapi install openmultiplayer/omp-stdlib@v1.4.0 + +# install globally (available to all projects) +tapi install openmultiplayer/omp-stdlib --global +``` + +Use `tapi install --help` for current flags and experimental options. diff --git a/src/commands/kill/README.md b/src/commands/kill/README.md index 97bf694..12c00f3 100644 --- a/src/commands/kill/README.md +++ b/src/commands/kill/README.md @@ -1,157 +1,15 @@ -# `kill` Command - -Force kill any running SA-MP/open.mp server processes for emergency cleanup. - -## Overview - -The `kill` command is designed for emergency situations where server processes become unresponsive or stuck. It forcefully terminates all SA-MP and open.mp server processes on the system and clears any stored server state. - -**Note**: For normal server shutdown, use **Ctrl+C** in the terminal running `tapi start`. - -## Usage - -```bash -tapi kill [options] -``` - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-f, --force` | Skip confirmation prompt | false | - -## Examples - -### Interactive Cleanup (Recommended) - -```bash -$ tapi kill -ar -⚠️ This will forcefully terminate ALL SA-MP/open.mp server processes. -πŸ’‘ For normal server shutdown, use Ctrl+C in the terminal running "tapi start" - -Continue? (y/N): y - -πŸ’€ Force killing server processes... -βœ… Killed omp-server.exe -βœ… Server processes terminated and state cleared -``` - -### Force Cleanup (No Confirmation) - -```bash -$ tapi kill --force - -πŸ’€ Force killing server processes... -βœ… Killed omp-server.exe -βœ… Killed samp-server.exe -βœ… Server processes terminated and state cleared -``` - -### No Processes Found - -```bash -$ tapi kill --force - -πŸ’€ Force killing server processes... -ℹ️ No server processes found running -``` - -## What It Does - -### Process Termination - -The command searches for and terminates: - -**Windows:** -- `omp-server.exe` -- `samp-server.exe` -- `samp03svr.exe` - -**Linux/macOS:** -- `omp-server` -- `samp-server` -- `samp03svr` - -### State Cleanup - -- Clears stored server state -- Removes PID tracking -- Cleans up temporary files -- Resets server status - -## When to Use - -### Emergency Situations - -- Server process becomes unresponsive -- Ctrl+C doesn't work in the start terminal -- Server process is stuck or crashed -- Multiple orphaned server processes -- Terminal was closed unexpectedly - -### NOT for Normal Use - -- ❌ Regular server shutdown (use Ctrl+C instead) -- ❌ Switching between projects (use Ctrl+C + start) -- ❌ Restarting for configuration changes - -## Platform Behavior - -### Windows -Uses `taskkill /F /IM /T` to: -- Force terminate processes (`/F`) -- Kill by image name (`/IM`) -- Kill process tree (`/T`) - -### Linux/macOS -Uses `pkill -f ` to: -- Find processes by pattern (`-f`) -- Send SIGTERM signal -- Clean process termination - -## Safety Features - -### Confirmation Prompt -- Warns about forceful termination -- Shows alternative (Ctrl+C) for normal shutdown -- Requires explicit confirmation - -### Targeted Termination -- Only kills SA-MP/open.mp server processes -- Doesn't affect other applications -- Safe for system stability - -## Troubleshooting - -### Permission Issues -```bash -❌ Kill operation failed: Access denied -``` -**Solution**: Run terminal as administrator (Windows) or use sudo (Linux/macOS) - -### Process Still Running -```bash -⚠️ Could not kill omp-server.exe: Process not found -``` -**Cause**: Process may have already terminated or different name -**Solution**: Check Task Manager/Activity Monitor manually - -### No Processes Found -```bash -ℹ️ No server processes found running -``` -**Cause**: No SA-MP/open.mp servers are currently running -**Solution**: This is normal - no action needed - -## Best Practices - -1. **Try Ctrl+C first**: Always attempt graceful shutdown before using kill -2. **Use confirmation**: Don't use `--force` unless necessary -3. **Check processes**: Verify what processes are running before killing -4. **Clean slate**: Use after crashes to ensure clean restart - -## Related Commands - -- `tapi start` - Start server with graceful shutdown support -- Process managers (Task Manager, Activity Monitor, htop) - Manual process inspection +# `kill` + +Force-stop any running server processes that tapi knows about (or can detect). + +```bash +tapi kill [--force] +``` + +Use when watch mode crashed and left zombies, or when CI needs a clean slate. For normal shutdown use `Ctrl+C` in `tapi start` or attach with `tapi start --existing`. + +- Detects `omp-server`, `samp-server`, etc. on Windows/Linux/macOS. +- Sends SIGTERM first, escalates to force kill if needed. +- Clears `.tapi/server-state.json` so the next `tapi start` begins fresh. + +`--force` skips the confirmation prompt. Use carefully in scripts. diff --git a/src/commands/setup/README.md b/src/commands/setup/README.md index 0519d93..3755ba7 100644 --- a/src/commands/setup/README.md +++ b/src/commands/setup/README.md @@ -1,381 +1,13 @@ -# `setup` Command - -Configure tapi settings for first-time use through an interactive setup wizard. - -## Overview - -The `setup` command provides an interactive wizard to configure tapi for first-time use. It guides users through essential configuration options including default author information, preferred editor settings, and GitHub integration. The setup process is designed to be user-friendly and only needs to be completed once. - -## Usage - -```bash -tapi setup [options] -``` - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-f, --force` | Force setup even if already configured | false | - -## Features - -### Interactive Setup Wizard -- **Guided Configuration**: Step-by-step setup process -- **Sensible Defaults**: Pre-filled values based on system information -- **Input Validation**: Ensures valid configuration values -- **One-time Setup**: Designed to be completed once per user - -### Configuration Categories -1. **Default Author**: Set your name for new projects -2. **Editor Preference**: Choose your preferred code editor -3. **GitHub Integration**: Configure GitHub token for package installation -4. **Setup Status**: Track completion status - -### Persistent Storage -- **Automatic Saving**: Configuration saved immediately after each step -- **Cross-Session Persistence**: Settings persist between command sessions -- **Backup Support**: Configuration can be reset and re-run -- **Validation**: Input validation and error handling - -## Setup Process - -### 1. Welcome and Introduction -- Displays welcome message and setup purpose -- Explains what will be configured -- Provides context for each configuration option - -### 2. Default Author Configuration -- Prompts for author name to use in new projects -- Pre-fills with existing configuration if available -- Validates input and provides feedback - -### 3. Editor Preference Selection -- Presents available editor options -- Explains integration features for each editor -- Saves preference for project setup - -### 4. GitHub Integration (Optional) -- Offers to configure GitHub token -- Explains benefits of GitHub integration -- Handles token input securely - -### 5. Completion and Summary -- Confirms all settings have been saved -- Provides next steps and usage information -- Shows how to modify settings later - -## Examples - -### First-Time Setup -```bash -$ tapi setup - -=== Welcome to tapi! === -This one-time setup will help configure tapi for your use. - -βœ” What name would you like to use as the default author for your projects? Developer -βœ” Which code editor do you use most for PAWN development? VS Code -βœ” Would you like to configure GitHub integration? Yes -βœ” Enter your GitHub personal access token: **************** - -=== Setup complete! === -βœ“ Default author: Developer -βœ“ Preferred editor: VS Code -βœ“ GitHub integration: Configured - -You can now use tapi. To change these settings in the future, run: tapi config -``` - -### Setup with Existing Configuration -```bash -$ tapi setup - -Setup has already been completed. - -Your current configuration: -β€’ Default author: Developer -β€’ Preferred editor: VS Code -β€’ GitHub integration: Configured - -To force setup to run again, use: tapi setup --force -To edit individual settings, use: tapi config -``` - -### Force Setup (Reconfigure) -```bash -$ tapi setup --force - -=== Welcome to tapi! === -This one-time setup will help configure tapi for your use. - -βœ” What name would you like to use as the default author for your projects? New Developer -βœ” Which code editor do you use most for PAWN development? Sublime Text -βœ” Would you like to configure GitHub integration? No - -=== Setup complete! === -βœ“ Default author: New Developer -βœ“ Preferred editor: Sublime Text -βœ“ GitHub integration: Not configured - -You can now use tapi. To change these settings in the future, run: tapi config -``` - -### Setup with Different Editor Options -```bash -$ tapi setup - -=== Welcome to tapi! === -This one-time setup will help configure tapi for your use. - -βœ” What name would you like to use as the default author for your projects? Developer -βœ” Which code editor do you use most for PAWN development? Other/None -βœ” Would you like to configure GitHub integration? Yes -βœ” Enter your GitHub personal access token: **************** - -=== Setup complete! === -βœ“ Default author: Developer -βœ“ Preferred editor: Other/None -βœ“ GitHub integration: Configured - -You can now use tapi. To change these settings in the future, run: tapi config -``` - -## Configuration Options - -### Default Author -**Purpose**: Sets the default author name for new projects -**Location**: `~/.tapi/preferences.json` -**Key**: `defaultAuthor` -**Example**: `"Developer Name"` - -**Usage**: This name will be pre-filled in the author field when running `tapi init` - -### Preferred Editor -**Purpose**: Determines which editor integration files to generate -**Location**: `~/.tapi/preferences.json` -**Key**: `editor` -**Options**: -- **VS Code**: Full integration with tasks, debugging, and IntelliSense -- **Sublime Text**: Basic configuration and syntax highlighting -- **Other/None**: No editor-specific setup - -**Usage**: Affects which editor files are created during `tapi init` - -### GitHub Integration -**Purpose**: Enables package installation from GitHub repositories -**Location**: `~/.tapi/preferences.json` -**Key**: `githubToken` -**Example**: `"ghp_xxxxxxxxxxxxxxxxxxxx"` - -**Benefits**: -- Access to private repositories -- Increased API rate limits -- Better package installation experience - -### Setup Status -**Purpose**: Tracks whether initial setup has been completed -**Location**: `~/.tapi/preferences.json` -**Key**: `setupComplete` -**Example**: `true` - -**Usage**: Prevents setup wizard from running automatically on subsequent uses - -## Configuration File Structure - -### preferences.json -```json -{ - "defaultAuthor": "Developer Name", - "editor": "VS Code", - "githubToken": "ghp_xxxxxxxxxxxxxxxxxxxx", - "setupComplete": true -} -``` - -### File Locations - -#### Windows -``` -C:\Users\\.tapi\preferences.json -``` - -#### Linux/macOS -``` -~/.tapi/preferences.json -``` - -## Interactive Prompts - -### Author Name Prompt -``` -What name would you like to use as the default author for your projects? -βœ” Developer Name -``` - -### Editor Selection Prompt -``` -Which code editor do you use most for PAWN development? -βœ” Select an option β€Ί [Use arrow keys to navigate] - β€’ Visual Studio Code (recommended) - β€’ Sublime Text - β€’ Other/None -``` - -### GitHub Integration Prompt -``` -Would you like to configure GitHub integration? (for package installations) -βœ” Yes/No β€Ί Yes - -Enter your GitHub personal access token (optional, press Enter to skip): -βœ” **************** -``` - -## Integration with Other Commands - -### init Command -- **Default Author**: Pre-fills author field in interactive prompts -- **Editor Preference**: Determines which editor files to generate -- **Setup Status**: Affects whether setup wizard runs automatically - -### config Command -- **Configuration Source**: Uses settings configured during setup -- **Modification Interface**: Provides way to change setup values -- **Reset Options**: Allows resetting to setup defaults - -### install Command -- **GitHub Token**: Used for package installation from GitHub -- **Rate Limits**: Increases API rate limits for authenticated requests -- **Private Repos**: Enables access to private repositories - -## Security Considerations - -### GitHub Token Storage -- **Location**: Local configuration file -- **Permissions**: User read/write only -- **Scope**: Minimal required permissions -- **Security**: Store securely, don't share - -### Token Permissions -Recommended GitHub token permissions: -- **repo**: Access to private repositories -- **read:packages**: Download packages -- **No admin permissions**: Minimal scope for security - -### Token Management -```bash -# View current token status -tapi config - -# Update token -tapi config -# Select "GitHub integration" β†’ "Update the token" - -# Remove token -tapi config -# Select "GitHub integration" β†’ "Remove the token" -``` - -## Error Handling - -### Common Setup Errors - -#### Permission Issues -```bash -βœ— Failed to save configuration - Error: EACCES: permission denied - Solution: Check directory permissions or run as administrator -``` - -#### Invalid Input -```bash -βœ— Invalid input provided - Error: Author name cannot be empty - Solution: Provide a valid author name -``` - -#### Network Issues (GitHub) -```bash -βœ— Failed to validate GitHub token - Error: Network timeout - Solution: Check internet connection and try again -``` - -### Recovery Options - -1. **Force Setup**: Use `--force` flag to re-run setup -2. **Manual Configuration**: Use `tapi config` to modify settings -3. **Delete Configuration**: Remove preferences.json to start fresh -4. **Re-run Setup**: Use `tapi setup --force` - -## Best Practices - -### Setup Process -1. **Complete Setup**: Run setup before using other commands -2. **Use Real Name**: Set your actual name as default author -3. **Choose Editor**: Select your preferred editor for integration -4. **Configure GitHub**: Set up token for package installation - -### Configuration Management -1. **One-time Setup**: Complete setup once per user -2. **Regular Review**: Use `tapi config` to review settings -3. **Update as Needed**: Modify settings when preferences change -4. **Backup Configuration**: Keep backup of preferences.json - -### Security -1. **Token Security**: Keep GitHub token secure and private -2. **Minimal Permissions**: Use tokens with minimal required permissions -3. **Regular Rotation**: Rotate GitHub tokens periodically -4. **Local Storage**: Be aware that tokens are stored locally - -## Troubleshooting - -### Common Issues - -#### "Setup has already been completed" -**Cause**: Setup wizard has been run before -**Solution**: Use `--force` flag to re-run setup - -#### "Permission denied" -**Cause**: Insufficient file system permissions -**Solution**: Check directory permissions or run as administrator - -#### "Invalid GitHub token" -**Cause**: Token expired or has insufficient permissions -**Solution**: Generate new token with proper permissions - -#### "Configuration file not found" -**Cause**: First-time usage or file corruption -**Solution**: Run setup to create initial configuration - -### Getting Help - -- **Force setup**: Use `--force` to re-run setup wizard -- **Manual configuration**: Use `tapi config` to modify settings -- **Reset configuration**: Use config command to reset settings -- **Check status**: Run `tapi config` to view current settings - -### Recovery Steps - -1. **Check setup status**: Run `tapi setup` to see current status -2. **Force re-setup**: Use `tapi setup --force` to reconfigure -3. **Manual configuration**: Use `tapi config` for individual settings -4. **Reset to defaults**: Use config command to reset all settings -5. **Delete and restart**: Remove preferences.json and run setup again - -## Next Steps After Setup - -### Immediate Actions -1. **Test Configuration**: Run `tapi config` to verify settings -2. **Create Project**: Run `tapi init` to create your first project -3. **Install Packages**: Use `tapi install` to add dependencies - -### Ongoing Usage -1. **Modify Settings**: Use `tapi config` to change preferences -2. **Update Token**: Refresh GitHub token when needed -3. **Review Settings**: Periodically review configuration - -### Advanced Usage -1. **Custom Configuration**: Edit preferences.json directly -2. **Team Settings**: Share configuration across team members -3. **Automation**: Use configuration in automated workflows +# `setup` + +One-time wizard that collects defaults (author name, editor preference, GitHub token). + +```bash +tapi setup [--force] +``` + +- Prompts for author, editor, and optional GitHub token. +- Saves configuration to `~/.tapi/config.json`. +- Run once after installing tapi; use `--force` to re-run and overwrite existing values. + +Need to change something later? Use [`tapi config`](../config/README.md). diff --git a/src/commands/start/README.md b/src/commands/start/README.md index ade184d..b884bcf 100644 --- a/src/commands/start/README.md +++ b/src/commands/start/README.md @@ -1,487 +1,29 @@ -# `start` Command - -Start the open.mp server with intelligent process management, cross-platform support, and integrated development environment features. - -## Overview - -The `start` command launches the SA-MP or open.mp server with proper configuration, process management, and development-friendly features. By default, the server runs inline in the current terminal with real-time output and interactive control. It handles server state tracking, prevents multiple instances, and provides graceful shutdown handling. - -## Usage - -```bash -tapi start [options] -``` - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-c, --config ` | Custom config file | config.json | -| `-e, --existing` | Connect to existing server | false | -| `-w, --window` | Start in new window (legacy mode) | false | -| `-d, --debug` | Start with debug output | false | -| `--watch` | Watch files and auto-rebuild + restart | false | - -### Global Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-v, --verbose` | Show detailed debug output | false | -| `-q, --quiet` | Minimize console output (show only progress bars) | false | -| `--log-to-file` | Save logs to file for debugging | false | - -## Features - -### Watch Mode - -- **Auto-rebuild**: Detects changes to `.pwn` and `.inc` files -- **Auto-restart**: Rebuilds and restarts server after file changes -- **Smart monitoring**: Watches `gamemodes/`, `filterscripts/`, `includes/` directories -- **Build validation**: Only restarts if build succeeds -- **Development-friendly**: Perfect for rapid iteration during development - -### Process Management - -- **State Tracking**: Monitors server running status -- **Instance Prevention**: Prevents multiple server instances -- **Graceful Shutdown**: Handles Ctrl+C and process termination -- **Crash Recovery**: Detects and reports server crashes - -### Cross-Platform Support - -- **Windows**: Native process management and window handling -- **Linux**: Terminal-based server management -- **macOS**: Unix-compatible process handling - -### Real-time Output & Control - -- **Inline Terminal**: Server runs directly in your terminal by default -- **Live Output**: See server logs and messages in real-time -- **Interactive Control**: Send commands directly to the server -- **Graceful Shutdown**: Clean server stop with Ctrl+C - -### Configuration Support - -- **Custom Config Files**: Use different server configurations - -- **Argument Passing**: Pass custom arguments to server - -## Server Startup Process - -### 1. Pre-flight Checks -- Validates server executable existence -- Checks for configuration files -- Verifies project structure - -### 2. Process State Check -- Checks if server is already running -- Offers connection to existing instance -- Prevents multiple server instances - -### 3. Configuration Loading -- Loads server configuration -- Validates config file format -- Applies custom settings - -### 4. Server Launch -- Spawns server process -- Captures output streams -- Manages process lifecycle - -### 5. State Management -- Tracks server process ID -- Monitors server status -- Handles graceful shutdown - -## Examples - -### Watch Mode Development (Recommended for Development) -```bash -$ tapi start --watch - -πŸ”„ Starting watch mode... -Press Ctrl+C to stop watching and exit - -πŸ”¨ Building project... -βœ… Build successful -πŸš€ Starting server... -βœ… Server started in watch mode - -[Real-time server output appears here...] -[10:30:00] Server plugins loaded. -[10:30:00] Started server on port: 7777 - -# Edit a .pwn file and save... -πŸ“ File changed: gamemodes/myproject.pwn -πŸ›‘ Stopping server... -πŸ”¨ Building project... -βœ… Build successful -πŸš€ Starting server... -βœ… Server started in watch mode -``` - -### Basic Server Start (Inline Terminal - Default) -```bash -$ tapi start - -=== Starting open.mp server... === -β†’ Working directory: /path/to/project -β†’ Server executable: omp-server.exe -β†’ Config file: config.json -β†’ Gamemodes: myproject - -Starting server in current terminal... -Press Ctrl+C to stop the server - -β†’ Server started with PID: 12345 - -[Real-time server output appears here...] -[10:30:00] Server plugins loaded. -[10:30:00] Started server on port: 7777, with maxplayers: 50 -[10:30:00] Gamemode 'myproject' loaded. - -^C -β†’ Received SIGINT, stopping server... -Server stopped normally -``` - -### Legacy Window Mode -```bash -$ tapi start --window - -=== Starting open.mp server... === -β†’ Working directory: /path/to/project -β†’ Server executable: omp-server.exe -β†’ Config file: config.json - -Starting server in a new window (legacy mode)... -Tip: Remove --window flag to run server inline with real-time output - -Server started in a new window -Check your taskbar for the server window -``` - - - -### Custom Configuration -```bash -$ tapi start -c production.json - -=== Starting open.mp server... === -β„Ή Working directory: /path/to/project -β„Ή Server executable: omp-server.exe -β„Ή Configuration: production.json - -Starting server in the current terminal... -Press Ctrl+C to stop the server. -``` - -### Connect to Existing Server -```bash -$ tapi start --existing - -βœ“ Connected to existing server instance -``` - -## Server Management - -### Stopping the Server - -#### Terminal Mode -- **Ctrl+C**: Gracefully stops the server -- **Automatic cleanup**: Clears server state and exits - -#### Window Mode -- **Ctrl+C**: In the terminal that started the server -- **Manual**: Close the server window -- **tapi kill**: Emergency cleanup for stuck processes - -### Emergency Cleanup - -If you need to force-kill unresponsive server processes: - -```bash -tapi kill [options] -``` - -#### Options - -| Option | Description | -|--------|-------------| -| `-f, --force` | Skip confirmation prompt | - -#### Examples - -```bash -# Interactive cleanup (with confirmation) -$ tapi kill - -⚠️ This will forcefully terminate ALL SA-MP/open.mp server processes. -πŸ’‘ For normal server shutdown, use Ctrl+C in the terminal running "tapi start" - -Continue? (y/N): y - -πŸ’€ Force killing server processes... -βœ… Killed omp-server.exe -βœ… Server processes terminated and state cleared - -# Force cleanup (no confirmation) -$ tapi kill --force - -πŸ’€ Force killing server processes... -βœ… Server processes terminated and state cleared -``` - -### Server Status - -The start command tracks server state including: -- **Process ID**: For process management -- **Start Time**: When the server was launched -- **Arguments**: Command line arguments used -- **Window Mode**: Whether running in separate window -- **Temp Files**: Temporary files to clean up - -## Configuration Files - -### config.json (Default) -```json -{ - "hostname": "open.mp Server", - "language": "English", - "maxplayers": 50, - "port": 7777, - "rcon_password": "changeme", - "password": "", - "announce": true, - "chatlogging": true, - "weburl": "https://open.mp", - "onfoot_rate": 30, - "incar_rate": 30, - "weapon_rate": 30, - "stream_distance": 300.0, - "stream_rate": 1000, - "maxnpc": 0, - "logtimeformat": "[%H:%M:%S]", - "plugins": [], - "filterscripts": [], - "pawn": { - "main_scripts": ["gamemodes/my-gamemode"], - "auto_reload": true - } -} -``` - -### Custom Configuration -```json -{ - "hostname": "My Custom Server", - "maxplayers": 100, - "port": 7778, - "rcon_password": "secure_password", - "password": "server_password", - "announce": false, - "plugins": ["mysql", "sscanf"], - "filterscripts": ["anticheat", "admin"], - "pawn": { - "main_scripts": ["gamemodes/custom-gamemode"], - "auto_reload": false - } -} -``` - -## Process Management - -### Server State Tracking - -The start command tracks server state using a state file: - -```json -{ - "pid": 12345, - "startTime": "2024-01-15T10:30:00.000Z", - "config": "config.json", - "workingDirectory": "/path/to/project", - "arguments": ["--config=config.json"], - "windowMode": false, - "tempFiles": [] -} -``` - -### Multiple Instance Prevention - -When a server is already running: - -```bash -$ tapi start - -βœ— Server is already running. Use Ctrl+C to stop it first. -β„Ή Or use --existing flag to connect to the running server - -$ tapi start --existing - -βœ“ Connected to existing server instance -``` - -### Graceful Shutdown - -When you press Ctrl+C in terminal mode: - -```bash -Starting server in the current terminal... -Press Ctrl+C to stop the server. - -^C -β„Ή Received Ctrl+C, stopping server -βœ“ Server stopped successfully -``` - -## Error Handling - -### Common Startup Errors - -#### Server Executable Not Found -```bash -βœ— Server executable not found. Make sure you are in the correct directory. - -Expected server files: -β€’ omp-server.exe (Windows) -β€’ omp-server (Linux/macOS) - -Run "tapi init" to set up a new project with server files -``` - -#### Configuration File Not Found -```bash -βœ— Configuration file not found: custom-config.json -β„Ή Available config files: config.json, production.json -``` - -#### Port Already in Use -```bash -βœ— Failed to start server - Error: Port 7777 is already in use - Solution: Change port in config.json or stop conflicting server -``` - -#### Permission Issues -```bash -βœ— Failed to start server - Error: EACCES: permission denied - Solution: Run as administrator or check file permissions -``` - -### Error Recovery - -The start command provides: - -1. **Clear error messages** with specific causes -2. **Helpful suggestions** for resolution -3. **Alternative options** when available -4. **State cleanup** on errors - -## Platform-Specific Behavior - -### Windows -- **Process Management**: Native Windows process handling -- **Window Mode**: Optional new window creation with proper PID tracking -- **VS Code Integration**: Automatic terminal detection -- **File Extensions**: Uses .exe executables - -### Linux/macOS -- **Process Management**: Unix process handling -- **Terminal Mode**: Always runs in current terminal -- **Signal Handling**: Proper SIGTERM/SIGINT handling -- **File Extensions**: Uses binary executables - -## Integration Features - -### VS Code Integration - -When VS Code is detected: - -```bash -β„Ή VS Code integration detected -β„Ή Server will run in integrated terminal -β„Ή Debug support available -``` - -### Development Workflow - -1. **Start server**: `tapi start` -2. **Edit code**: Make changes in your editor -3. **Build code**: `tapi build` (or Ctrl+Shift+B in VS Code) -4. **Auto-reload**: Server automatically reloads changes (if enabled) -5. **Stop server**: Ctrl+C (normal) or `tapi kill` (emergency) - -### Continuous Development - -```bash -# Terminal 1: Start server -tapi start - -# Terminal 2: Build and watch -tapi build --watch - -# Terminal 3: Monitor logs -tail -f server.log - -# Terminal 4: Emergency cleanup if needed -tapi kill -``` - -## Performance Considerations - -### Resource Usage -- **Memory**: Server typically uses 50-200MB RAM -- **CPU**: Minimal when idle, scales with player count -- **Network**: Bandwidth depends on player activity - -### Optimization Tips -- **Debug mode**: Disable in production for better performance -- **Logging**: Reduce log verbosity for better performance -- **Plugins**: Load only necessary plugins -- **Auto-reload**: Disable in production - -## Troubleshooting - -### Common Issues - -#### "Server is already running" -**Cause**: Another server instance is active -**Solution**: Use Ctrl+C to stop the server - -#### "Server executable not found" -**Cause**: Not in a project directory or server not installed -**Solution**: Run `tapi init` to set up project - -#### "Port already in use" -**Cause**: Another service is using the port -**Solution**: Change port in config.json or stop conflicting service - -#### "Permission denied" -**Cause**: Insufficient permissions -**Solution**: Run as administrator or check file permissions - -#### "Configuration file not found" -**Cause**: Specified config file doesn't exist -**Solution**: Check file path or use default config.json - -#### "Server won't stop" -**Cause**: Server process is unresponsive -**Solution**: Use Ctrl+C or `tapi kill` for emergency cleanup - -### Getting Help - -- **Verbose mode**: Use `--verbose` for detailed startup information -- **Debug mode**: Use `--debug` to see server debug output -- **Check logs**: Review server logs for specific errors -- **Validate config**: Ensure config.json is properly formatted -- **Emergency cleanup**: Use `tapi kill` for stuck processes - -### Recovery Steps - -1. **Stop all instances**: Use `tapi kill --force` -2. **Check configuration**: Validate config.json format -3. **Verify files**: Ensure server executable exists -4. **Try debug mode**: Use `--debug` for detailed error information -5. **Reinstall**: Run `tapi init` to reinstall server files +# `start` + +Run the open.mp / SA-MP server with process tracking and optional watch mode. + +```bash +tapi start [options] +``` + +## Options + +| Flag | Description | +|------|-------------| +| `-c, --config ` | Use a specific server config (`config.json`, `server.cfg`, ...) | +| `-e, --existing` | Attach to an already running server tracked by tapi | +| `-w, --window` | Launch in a new OS window (legacy behaviour) | +| `-d, --debug` | Stream verbose server output | +| `--watch` | Watch `.pwn/.inc` files, rebuild, and restart automatically | + +### Watch mode + +`--watch` monitors `gamemodes/`, `filterscripts/`, and `includes/`: + +1. Build (`tapi build`). +2. Start server. +3. On change β†’ stop, rebuild, restart (only if build succeeds). + +### Process state + +`tapi start` stores metadata in `.tapi/server-state.json` so `--existing` and `tapi kill` know which process to manage. Ctrl+C performs a graceful shutdown; crashes propagate a non-zero exit code with logs left in the terminal. diff --git a/src/commands/update/README.md b/src/commands/update/README.md index 3d325ac..dbadaa8 100644 --- a/src/commands/update/README.md +++ b/src/commands/update/README.md @@ -1,329 +1,14 @@ -# `update` Command - -Check for and install tapi updates automatically from GitHub releases. - -## Overview - -The `update` command provides an automated way to keep tapi up-to-date by checking GitHub releases and downloading the latest version. It supports both manual updates and automatic checking. - -## Usage - -```bash -tapi update [options] -``` - -## Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-c, --check` | Only check for updates, do not install | false | -| `-f, --force` | Force update even if already on latest version | false | -| `--pre` | Include pre-release versions | false | - -### Global Options - -| Option | Description | Default | -|--------|-------------|---------| -| `-v, --verbose` | Show detailed debug output | false | -| `-q, --quiet` | Minimize console output (show only progress bars) | false | -| `--log-to-file` | Save logs to file for debugging | false | - -## Features - -### Automatic Update Detection -- Checks GitHub releases for new versions -- Compares semantic versions intelligently -- Caches update checks (once per 24 hours) -- Shows release notes and changelog - -### Cross-Platform Support -- **Windows**: Downloads and runs the latest installer -- **Linux**: Downloads and replaces the binary -- **macOS**: Downloads and replaces the binary - -### Safe Update Process -- Creates backup before updating (Linux/macOS) -- Validates downloads before installation -- Rollback on failure -- Permission handling - -### Update Notifications -- Automatic background checks on startup -- Non-intrusive notifications -- Respects user preferences - -## Examples - -### Check for Updates -```bash -$ tapi update --check - -Checking for tapi updates... -Current version: 1.0.0-alpha.1 -Latest version: 1.0.1 - -New version available: 1.0.1 -Release name: Bug Fixes and Improvements -Changes: -- Fixed installer version check issue -- Added auto-update functionality -- Improved error handling - -Use "tapi update" to install the update -``` - -### Install Update -```bash -$ tapi update - -Checking for tapi updates... -Current version: 1.0.0-alpha.1 -Latest version: 1.0.1 - -New version available: 1.0.1 -Release name: Bug Fixes and Improvements -Changes: -- Fixed installer version check issue -- Added auto-update functionality -- Improved error handling - -? Update to version 1.0.1? (Y/n) y - -Downloading tapi-setup-1.0.1.exe... -Starting installer... -The installer will now run. This terminal will close. -``` - -### Force Update -```bash -$ tapi update --force - -Checking for tapi updates... -Current version: 1.0.1 -Latest version: 1.0.1 - -New version available: 1.0.1 (forced) -Release name: Bug Fixes and Improvements - -? Update to version 1.0.1? (Y/n) y - -Downloading tapi-setup-1.0.1.exe... -Starting installer... -``` - -### Include Pre-releases -```bash -$ tapi update --pre --check - -Checking for tapi updates... -Current version: 1.0.0 -Latest version: 1.1.0-beta.1 - -New version available: 1.1.0-beta.1 -Release name: Beta Testing Release -Changes: -- New experimental features -- Performance improvements -- Breaking changes (see migration guide) - -Use "tapi update --pre" to install the pre-release -``` - -### Already Up to Date -```bash -$ tapi update - -Checking for tapi updates... -Current version: 1.0.1 -Latest version: 1.0.1 - -You are already on the latest version (1.0.1) -``` - -## Automatic Update Checking - -### Background Checks -tapi automatically checks for updates in the background when: -- Running any command (except `--help` and `--version`) -- Not during first setup -- Once every 24 hours (cached) - -### Update Notifications -When a new version is available: - -```bash -$ tapi build - -πŸŽ‰ A new version of tapi is available: 1.0.1 -Run "tapi update" to upgrade - -=== Building PAWN project... === -... -``` - -### Privacy -- Update checks are anonymous -- No telemetry or usage data collected -- Only version comparison data sent - -## Platform-Specific Behavior - -### Windows -- Downloads the latest `.exe` installer -- Runs installer automatically -- Current process exits to allow installation -- Requires admin privileges for installation - -### Linux/macOS -- Downloads the latest binary -- Creates backup of current binary -- Replaces binary in-place -- Makes new binary executable -- Restores backup on failure - -## Update Process Flow - -### 1. Version Check -- Fetches latest release from GitHub API -- Compares semantic versions -- Filters drafts and pre-releases (unless `--pre`) - -### 2. User Confirmation -- Shows release information -- Displays changelog/release notes -- Prompts for confirmation (unless scripted) - -### 3. Download -- Downloads appropriate asset for platform -- Validates file integrity -- Handles redirects and GitHub CDN - -### 4. Installation -- **Windows**: Launches installer and exits -- **Unix**: Backs up, replaces, and validates binary - -### 5. Cleanup -- Removes temporary files -- Updates cache -- Reports success/failure - -## Error Handling - -### Common Issues - -#### Network Connectivity -```bash -Failed to update tapi -Error: Request timeout -``` -**Solution**: Check internet connection and try again - -#### GitHub API Rate Limits -```bash -Failed to check for updates: GitHub API returned 403 -``` -**Solution**: Wait an hour and try again (rate limits reset) - -#### Permission Denied -```bash -Failed to update tapi -Error: EACCES: permission denied -``` -**Solution**: -- Windows: Run as Administrator -- Linux/macOS: Use `sudo` or check file permissions - -#### No Installer Available -```bash -Failed to update tapi -Error: No installer found for linux -``` -**Solution**: Platform not supported or release incomplete - -#### Download Failure -```bash -Failed to update tapi -Error: Download failed: 404 -``` -**Solution**: Release assets may be missing, try again later - -### Recovery - -If an update fails: - -1. **Check error message** for specific issue -2. **Verify internet connection** -3. **Try with `--force` flag** -4. **Manual installation** as fallback -5. **Report issue** if persistent - -## Configuration - -### Cache Location -Update check cache is stored at: -- **Windows**: `%USERPROFILE%\.tapi\update-cache.json` -- **Linux/macOS**: `~/.tapi/update-cache.json` - -### Cache Structure -```json -{ - "lastCheck": "2024-01-15T10:30:00.000Z", - "hasUpdate": true, - "latestVersion": "1.0.1", - "currentVersion": "1.0.0" -} -``` - -### Manual Cache Reset -```bash -# Windows -del "%USERPROFILE%\.tapi\update-cache.json" - -# Linux/macOS -rm ~/.tapi/update-cache.json -``` - -## Integration with CI/CD - -### Automated Updates -```bash -# Check for updates without installing -tapi update --check - -# Force update in CI environment -echo "y" | tapi update --force -``` - -### Version Pinning -For stable CI environments, pin to specific versions rather than using auto-update. - -## Security Considerations - -### Secure Downloads -- Downloads from official GitHub releases only -- Validates GitHub API responses -- Uses HTTPS for all connections -- Verifies file integrity where possible - -### Update Authenticity -- Updates come from official repository -- GitHub provides cryptographic signatures -- No third-party update servers - -### Permissions -- Requires appropriate permissions for installation -- Creates backups before modification -- Fails safely on permission errors - -## Related Commands - -- `tapi --version` - Show current version -- `tapi config` - View configuration settings -- `tapi setup` - Reconfigure after updates - -## Notes - -- Update checks respect GitHub API rate limits -- Pre-releases are excluded from automatic checks -- Updates preserve user configuration and data -- Internet connection required for update checks +# `update` + +Check whether a newer tapi release is available. (Automatic updating will arrive in a future release.) + +```bash +tapi update [--check|--force|--pre] +``` + +- Queries GitHub releases and prints the latest tag. +- `--check` does the same (alias). +- `--pre` includes prerelease versions. +- `--force` placeholder for future auto-update logic. + +For now, pull the repo or reinstall when a newer version is reported. From d1b63e4d5e751fb2dc79a8b4c35f57e37e8e7a09 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 10:59:21 +0100 Subject: [PATCH 13/20] docs: add init presets documentation for reusable project configurations --- docs/init-presets.md | 118 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/init-presets.md diff --git a/docs/init-presets.md b/docs/init-presets.md new file mode 100644 index 0000000..6437c1b --- /dev/null +++ b/docs/init-presets.md @@ -0,0 +1,118 @@ +# Init Presets + +`tapi init` can run from a preset file so teams can reuse the same answers across projects or automate non-interactive setups. + +## Where presets are loaded from + +When you run `tapi init`, the loader checks for the first preset in this order: + +1. A path or preset name passed to `--preset`. + - An absolute path is used as-is. + - A relative path is resolved against the current working directory. + - Otherwise, the name is matched against project presets in `.tapi/workflows/` and finally against global presets in `~/.tapi/workflows/`, using the suffixes `.yml`, `.yaml`, `.json`. +2. Project defaults in `.tapi/workflows/init.*` (falling back to `.tapi/workflows/default.*`). +3. Global defaults in `~/.tapi/workflows/init.*` or `default.*`. + +The first file that exists and parses successfully is used. Empty files are rejected. + +## CLI flags + +| Flag | Description | +| --- | --- | +| `--preset ` | Load answers from a preset file or named preset. | +| `--accept-preset` | Apply preset defaults without forcing full non-interactive mode. Missing answers still trigger prompts. | +| `--non-interactive` | Skip all prompts and run with preset / CLI values only. Fails if required values are missing. | +| `-q, --quiet` | Reduce console output (can also be set from the preset). | +| `--legacy-samp`, `--skip-compiler` | Still honoured; preset values win when CLI flags are not provided. | + +## Placeholder reference + +You can reference placeholders inside preset strings (project name, description, etc.). At runtime they are replaced with context values: + +| Placeholder | Description | +| --- | --- | +| `${folder}` | Basename of the current working directory. | +| `${slug}` | Lowercase / kebab-case version of `${folder}`. | +| `${cwd}` | Absolute path to the current working directory. | +| `${user}` | System username (`os.userInfo`, falling back to `USER`/`USERNAME`). | +| `${hostname}` | Machine hostname. | +| `${date}` | ISO timestamp of the init run. | +| `${timestamp}` | Numeric timestamp (`Date.now()`). | +| `${year}` | 4-digit year. | +| `${gitBranch}` | `git rev-parse --abbrev-ref HEAD` (empty when unavailable). | +| `${projectType}` | `project.projectType` from the preset (empty if not set). | +| `${name}` | The preset project name (useful inside descriptions). | +| `${editor}` | The preset editor value (after defaults). | +| `${env:NAME}` | Value of environment variable `NAME` (empty if unset). | + +## Example preset + +```yaml +project: + name: ${folder} + description: "${user}'s ${projectType} sandbox" + projectType: gamemode + editor: VS Code + initGit: true + downloadServer: true +compiler: + downloadCompiler: false + downloadStdLib: true +options: + legacySamp: false + skipCompiler: false +acceptPreset: true +``` + +Drop this file under `.tapi/workflows/init.yml` (or `init-.yml`) to make it project-specific, or save it in `~/.tapi/workflows/init-.yml` to share it globally. + +Project-level locations supported: + +- `.tapi/workflows/init.yml` for the default workflow +- `.tapi/workflows/init-.yml` for named variants (called with `tapi init --preset `) +- `.tapi/workflows/init/.yml` (optional subdirectory form, also invoked with `--preset `) + +## Usage examples + +```bash +# Use project-local preset, allow prompts for missing answers +$tapi init --preset .tapi/workflows/init.yml + +# Use global preset called "openmp" (stored as `~/.tapi/workflows/init-openmp.yml`) and accept all defaults +$tapi init --preset openmp --accept-preset + +# Fully unattended init (fails if a required value is missing) +$tapi init --preset ci --non-interactive --quiet +``` + +## Tips + +- Presets only fill in values that aren’t already provided on the CLI. Passing `--name` overrides a preset’s `project.name`. +- Non-interactive mode is ideal for CI/CD, but make sure every required field is supplied (name, description, author, etc.). +- Share presets in version control (`.tapi/workflows/init.yml`) to give new contributors a zero-prompt getting started experience. + +## Automating downloads + +Workflows can override the compiler, standard library, or other downloadable assets. This is handy for private bundles or internal mirrors. URLs may include `${platform}` (resolved to `windows` or `linux`). + +```yaml +compiler: + downloadCompiler: true + compilerVersion: latest + compilerDownloadUrl: "https://example.com/compiler-${platform}.zip" + downloadStdLib: true + stdLibDownloadUrl: "https://example.com/stdlib.zip" +``` + +### Secrets and authentication + +If a URL needs credentials, load them into the environment before running `tapi`. Workflows can reference them with `${env:VAR}`. + +```yaml +compiler: + compilerDownloadUrl: "https://internal.example.com/compiler-${platform}.zip?token=${env:TAPI_TOKEN}" +``` + +> `tapi` does **not** read `.env` files automatically. Make sure your shell (or CI pipeline) exports any variables referenced in the workflow first (e.g. `export TAPI_TOKEN=...`). + +For interactive sessions, any fields supplied in the workflow are used as prompt defaults; anything missing still triggers a question. In non-interactive mode (or when `acceptPreset` is true) only the values present in the workflow/CLI will be used, and missing required fields will abort the run. From a02ae587e8e8826ce500faa560ecc5fcf41ffc8f Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 10:59:38 +0100 Subject: [PATCH 14/20] refactor: improve ANSI stripping logic in logger tests --- tests/unit/logger.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/logger.test.ts b/tests/unit/logger.test.ts index 50c4e09..964aadd 100644 --- a/tests/unit/logger.test.ts +++ b/tests/unit/logger.test.ts @@ -1,7 +1,8 @@ import { logger } from '../../src/utils/logger'; -const stripAnsi = (value: string): string => - value.replace(/\u001b\[[0-9;]*m/g, ''); +const ANSI_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'); + +const stripAnsi = (value: string): string => value.replace(ANSI_REGEX, ''); describe('Logger', () => { let consoleSpy: jest.SpyInstance; From 221711355ef11ad03321252ba196cb2d3e9bdf6c Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 10:59:48 +0100 Subject: [PATCH 15/20] feat: enhance compiler setup with custom download URLs and improved standard library management --- src/commands/init/compiler.ts | 389 ++++++++++++++++++++-------------- 1 file changed, 227 insertions(+), 162 deletions(-) diff --git a/src/commands/init/compiler.ts b/src/commands/init/compiler.ts index 3c8771c..ae2568f 100644 --- a/src/commands/init/compiler.ts +++ b/src/commands/init/compiler.ts @@ -7,6 +7,9 @@ import { CompilerAnswers } from './types'; import { downloadFileWithProgress } from './serverDownload'; const STD_LIB_SENTINELS = ['a_samp.inc', 'open.mp.inc', 'omp.inc', 'open.mp']; +const STD_LIB_TEMP = 'omp_stdlib_temp'; + +type PlatformSlug = 'windows' | 'linux'; /** * Detect whether a standard library already exists in common include locations. @@ -42,30 +45,31 @@ export function hasExistingStandardLibrary(): boolean { export async function setupCompiler( compilerAnswers: CompilerAnswers ): Promise { - if (compilerAnswers.downloadCompiler) { - try { - await downloadCompiler( - compilerAnswers.compilerVersion, - compilerAnswers.keepQawno !== false, // default to true if undefined - compilerAnswers.installCompilerFolder || false, - compilerAnswers.downgradeQawno || false - ); + if (compilerAnswers.downloadCompiler) { + try { + await downloadCompiler( + compilerAnswers.compilerVersion, + compilerAnswers.keepQawno !== false, // default to true if undefined + compilerAnswers.installCompilerFolder || false, + compilerAnswers.downgradeQawno || false, + compilerAnswers.compilerDownloadUrl + ); if (logger.getVerbosity() === 'verbose') { logger.success('Compiler installed'); } - } catch { + } catch { // error handled within download function } } - if (compilerAnswers.downloadStdLib) { - try { - await downloadopenmpStdLib(); + if (compilerAnswers.downloadStdLib) { + try { + await downloadopenmpStdLib(compilerAnswers.stdLibDownloadUrl); if (logger.getVerbosity() === 'verbose') { logger.success('Standard library installed'); - } - } catch { - // error handled within download function + } + } catch { + // error handled within download function } } } @@ -96,7 +100,162 @@ function getCompilerRepository(version: string): { // 3.10.10 and older use pawn-lang repo return { user: 'pawn-lang', repo: 'compiler' }; -} +} + +function getPlatformSlug(): PlatformSlug { + switch (process.platform) { + case 'win32': + return 'windows'; + case 'linux': + return 'linux'; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } +} + +function resolveStdLibTargetDir(): { includesDir: string; includesDirName: string } { + const qawnoPath = path.join(process.cwd(), 'qawno'); + const compilerPath = path.join(process.cwd(), 'compiler'); + + if (fs.existsSync(qawnoPath)) { + return { + includesDir: path.resolve(qawnoPath, 'include'), + includesDirName: 'qawno/include', + }; + } + + if (fs.existsSync(compilerPath)) { + return { + includesDir: path.resolve(compilerPath, 'include'), + includesDirName: 'compiler/include', + }; + } + + throw new Error( + 'No qawno/ or compiler/ directory found. Cannot install standard library.' + ); +} + +function ensureDirExists(dir: string, label: string): void { + if (!dir.startsWith(process.cwd())) { + throw new Error('Invalid includes path outside project root'); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + if (logger.getVerbosity() === 'verbose') { + logger.detail(`Created directory: ${label}`); + } + } +} + +function hasExistingStdLib(dir: string, label: string): boolean { + const files = fs.existsSync(dir) ? fs.readdirSync(dir) : []; + const exists = files.some((file) => STD_LIB_SENTINELS.includes(file.toLowerCase())); + if (exists && logger.getVerbosity() === 'verbose') { + logger.detail(`Standard library files already exist in ${label}, skipping download`); + logger.detail('If you want to update the standard library, please remove existing .inc files first'); + } + return exists; +} + +async function downloadStdLibArchive( + url: string, + includesDir: string, + includesDirName: string +): Promise { + const tempDir = path.join(process.cwd(), STD_LIB_TEMP); + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + fs.mkdirSync(tempDir, { recursive: true }); + + const archiveName = path.basename(url.split('?')[0] || 'stdlib.zip'); + const archiveRelative = path.join(STD_LIB_TEMP, archiveName); + await downloadFileWithProgress(url, archiveRelative); + + const archivePath = path.join(tempDir, archiveName); + await extractArchive(archivePath, tempDir); + + const entries = fs.readdirSync(tempDir).filter((item) => item !== archiveName); + let sourceDir = tempDir; + if (entries.length === 1) { + const candidate = path.join(tempDir, entries[0]); + if (fs.statSync(candidate).isDirectory()) { + sourceDir = candidate; + } + } + + const includeCandidate = path.join(sourceDir, 'include'); + if (fs.existsSync(includeCandidate) && fs.statSync(includeCandidate).isDirectory()) { + sourceDir = includeCandidate; + } + + fs.cpSync(sourceDir, includesDir, { recursive: true }); + + fs.rmSync(tempDir, { recursive: true, force: true }); + + if (logger.getVerbosity() === 'verbose') { + logger.detail(`Downloaded standard library archive to ${includesDirName}`); + } +} + +async function extractArchive(archivePath: string, destination: string): Promise { + if (archivePath.endsWith('.zip')) { + const AdmZip = require('adm-zip'); + const zip = new AdmZip(archivePath); + zip.extractAllTo(destination, true); + return; + } + + if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) { + const tar = require('tar'); + await tar.extract({ file: archivePath, cwd: destination }); + return; + } + + throw new Error(`Unsupported archive format for ${archivePath}`); +} + +async function cloneStdLibRepo(includesDir: string): Promise { + const git = simpleGit(); + await git.clone('https://github.com/openmultiplayer/omp-stdlib.git', includesDir, ['--depth=1']); +} + +function pruneStdLibArtifacts(includesDir: string): void { + const unnecessaryFilesAndDirs = [ + 'README.md', + '.git', + 'documentation', + '.editorconfig', + '.gitattributes', + 'LICENSE.md', + 'pawndoc.xsl', + ]; + + for (const item of unnecessaryFilesAndDirs) { + const itemPath = path.resolve(includesDir, item); + if (!itemPath.startsWith(includesDir)) { + logger.warn(`Skipping invalid path: ${itemPath}`); + continue; + } + + if (!fs.existsSync(itemPath)) { + continue; + } + + try { + if (fs.statSync(itemPath).isDirectory()) { + fs.rmSync(itemPath, { recursive: true, force: true }); + } else { + fs.unlinkSync(itemPath); + } + } catch (error) { + logger.warn( + `Failed to remove ${item}: ${error instanceof Error ? error.message : 'unknown error'}` + ); + } + } +} /** * Download and extract the specified compiler version to the project. @@ -105,16 +264,17 @@ export async function downloadCompiler( versionInput: string, keepQawno: boolean = true, installCompilerFolder: boolean = false, - downgradeQawno: boolean = false -): Promise { - const version = - versionInput === 'latest' ? await getLatestCompilerVersion() : versionInput; - - // Store original version with 'v' for tag, and clean version for filenames - const tagVersion = version.startsWith('v') ? version : `v${version}`; - const cleanVersion = version.startsWith('v') ? version.substring(1) : version; - - const { user, repo } = getCompilerRepository(cleanVersion); + downgradeQawno: boolean = false, + customUrl?: string +): Promise { + const version = + versionInput === 'latest' ? await getLatestCompilerVersion() : versionInput; + + // Store original version with 'v' for tag, and clean version for filenames + const tagVersion = version.startsWith('v') ? version : `v${version}`; + const cleanVersion = version.startsWith('v') ? version.substring(1) : version; + + const { user, repo } = getCompilerRepository(cleanVersion); const compilerTmpDir = path.join(process.cwd(), 'compiler_temp'); if (fs.existsSync(compilerTmpDir)) { @@ -133,21 +293,27 @@ export async function downloadCompiler( } fs.mkdirSync(compilerTmpDir, { recursive: true }); - let downloadUrl: string, filename: string; - switch (process.platform) { - case 'win32': { - downloadUrl = `https://github.com/${user}/${repo}/releases/download/${tagVersion}/pawnc-${cleanVersion}-windows.zip`; - filename = `pawnc-${cleanVersion}-windows.zip`; - break; - } - case 'linux': { - downloadUrl = `https://github.com/${user}/${repo}/releases/download/${tagVersion}/pawnc-${cleanVersion}-linux.tar.gz`; - filename = `pawnc-${cleanVersion}-linux.tar.gz`; - break; - } - default: - throw new Error(`Unsupported platform: ${process.platform}`); - } + let downloadUrl: string; + let filename: string; + if (customUrl) { + downloadUrl = customUrl.replace('${platform}', getPlatformSlug()); + filename = path.basename(downloadUrl.split('?')[0]); + } else { + switch (process.platform) { + case 'win32': { + downloadUrl = `https://github.com/${user}/${repo}/releases/download/${tagVersion}/pawnc-${cleanVersion}-windows.zip`; + filename = `pawnc-${cleanVersion}-windows.zip`; + break; + } + case 'linux': { + downloadUrl = `https://github.com/${user}/${repo}/releases/download/${tagVersion}/pawnc-${cleanVersion}-linux.tar.gz`; + filename = `pawnc-${cleanVersion}-linux.tar.gz`; + break; + } + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } try { if (logger.getVerbosity() === 'verbose') { @@ -203,128 +369,27 @@ export async function downloadCompiler( * Retrieve and install the open.mp standard library into the active compiler directory. */ export async function downloadopenmpStdLib( - targetLocation?: 'qawno' | 'compiler' + customUrl?: string ): Promise { - // Only show spinner in verbose mode - if (logger.getVerbosity() === 'verbose') { - logger.detail('Downloading open.mp standard library...'); - } - - try { - // Determine where to install based on which compiler setup exists - let includesDir: string; - let includesDirName: string; - - if ( - targetLocation === 'qawno' || - fs.existsSync(path.join(process.cwd(), 'qawno')) - ) { - includesDir = path.resolve(process.cwd(), 'qawno', 'include'); - includesDirName = 'qawno/include'; - } else if ( - targetLocation === 'compiler' || - fs.existsSync(path.join(process.cwd(), 'compiler')) - ) { - includesDir = path.resolve(process.cwd(), 'compiler', 'include'); - includesDirName = 'compiler/include'; - } else { - // Fallback - doubt we will ever hit this - throw new Error( - 'No qawno/ or compiler/ directory found. Cannot install standard library.' - ); - } - - if (!includesDir.startsWith(process.cwd())) { - throw new Error( - 'Invalid path: includes directory is outside the project root' - ); - } - - // Ensure the directory exists - if (!fs.existsSync(includesDir)) { - fs.mkdirSync(includesDir, { recursive: true }); - if (logger.getVerbosity() === 'verbose') { - logger.detail(`Created directory: ${includesDirName}`); - } - } - - // Check if standard library files already exist - const files = fs.readdirSync(includesDir); - const hasStdLibFiles = files.some((file) => - STD_LIB_SENTINELS.includes(file.toLowerCase()) - ); - - if (hasStdLibFiles) { - if (logger.getVerbosity() === 'verbose') { - logger.detail( - `Standard library files already exist in ${includesDirName}, skipping download` - ); - logger.detail( - 'If you want to update the standard library, please remove existing .inc files first' - ); - } - return; - } - - const git = simpleGit(); - await git.clone( - 'https://github.com/openmultiplayer/omp-stdlib.git', - includesDir, - ['--depth=1'] - ); - - // Clean up unnecessary files - const unnecessaryFilesAndDirs = [ - 'README.md', - '.git', - 'documentation', - '.editorconfig', - '.gitattributes', - 'LICENSE.md', - 'pawndoc.xsl', - ]; - - for (const item of unnecessaryFilesAndDirs) { - const itemPath = path.resolve(includesDir, item); - - if (!itemPath.startsWith(includesDir)) { - logger.warn(`Skipping invalid path: ${itemPath}`); - continue; - } - - if (fs.existsSync(itemPath)) { - try { - if (fs.statSync(itemPath).isDirectory()) { - fs.rmSync(itemPath, { recursive: true, force: true }); - if (logger.getVerbosity() === 'verbose') { - logger.detail(`Removed directory: ${item}`); - } - } else { - fs.unlinkSync(itemPath); - if (logger.getVerbosity() === 'verbose') { - logger.detail(`Removed file: ${item}`); - } - } - } catch (error) { - logger.warn( - `Failed to remove ${item}: ${error instanceof Error ? error.message : 'unknown error'}` - ); - } - } - } - - if (logger.getVerbosity() === 'verbose') { - logger.detail( - `Downloaded and extracted open.mp standard library to ${includesDirName}` - ); - } - } catch (error) { - logger.error( - `Failed to download open.mp standard library: ${error instanceof Error ? error.message : 'unknown error'}` - ); - throw error; - } -} + const { includesDir, includesDirName } = resolveStdLibTargetDir(); + + ensureDirExists(includesDir, includesDirName); + + if (hasExistingStdLib(includesDir, includesDirName)) { + return; + } + + if (customUrl) { + await downloadStdLibArchive(customUrl, includesDir, includesDirName); + return; + } + + await cloneStdLibRepo(includesDir); + pruneStdLibArtifacts(includesDir); + if (logger.getVerbosity() === 'verbose') { + logger.detail(`Downloaded and extracted open.mp standard library to ${includesDirName}`); + } +} export async function getLatestCompilerVersion(): Promise { return new Promise((resolve, reject) => { From 32a415ffd5e004abf3529a206b8530cccced15dd Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 10:59:55 +0100 Subject: [PATCH 16/20] feat: add preset options to init command for enhanced project configuration --- src/commands/init/index.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 5353152..7be7883 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -1,6 +1,5 @@ import { Command } from 'commander'; -import { setupInitCommand } from './setup'; -import { showBanner } from '../../utils/banner'; +import { setupInitCommand } from './setup'; /** * Register the `init` command that bootstraps a new project via the setup wizard. @@ -13,15 +12,15 @@ export default function (program: Command): void { .description('Initialize a new open.mp project') .option('-n, --name ', 'project name') .option('-d, --description ', 'project description') - .option('-a, --author ', 'project author') - .option('-q, --quiet', 'minimize console output (show only progress bars)') - .option('--skip-compiler', 'skip compiler setup and use default settings') - .option('--legacy-samp', 'initialize with SA-MP legacy support') - .action(async (options) => { - // Now show banner and run setup - showBanner(options.quiet); - - // run the init setup process - await setupInitCommand(options); - }); -} + .option('-a, --author ', 'project author') + .option('-q, --quiet', 'minimize console output (show only progress bars)') + .option('--skip-compiler', 'skip compiler setup and use default settings') + .option('--legacy-samp', 'initialize with SA-MP legacy support') + .option('--preset ', 'use preset file or preset name from ~/.tapi/workflows') + .option('--accept-preset', 'apply preset values without prompts') + .option('--non-interactive', 'skip all prompts; requires preset or CLI values') + .action(async (options) => { + // run the init setup process + await setupInitCommand(options); + }); +} From 50cd89c86cad3bcff1754ecc660a3079bff7d6f4 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 11:00:04 +0100 Subject: [PATCH 17/20] feat: enhance project initialization prompts with default values and non-interactive mode support --- src/commands/init/prompts.ts | 724 ++++++++++++++++++++--------------- 1 file changed, 410 insertions(+), 314 deletions(-) diff --git a/src/commands/init/prompts.ts b/src/commands/init/prompts.ts index ebcf1b4..b3d32c0 100644 --- a/src/commands/init/prompts.ts +++ b/src/commands/init/prompts.ts @@ -1,8 +1,8 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { input, select, confirm } from '@inquirer/prompts'; -import { configManager } from '../../utils/config'; -import { logger } from '../../utils/logger'; +import * as fs from 'fs'; +import * as path from 'path'; +import { input, select, confirm } from '@inquirer/prompts'; +import { configManager } from '../../utils/config'; +import { logger } from '../../utils/logger'; import { getLatestCompilerVersion, hasExistingStandardLibrary } from './compiler'; import { CommandOptions, InitialAnswers, CompilerAnswers } from './types'; @@ -10,95 +10,303 @@ import { CommandOptions, InitialAnswers, CompilerAnswers } from './types'; * Prompt for base project metadata when running `tapi init`. */ export async function promptForInitialOptions( - options: CommandOptions + options: CommandOptions, + defaults: Partial = {}, + nonInteractive = false ): Promise { - const isLegacySamp = options.legacySamp; - const defaultAuthor = configManager.getDefaultAuthor(); - const name = - options.name || - (await input({ - message: 'Project name:', - default: path.basename(process.cwd()), - })); - const description = - options.description || - (await input({ - message: 'Project description:', - })); - const author = - options.author || - (await input({ - message: 'Author:', - default: defaultAuthor || '', - })); - const projectType = (await select({ - message: 'Project type:', - choices: [ - { value: 'gamemode', name: 'gamemode' }, - { value: 'filterscript', name: 'filterscript' }, - { value: 'library', name: 'library' }, - ], - default: 'gamemode', - })) as 'gamemode' | 'filterscript' | 'library'; - const editor = (await select({ - message: 'Which editor are you using?', - choices: [ - { value: 'VS Code', name: 'VS Code' }, - { value: 'Sublime Text', name: 'Sublime Text' }, - { value: 'Other/None', name: 'Other/None' }, - ], - default: configManager.getEditor() || 'VS Code', - })) as 'VS Code' | 'Sublime Text' | 'Other/None'; - const initGit = await confirm({ - message: 'Initialize Git repository?', - default: true, - }); - const downloadServer = await confirm({ - message: `Add ${isLegacySamp ? 'SA-MP' : 'open.mp'} server package?`, - default: true, - }); - return { - name, - description, - author, - projectType, - addStdLib: true, - initGit, - downloadServer, - editor, - }; -} - + const isLegacySamp = options.legacySamp; + const folderName = path.basename(process.cwd()); + const configDefaultAuthor = configManager.getDefaultAuthor(); + + const resolvedName = defaults.name ?? options.name ?? folderName; + const resolvedDescription = defaults.description ?? options.description ?? ''; + const resolvedAuthor = + defaults.author ?? options.author ?? configDefaultAuthor ?? ''; + const resolvedProjectType = (defaults.projectType ?? 'gamemode') as + | 'gamemode' + | 'filterscript' + | 'library'; + const resolvedEditor = ( + defaults.editor ?? configManager.getEditor() ?? 'VS Code' + ) as 'VS Code' | 'Sublime Text' | 'Other/None'; + const resolvedInitGit = defaults.initGit ?? true; + const resolvedDownloadServer = defaults.downloadServer ?? true; + const resolvedAddStdLib = defaults.addStdLib ?? true; + + if (nonInteractive) { + return { + name: resolvedName, + description: resolvedDescription, + author: resolvedAuthor, + projectType: resolvedProjectType, + addStdLib: resolvedAddStdLib, + initGit: resolvedInitGit, + downloadServer: resolvedDownloadServer, + editor: resolvedEditor, + }; + } + + const name = + options.name ?? + defaults.name ?? + (await input({ + message: 'Project name:', + default: resolvedName, + })); + + const description = + options.description ?? + defaults.description ?? + (await input({ + message: 'Project description:', + default: resolvedDescription, + })); + + const author = + options.author ?? + defaults.author ?? + (await input({ + message: 'Author:', + default: resolvedAuthor, + })); + + const projectType = (await select({ + message: 'Project type:', + choices: [ + { value: 'gamemode', name: 'gamemode' }, + { value: 'filterscript', name: 'filterscript' }, + { value: 'library', name: 'library' }, + ], + default: resolvedProjectType, + })) as 'gamemode' | 'filterscript' | 'library'; + + const editor = (await select({ + message: 'Which editor are you using?', + choices: [ + { value: 'VS Code', name: 'VS Code' }, + { value: 'Sublime Text', name: 'Sublime Text' }, + { value: 'Other/None', name: 'Other/None' }, + ], + default: resolvedEditor, + })) as 'VS Code' | 'Sublime Text' | 'Other/None'; + + const initGit = await confirm({ + message: 'Initialize Git repository?', + default: resolvedInitGit, + }); + const downloadServer = await confirm({ + message: `Add ${isLegacySamp ? 'SA-MP' : 'open.mp'} server package?`, + default: resolvedDownloadServer, + }); + return { + name, + description, + author, + projectType, + addStdLib: resolvedAddStdLib, + initGit, + downloadServer, + editor, + }; +} + /** * Collect compiler installation preferences, handling existing installs and version conflicts. */ -export async function promptForCompilerOptions(isLegacySamp: boolean = false): Promise { - // Check if there's already a compiler in the current directory - const hasExistingCompiler = - fs.existsSync(path.join(process.cwd(), 'pawno', 'pawncc.exe')) || - fs.existsSync(path.join(process.cwd(), 'pawno', 'pawncc')) || - fs.existsSync(path.join(process.cwd(), 'qawno', 'pawncc.exe')) || - fs.existsSync(path.join(process.cwd(), 'qawno', 'pawncc')) || - fs.existsSync(path.join(process.cwd(), 'compiler', 'pawncc.exe')) || - fs.existsSync(path.join(process.cwd(), 'compiler', 'pawncc')); - - // Only ask to download compiler if there's already one, otherwise always download - const downloadCompiler = hasExistingCompiler - ? await confirm({ - message: 'Download community pawn compiler? (A compiler already exists)', - default: false, - }) - : true; - +export async function promptForCompilerOptions( + isLegacySamp: boolean = false, + defaults: Partial = {}, + nonInteractive = false +): Promise { + const hasExistingCompiler = + fs.existsSync(path.join(process.cwd(), 'pawno', 'pawncc.exe')) || + fs.existsSync(path.join(process.cwd(), 'pawno', 'pawncc')) || + fs.existsSync(path.join(process.cwd(), 'qawno', 'pawncc.exe')) || + fs.existsSync(path.join(process.cwd(), 'qawno', 'pawncc')) || + fs.existsSync(path.join(process.cwd(), 'compiler', 'pawncc.exe')) || + fs.existsSync(path.join(process.cwd(), 'compiler', 'pawncc')); const stdLibAlreadyPresent = hasExistingStandardLibrary(); - let compilerVersion = 'latest'; - let keepQawno = true; - let installCompilerFolder = false; - let useCompilerFolder = false; - let downloadStdLib = !stdLibAlreadyPresent; - let downgradeQawno = false; + + const defaultDownloadCompiler = () => (hasExistingCompiler ? false : true); + + if (nonInteractive) { + const downloadCompiler = + defaults.downloadCompiler ?? defaultDownloadCompiler(); + return { + downloadCompiler, + compilerVersion: defaults.compilerVersion ?? 'latest', + keepQawno: defaults.keepQawno ?? true, + downgradeQawno: defaults.downgradeQawno ?? false, + installCompilerFolder: defaults.installCompilerFolder ?? false, + useCompilerFolder: defaults.useCompilerFolder ?? false, + downloadStdLib: + defaults.downloadStdLib ?? !stdLibAlreadyPresent, + compilerDownloadUrl: defaults.compilerDownloadUrl, + stdLibDownloadUrl: defaults.stdLibDownloadUrl, + }; + } + + let downloadCompiler: boolean; + if (defaults.downloadCompiler !== undefined) { + downloadCompiler = defaults.downloadCompiler; + } else if (hasExistingCompiler) { + downloadCompiler = await confirm({ + message: 'Download community pawn compiler? (A compiler already exists)', + default: false, + }); + } else { + downloadCompiler = true; + } + + let compilerVersion = defaults.compilerVersion ?? 'latest'; + let keepQawno = defaults.keepQawno ?? true; + let installCompilerFolder = defaults.installCompilerFolder ?? false; + let useCompilerFolder = defaults.useCompilerFolder ?? false; + let downloadStdLib = defaults.downloadStdLib ?? !stdLibAlreadyPresent; + let downgradeQawno = defaults.downgradeQawno ?? false; + const compilerDownloadUrl = defaults.compilerDownloadUrl; + const stdLibDownloadUrl = defaults.stdLibDownloadUrl; if (!downloadCompiler) { + if (defaults.downloadStdLib === undefined) { + if (!stdLibAlreadyPresent) { + downloadStdLib = await confirm({ + message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, + default: true, + }); + } else if (logger.getVerbosity() !== 'quiet') { + logger.hint( + 'Standard library detected. Skipping download β€” remove existing includes if you need a fresh copy.' + ); + downloadStdLib = false; + } + } else if (stdLibAlreadyPresent && defaults.downloadStdLib && logger.getVerbosity() === 'verbose') { + logger.detail('Preset requests standard library download despite existing files'); + } + return { + downloadCompiler, + compilerVersion, + keepQawno, + downgradeQawno, + installCompilerFolder, + useCompilerFolder, + downloadStdLib, + }; + } + + if (defaults.compilerVersion === undefined) { + compilerVersion = await input({ + message: 'Enter the compiler version (or "latest" for the latest version):', + default: compilerVersion, + }); + } + + const qawnoDir = path.join(process.cwd(), 'qawno'); + const hasQawno = fs.existsSync(qawnoDir); + let existingVersion: string | null = null; + let cleanTargetVersion = compilerVersion; + if (compilerVersion === 'latest') { + cleanTargetVersion = await getLatestCompilerVersion(); + } + if (cleanTargetVersion.startsWith('v')) { + cleanTargetVersion = cleanTargetVersion.substring(1); + } + + if (hasQawno) { + existingVersion = await checkExistingCompilerVersion(qawnoDir); + } + + if ( + hasQawno && + existingVersion && + defaults.keepQawno === undefined + ) { + const comparison = compareVersions(cleanTargetVersion, existingVersion); + const isDowngrade = comparison < 0; + + if (isDowngrade) { + logger.warn(`Version conflict detected!`); + logger.warn(` Server package includes: ${existingVersion}`); + logger.warn(` Community compiler version: ${cleanTargetVersion}`); + logger.warn(` Installing community compiler would be a downgrade!`); + + const action = await select({ + message: 'How would you like to handle this version conflict?', + choices: [ + { + value: 'keep_server', + name: `Keep server's compiler (${existingVersion}) - recommended`, + }, + { + value: 'downgrade', + name: `Replace with community compiler (${cleanTargetVersion}) - not recommended`, + }, + { + value: 'both', + name: `Install both (community in compiler/ folder)`, + }, + ], + default: 'keep_server', + }); + + if (action === 'keep_server') { + keepQawno = true; + downgradeQawno = false; + installCompilerFolder = false; + } else if (action === 'downgrade') { + keepQawno = false; + downgradeQawno = true; + installCompilerFolder = false; + } else { + keepQawno = true; + downgradeQawno = false; + installCompilerFolder = true; + useCompilerFolder = true; + } + } else { + keepQawno = await confirm({ + message: `Server has ${existingVersion}, community compiler is ${cleanTargetVersion}. Replace server's compiler?`, + default: false, + }); + if (keepQawno) { + downgradeQawno = false; + } + } + } else if (defaults.keepQawno !== undefined) { + keepQawno = defaults.keepQawno; + if (defaults.downgradeQawno !== undefined) { + downgradeQawno = defaults.downgradeQawno; + } + } + + if ( + defaults.installCompilerFolder === undefined && + (!hasQawno || (keepQawno && !downgradeQawno && !installCompilerFolder)) + ) { + installCompilerFolder = await confirm({ + message: 'Install community compiler in compiler/ folder?', + default: true, + }); + } else if (defaults.installCompilerFolder !== undefined) { + installCompilerFolder = defaults.installCompilerFolder; + } + + if ( + defaults.useCompilerFolder === undefined && + keepQawno && + installCompilerFolder && + hasQawno && + !downgradeQawno + ) { + useCompilerFolder = await confirm({ + message: 'Use compiler/ folder for builds (otherwise use qawno/)?', + default: true, + }); + } else if (defaults.useCompilerFolder !== undefined) { + useCompilerFolder = defaults.useCompilerFolder; + } + + if (defaults.downloadStdLib === undefined) { if (!stdLibAlreadyPresent) { downloadStdLib = await confirm({ message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, @@ -110,232 +318,120 @@ export async function promptForCompilerOptions(isLegacySamp: boolean = false): P ); downloadStdLib = false; } - return { - downloadCompiler, - compilerVersion, - keepQawno, - downgradeQawno, - installCompilerFolder, - useCompilerFolder, - downloadStdLib, - }; - } - - compilerVersion = await input({ - message: 'Enter the compiler version (or "latest" for the latest version):', - default: 'latest', - }); - - const qawnoDir = path.join(process.cwd(), 'qawno'); - const hasQawno = fs.existsSync(qawnoDir); - let existingVersion: string | null = null; - let cleanTargetVersion = compilerVersion; - if (compilerVersion === 'latest') { - cleanTargetVersion = await getLatestCompilerVersion(); - } - if (cleanTargetVersion.startsWith('v')) { - cleanTargetVersion = cleanTargetVersion.substring(1); - } - - if (hasQawno) { - existingVersion = await checkExistingCompilerVersion(qawnoDir); - if ( - existingVersion && - compareVersions(cleanTargetVersion, existingVersion) === 0 - ) { - // Already latest, no need to ask about keeping or downgrading - keepQawno = true; - downgradeQawno = false; - } else if (existingVersion) { - const comparison = compareVersions(cleanTargetVersion, existingVersion); - const isDowngrade = comparison < 0; - - if (isDowngrade) { - logger.warn(`Version conflict detected!`); - logger.warn(` Server package includes: ${existingVersion}`); - logger.warn(` Community compiler version: ${cleanTargetVersion}`); - logger.warn(` Installing community compiler would be a downgrade!`); - - const action = await select({ - message: 'How would you like to handle this version conflict?', - choices: [ - { - value: 'keep_server', - name: `Keep server's compiler (${existingVersion}) - recommended`, - }, - { - value: 'downgrade', - name: `Replace with community compiler (${cleanTargetVersion}) - not recommended`, - }, - { - value: 'both', - name: `Install both (community in compiler/ folder)`, - }, - ], - default: 'keep_server', - }); - - if (action === 'keep_server') { - keepQawno = true; - downgradeQawno = false; - installCompilerFolder = false; - } else if (action === 'downgrade') { - keepQawno = false; - downgradeQawno = true; - installCompilerFolder = false; - } else { - // both - keepQawno = true; - downgradeQawno = false; - installCompilerFolder = true; - useCompilerFolder = true; - } - } else { - // Upgrade scenario - keepQawno = await confirm({ - message: `Server has ${existingVersion}, community compiler is ${cleanTargetVersion}. Replace server's compiler?`, - default: false, - }); - if (keepQawno) { - downgradeQawno = false; - } - } - } - } - - // Only ask about compiler/ folder if not already decided - if (!hasQawno || (keepQawno && !downgradeQawno && !installCompilerFolder)) { - installCompilerFolder = await confirm({ - message: 'Install community compiler in compiler/ folder?', - default: true, - }); - } - - // If both exist and not downgrading, ask which to use - if (keepQawno && installCompilerFolder && hasQawno && !downgradeQawno) { - useCompilerFolder = await confirm({ - message: 'Use compiler/ folder for builds (otherwise use qawno/)?', - default: true, - }); - } - - if (!stdLibAlreadyPresent) { - downloadStdLib = await confirm({ - message: `Download ${isLegacySamp ? 'SA-MP' : 'open.mp'} standard library?`, - default: true, + } else { + if (stdLibAlreadyPresent && defaults.downloadStdLib && logger.getVerbosity() === 'verbose') { + logger.detail( + 'Preset requests standard library download despite existing files' + ); + } + downloadStdLib = defaults.downloadStdLib; + } + + return { + downloadCompiler, + compilerVersion, + keepQawno, + downgradeQawno, + installCompilerFolder, + useCompilerFolder, + downloadStdLib, + compilerDownloadUrl, + stdLibDownloadUrl, + }; +} +async function checkExistingCompilerVersion( + qawnoDir: string +): Promise { + const platform = process.platform; + const exeExtension = platform === 'win32' ? '.exe' : ''; + const compilerPath = path.join(qawnoDir, `pawncc${exeExtension}`); + + if (!fs.existsSync(compilerPath)) { + return null; + } + + const tmp = require('os').tmpdir(); + const pwnTestFile = path.join(tmp, `__pawn_version_check_${Date.now()}.pwn`); + const amxTestFile = path.join( + process.cwd(), + path.basename(pwnTestFile).replace(/\.pwn$/, '.amx') + ); + const fsPromises = fs.promises; + + try { + // Write minimal pawn file to check compiler version + await fsPromises.writeFile(pwnTestFile, 'main() {}'); + + const { spawn } = require('child_process'); + return await new Promise((resolve) => { + const compiler = spawn(compilerPath, [pwnTestFile], { + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 5000, + }); + + let output = ''; + let cleaned = false; + const cleanup = () => { + if (!cleaned) { + cleaned = true; + fsPromises.unlink(pwnTestFile).catch(() => {}); + fsPromises.unlink(amxTestFile).catch(() => {}); + } + }; + + const timeoutId = setTimeout(() => { + compiler.kill(); + resolve(null); + cleanup(); + }, 5000); + + compiler.stdout.on('data', (data: Buffer) => { + output += data.toString(); + }); + compiler.stderr.on('data', (data: Buffer) => { + output += data.toString(); + }); + + compiler.on('close', () => { + clearTimeout(timeoutId); + const versionMatch = output.match(/Pawn compiler\s+([0-9.]+)/i); + resolve(versionMatch ? versionMatch[1] : null); + cleanup(); + }); + + compiler.on('error', () => { + clearTimeout(timeoutId); + resolve(null); + cleanup(); + }); }); - } else if (logger.getVerbosity() !== 'quiet') { - logger.hint( - 'Standard library detected. Skipping download β€” remove existing includes if you need a fresh copy.' - ); - downloadStdLib = false; + } catch (error) { + if (logger.getVerbosity() === 'verbose') { + logger.detail( + `Could not check existing compiler version: ${error instanceof Error ? error.message : 'unknown error'}` + ); + } + try { + await fsPromises.unlink(pwnTestFile); + } catch { + // Ignore cleanup errors + } + return null; } - - return { - downloadCompiler, - compilerVersion, - keepQawno, - downgradeQawno, - installCompilerFolder, - useCompilerFolder, - downloadStdLib, - }; -} - -// Helper function to check existing compiler version -async function checkExistingCompilerVersion( - qawnoDir: string -): Promise { - const platform = process.platform; - const exeExtension = platform === 'win32' ? '.exe' : ''; - const compilerPath = path.join(qawnoDir, `pawncc${exeExtension}`); - - if (!fs.existsSync(compilerPath)) { - return null; - } - - const tmp = require('os').tmpdir(); - const pwnTestFile = path.join(tmp, `__pawn_version_check_${Date.now()}.pwn`); - const amxTestFile = path.join( - process.cwd(), - path.basename(pwnTestFile).replace(/\.pwn$/, '.amx') - ); - const fsPromises = fs.promises; - - try { - // Write minimal pawn file to check compiler version - await fsPromises.writeFile(pwnTestFile, 'main() {}'); - - const { spawn } = require('child_process'); - return await new Promise((resolve) => { - const compiler = spawn(compilerPath, [pwnTestFile], { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 5000, - }); - - let output = ''; - let cleaned = false; - const cleanup = () => { - if (!cleaned) { - cleaned = true; - fsPromises.unlink(pwnTestFile).catch(() => {}); - fsPromises.unlink(amxTestFile).catch(() => {}); - } - }; - - const timeoutId = setTimeout(() => { - compiler.kill(); - resolve(null); - cleanup(); - }, 5000); - - compiler.stdout.on('data', (data: Buffer) => { - output += data.toString(); - }); - compiler.stderr.on('data', (data: Buffer) => { - output += data.toString(); - }); - - compiler.on('close', () => { - clearTimeout(timeoutId); - const versionMatch = output.match(/Pawn compiler\s+([0-9.]+)/i); - resolve(versionMatch ? versionMatch[1] : null); - cleanup(); - }); - - compiler.on('error', () => { - clearTimeout(timeoutId); - resolve(null); - cleanup(); - }); - }); - } catch (error) { - if (logger.getVerbosity() === 'verbose') { - logger.detail( - `Could not check existing compiler version: ${error instanceof Error ? error.message : 'unknown error'}` - ); - } - try { - await fsPromises.unlink(pwnTestFile); - } catch { - // Ignore cleanup errors - } - return null; - } -} - -// Helper function to compare versions (returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2) -function compareVersions(v1: string, v2: string): number { - const parts1 = v1.split('.').map(Number); - const parts2 = v2.split('.').map(Number); - - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const part1 = parts1[i] || 0; - const part2 = parts2[i] || 0; - - if (part1 < part2) return -1; - if (part1 > part2) return 1; - } - - return 0; -} +} + +// Helper function to compare versions (returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2) +function compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const part1 = parts1[i] || 0; + const part2 = parts2[i] || 0; + + if (part1 < part2) return -1; + if (part1 > part2) return 1; + } + + return 0; +} From d6fe95ffe84c8f27c411aef38e985655c0085d91 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 11:00:10 +0100 Subject: [PATCH 18/20] feat: improve setup command with preset loading, enhanced logging, and non-interactive mode handling --- src/commands/init/setup.ts | 164 ++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 37 deletions(-) diff --git a/src/commands/init/setup.ts b/src/commands/init/setup.ts index d005ab7..585c089 100644 --- a/src/commands/init/setup.ts +++ b/src/commands/init/setup.ts @@ -6,13 +6,15 @@ import { CommandOptions, InitialAnswers, CompilerAnswers } from './types'; import { promptForInitialOptions, promptForCompilerOptions } from './prompts'; import { confirm, select, checkbox } from '@inquirer/prompts'; import { setupProjectStructure } from './projectStructure'; -import { setupCompiler } from './compiler'; +import { setupCompiler, hasExistingStandardLibrary } from './compiler'; import { downloadopenmpServer, downloadSampServer, ServerInstallationSummary, } from './serverDownload'; import { cleanupGamemodeFiles, cleanupFiles, createSpinner } from './utils'; +import { loadInitPreset } from '../../utils/preset'; +import { showBanner } from '../../utils/banner'; interface ConflictResolution { proceed: boolean; @@ -310,7 +312,85 @@ function detectExistingProject(): ExistingProject | null { /** * Execute the full initialization workflow: prompts, validation, downloads, and file generation. */ -export async function setupInitCommand(options: CommandOptions): Promise { +export async function setupInitCommand(rawOptions: CommandOptions): Promise { + const options: CommandOptions = { ...rawOptions }; + + if (options.quiet) { + logger.setVerbosity('quiet'); + } else if (options.verbose) { + logger.setVerbosity('verbose'); + } + + let loadedPreset: ReturnType | null = null; + try { + loadedPreset = loadInitPreset(options.preset); + } catch (error) { + logger.error( + `Failed to load init preset: ${error instanceof Error ? error.message : 'unknown error'}` + ); + process.exit(1); + } + + if (loadedPreset?.options?.legacySamp !== undefined && options.legacySamp === undefined) { + options.legacySamp = loadedPreset.options.legacySamp; + } + + if (loadedPreset?.options?.skipCompiler !== undefined && options.skipCompiler === undefined) { + options.skipCompiler = loadedPreset.options.skipCompiler; + } + + if (loadedPreset?.options?.quiet && !options.quiet) { + options.quiet = true; + logger.setVerbosity('quiet'); + } else if ( + !options.quiet && + (options.verbose || loadedPreset?.options?.verbose) && + loadedPreset?.options?.quiet !== true + ) { + options.verbose = true; + logger.setVerbosity('verbose'); + } + + const projectDefaults: Partial = { + ...(loadedPreset?.project ?? {}), + }; + const compilerDefaults: Partial = { + ...(loadedPreset?.compiler ?? {}), + }; + + if (!options.name && projectDefaults.name) { + options.name = projectDefaults.name; + } + if (!options.description && projectDefaults.description) { + options.description = projectDefaults.description; + } + if (!options.author && projectDefaults.author) { + options.author = projectDefaults.author; + } + + const usePresetNonInteractive = Boolean( + options.acceptPreset || options.nonInteractive || loadedPreset?.acceptPreset + ); + + showBanner(logger.getVerbosity() !== 'quiet'); + + const verbosity = logger.getVerbosity(); + const isQuiet = verbosity === 'quiet'; + const isVerbose = verbosity === 'verbose'; + + const shareDetail = (message: string) => { + if (isVerbose) { + logger.detail(message); + } + }; + + if (loadedPreset?.source) { + shareDetail(`Loaded init preset from ${loadedPreset.source}`); + } + if (usePresetNonInteractive) { + shareDetail('Running init in non-interactive mode'); + } + // Check for existing project formats const existingProject = detectExistingProject(); if (existingProject) { @@ -369,6 +449,13 @@ export async function setupInitCommand(options: CommandOptions): Promise { // If there are non-safe files, provide detailed conflict resolution (unless it's a bare server package) if (nonSafeFiles.length > 0 && !(serverPackage.type && !serverPackage.hasContent)) { + if (usePresetNonInteractive) { + logger.error( + 'Conflicting files detected while running with --non-interactive/--accept-preset.' + ); + logger.error('Initialization aborted to avoid prompting for manual input.'); + return; + } const conflictResolution = await handleConflictResolution(nonSafeFiles); if (!conflictResolution.proceed) { logger.warn('Initialization aborted by user.'); @@ -387,7 +474,6 @@ export async function setupInitCommand(options: CommandOptions): Promise { .some((file) => file.endsWith('.pwn') || file.endsWith('.inc')); let detectedName: string | undefined; - let detectedInitGit = false; const _detectedProjectType: 'gamemode' | 'filterscript' | 'library' = 'gamemode'; // Suggest project name based on server package type if no other name detected @@ -482,14 +568,12 @@ export async function setupInitCommand(options: CommandOptions): Promise { if (mainPwn) { detectedName = path.basename(mainPwn, '.pwn'); } - // Detect .git - detectedInitGit = fs.existsSync(path.join(process.cwd(), '.git')); + const gitExists = fs.existsSync(path.join(process.cwd(), '.git')); + if (gitExists && projectDefaults.initGit === undefined) { + projectDefaults.initGit = false; + } } - const verbosity = logger.getVerbosity(); - const isQuiet = verbosity === 'quiet'; - const isVerbose = verbosity === 'verbose'; - const announceStep = (step: number, label: string) => { if (isQuiet) return; logger.working(`Step ${step}/5: ${label}`); @@ -500,12 +584,6 @@ export async function setupInitCommand(options: CommandOptions): Promise { logger.success(message); }; - const shareDetail = (message: string) => { - if (isVerbose) { - logger.detail(message); - } - }; - try { // Auto-detect server type from package, or use command line option const isLegacySamp = serverPackage.type === 'samp' ? true : @@ -528,14 +606,18 @@ export async function setupInitCommand(options: CommandOptions): Promise { // Step 1: Project Configuration announceStep(1, 'Project configuration'); - const initialAnswers = await promptForInitialOptions({ - ...options, - name: detectedName || options.name, - initGit: detectedInitGit, - }); + const promptOptions: CommandOptions = { ...options }; + if (detectedName && !promptOptions.name) { + promptOptions.name = detectedName; + } + const initialAnswers = await promptForInitialOptions( + promptOptions, + projectDefaults, + usePresetNonInteractive + ); completeStep('Configuration saved'); - - // Step 2: Directory Structure Setup + + // Step 2: Directory Structure Setup announceStep(2, 'Preparing project files'); await setupProjectStructure(initialAnswers, isLegacySamp); let serverInstallSummary: ServerInstallationSummary | undefined; @@ -583,29 +665,37 @@ export async function setupInitCommand(options: CommandOptions): Promise { if (options.skipCompiler) { shareDetail('Skipping compiler setup (--skip-compiler)'); + const stdLibPresent = hasExistingStandardLibrary(); compilerAnswers = { downloadCompiler: false, - compilerVersion: 'latest', - keepQawno: true, - downgradeQawno: false, - installCompilerFolder: false, - useCompilerFolder: false, - downloadStdLib: true, + compilerVersion: compilerDefaults.compilerVersion ?? 'latest', + keepQawno: compilerDefaults.keepQawno ?? true, + downgradeQawno: compilerDefaults.downgradeQawno ?? false, + installCompilerFolder: compilerDefaults.installCompilerFolder ?? false, + useCompilerFolder: compilerDefaults.useCompilerFolder ?? false, + downloadStdLib: + compilerDefaults.downloadStdLib ?? !stdLibPresent, }; } else { - compilerAnswers = await promptForCompilerOptions(isLegacySamp).catch((error) => { - if (error.message === 'User force closed the prompt with 0') { + compilerAnswers = await promptForCompilerOptions( + isLegacySamp, + compilerDefaults, + usePresetNonInteractive + ).catch((error) => { + if (error instanceof Error && error.message === 'User force closed the prompt with 0') { logger.warn( 'Compiler setup was interrupted. Using default settings.' ); + const stdLibPresent = hasExistingStandardLibrary(); return { - downloadCompiler: false, - compilerVersion: 'latest', - keepQawno: true, - downgradeQawno: false, - installCompilerFolder: false, - useCompilerFolder: false, - downloadStdLib: true, + downloadCompiler: compilerDefaults.downloadCompiler ?? false, + compilerVersion: compilerDefaults.compilerVersion ?? 'latest', + keepQawno: compilerDefaults.keepQawno ?? true, + downgradeQawno: compilerDefaults.downgradeQawno ?? false, + installCompilerFolder: compilerDefaults.installCompilerFolder ?? false, + useCompilerFolder: compilerDefaults.useCompilerFolder ?? false, + downloadStdLib: + compilerDefaults.downloadStdLib ?? !stdLibPresent, }; } throw error; From de755eecc06371c56f202c07bcc596592601204d Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 11:00:15 +0100 Subject: [PATCH 19/20] feat: extend command options with preset and compiler download URL support --- src/commands/init/types.ts | 41 +++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/commands/init/types.ts b/src/commands/init/types.ts index af7199e..6dd3e83 100644 --- a/src/commands/init/types.ts +++ b/src/commands/init/types.ts @@ -2,16 +2,19 @@ * CLI flags accepted by `tapi init`. */ export interface CommandOptions { - name?: string; - description?: string; - author?: string; - quiet?: boolean; - verbose?: boolean; - initGit?: boolean; - logToFile?: boolean | string; - skipCompiler?: boolean; - legacySamp?: boolean; -} + name?: string; + description?: string; + author?: string; + quiet?: boolean; + verbose?: boolean; + initGit?: boolean; + logToFile?: boolean | string; + skipCompiler?: boolean; + legacySamp?: boolean; + preset?: string; + acceptPreset?: boolean; + nonInteractive?: boolean; +} /** * Answers gathered from the initial project prompts. @@ -31,14 +34,16 @@ export interface InitialAnswers { * Responses collected when configuring the compiler installation. */ export interface CompilerAnswers { - downloadCompiler: boolean; - compilerVersion: string; - keepQawno?: boolean; - downgradeQawno?: boolean; - installCompilerFolder?: boolean; - useCompilerFolder?: boolean; - downloadStdLib: boolean; -} + downloadCompiler: boolean; + compilerVersion: string; + keepQawno?: boolean; + downgradeQawno?: boolean; + installCompilerFolder?: boolean; + useCompilerFolder?: boolean; + downloadStdLib: boolean; + compilerDownloadUrl?: string; + stdLibDownloadUrl?: string; +} /** * Minimal process snapshot used during setup-run operations. From 7bcf8e8f6cedde2b838c613b002a1dd65be2dc64 Mon Sep 17 00:00:00 2001 From: itsneufox Date: Sat, 18 Oct 2025 11:00:21 +0100 Subject: [PATCH 20/20] feat: implement preset loading functionality for project initialization --- src/utils/preset.ts | 429 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 src/utils/preset.ts diff --git a/src/utils/preset.ts b/src/utils/preset.ts new file mode 100644 index 0000000..8fac249 --- /dev/null +++ b/src/utils/preset.ts @@ -0,0 +1,429 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { spawnSync } from 'child_process'; +import { InitialAnswers, CompilerAnswers } from '../commands/init/types'; + +interface PresetOptions { + legacySamp?: boolean; + skipCompiler?: boolean; + quiet?: boolean; + verbose?: boolean; + nonInteractive?: boolean; +} + +export interface LoadedInitPreset { + project?: Partial; + compiler?: Partial; + options?: PresetOptions; + acceptPreset?: boolean; + source: string; +} + +type RawPreset = { + project?: unknown; + compiler?: unknown; + options?: unknown; + acceptPreset?: unknown; +}; + +type RawPresetObject = Record; + +export function loadInitPreset(requested?: string): LoadedInitPreset | null { + const candidatePaths = resolvePresetCandidates(requested); + + for (const presetPath of candidatePaths) { + if (!presetPath) continue; + if (!fs.existsSync(presetPath) || !fs.statSync(presetPath).isFile()) { + continue; + } + + const rawContent = fs.readFileSync(presetPath, 'utf8'); + if (!rawContent.trim()) { + throw new Error(`Preset file "${presetPath}" is empty`); + } + + const parsed = parsePreset(rawContent, presetPath); + const context = buildPlaceholderContext(parsed); + const resolved = resolvePlaceholders(parsed, context); + + const project = normalizeProject(resolved.project); + const compiler = normalizeCompiler(resolved.compiler); + const options = normalizeOptions(resolved.options); + + const acceptPreset = + typeof resolved.acceptPreset === 'boolean' + ? resolved.acceptPreset + : options?.nonInteractive; + + if (options && Object.prototype.hasOwnProperty.call(options, 'nonInteractive')) { + delete options.nonInteractive; + } + + return { + project: Object.keys(project).length ? project : undefined, + compiler: Object.keys(compiler).length ? compiler : undefined, + options: options && Object.keys(options).length ? options : undefined, + acceptPreset, + source: presetPath, + }; + } + + return null; +} + +function resolvePresetCandidates(requested?: string): string[] { + const candidates: string[] = []; + const command = 'init'; + + const projectWorkflows = path.join(process.cwd(), '.tapi', 'workflows'); + const userWorkflows = path.join(os.homedir(), '.tapi', 'workflows'); + + const addCommandFiles = (baseDir: string, name?: string) => { + const suffix = name ? `-${name}` : ''; + candidates.push( + path.join(baseDir, `${command}${suffix}.yml`), + path.join(baseDir, `${command}${suffix}.yaml`), + path.join(baseDir, `${command}${suffix}.json`) + ); + if (name) { + candidates.push( + path.join(baseDir, command, `${name}.yml`), + path.join(baseDir, command, `${name}.yaml`), + path.join(baseDir, command, `${name}.json`) + ); + } else { + candidates.push( + path.join(baseDir, command, 'default.yml'), + path.join(baseDir, command, 'default.yaml'), + path.join(baseDir, command, 'default.json') + ); + } + }; + + if (requested) { + const absolute = path.isAbsolute(requested) + ? requested + : path.resolve(process.cwd(), requested); + + candidates.push(absolute); + addCommandFiles(projectWorkflows, requested); + addCommandFiles(userWorkflows, requested); + return candidates; + } + + addCommandFiles(projectWorkflows); + addCommandFiles(userWorkflows); + + return candidates; +} + +function parsePreset(contents: string, filePath: string): RawPreset { + const trimmed = contents.trim(); + const ext = path.extname(filePath).toLowerCase(); + + if (ext === '.json' || trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + return JSON.parse(trimmed) as RawPreset; + } catch (error) { + throw new Error( + `Failed to parse JSON preset "${filePath}": ${ + error instanceof Error ? error.message : 'unknown error' + }` + ); + } + } + + try { + return parseSimpleYaml(contents); + } catch (error) { + throw new Error( + `Failed to parse YAML preset "${filePath}": ${ + error instanceof Error ? error.message : 'unknown error' + }` + ); + } +} + +function parseSimpleYaml(text: string): RawPreset { + const sanitized = text.replace(/\t/g, ' '); + const lines = sanitized.split(/\r?\n/); + const [result] = parseYamlBlock(lines, 0, 0); + return result as RawPreset; +} + +function parseYamlBlock( + lines: string[], + startIndex: number, + indent: number +): [RawPresetObject, number] { + const result: RawPresetObject = {}; + let index = startIndex; + + while (index < lines.length) { + let line = lines[index]; + if (!line.trim() || line.trimStart().startsWith('#')) { + index += 1; + continue; + } + + const currentIndent = line.match(/^ */)?.[0].length ?? 0; + if (currentIndent < indent) { + break; + } + if (currentIndent > indent) { + throw new Error(`Invalid indentation at line ${index + 1}`); + } + + line = line.slice(indent); + if (line.startsWith('-')) { + throw new Error('Arrays are not supported in init presets'); + } + + const separatorIndex = line.indexOf(':'); + if (separatorIndex === -1) { + throw new Error(`Invalid preset entry at line ${index + 1}`); + } + + const key = line.slice(0, separatorIndex).trim(); + let remainder = line.slice(separatorIndex + 1); + const commentIndex = remainder.indexOf('#'); + if (commentIndex !== -1) { + const before = remainder.slice(0, commentIndex); + if (!before || /\s$/.test(before)) { + remainder = before; + } + } + + const valueText = remainder.trim(); + if (!valueText) { + const [child, nextIndex] = parseYamlBlock(lines, index + 1, indent + 2); + result[key] = child; + index = nextIndex; + } else { + result[key] = parseScalar(valueText); + index += 1; + } + } + + return [result, index]; +} + +function parseScalar(value: string): unknown { + const lower = value.toLowerCase(); + if (lower === 'true') return true; + if (lower === 'false') return false; + if (lower === 'null') return null; + if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value); + + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith('\'') && value.endsWith('\'')) + ) { + return value.slice(1, -1); + } + + return value; +} + +function buildPlaceholderContext(raw: RawPreset): Record { + const now = new Date(); + const cwd = process.cwd(); + const folderName = path.basename(cwd); + const project = (raw.project ?? {}) as RawPresetObject; + + return { + folder: folderName, + slug: createSlug(folderName), + cwd, + user: safeUserName(), + hostname: os.hostname(), + date: now.toISOString(), + timestamp: String(now.getTime()), + year: String(now.getFullYear()), + gitBranch: detectGitBranch(), + projectType: + typeof project.projectType === 'string' ? (project.projectType as string) : '', + name: typeof project.name === 'string' ? (project.name as string) : '', + editor: typeof project.editor === 'string' ? (project.editor as string) : '', + }; +} + +function createSlug(value: string): string { + const base = value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/--+/g, '-'); + return base || 'project'; +} + +function detectGitBranch(): string { + try { + const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { + cwd: process.cwd(), + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf8', + }); + + if (result.status === 0) { + const branch = (result.stdout || '').trim(); + return branch === 'HEAD' ? '' : branch; + } + } catch { + // Ignore git errors + } + + return ''; +} + +function safeUserName(): string { + try { + const info = os.userInfo(); + if (info && info.username) { + return info.username; + } + } catch { + // Ignore lookup errors + } + + return process.env.USER || process.env.USERNAME || ''; +} + +function resolvePlaceholders(value: T, context: Record): T { + if (typeof value === 'string') { + return value.replace(/\$\{([^}]+)\}/g, (_, token: string) => { + if (token.startsWith('env:')) { + return process.env[token.slice(4)] ?? ''; + } + return context[token] ?? ''; + }) as unknown as T; + } + + if (Array.isArray(value)) { + return value.map((item) => resolvePlaceholders(item, context)) as unknown as T; + } + + if (value && typeof value === 'object') { + const result: Record = {}; + for (const [key, inner] of Object.entries(value as RawPresetObject)) { + result[key] = resolvePlaceholders(inner, context); + } + return result as unknown as T; + } + + return value; +} + +function normalizeProject(raw: unknown): Partial { + if (!raw || typeof raw !== 'object') { + return {}; + } + + const candidate = raw as RawPresetObject; + const result: Partial = {}; + + if (typeof candidate.name === 'string') { + result.name = candidate.name as string; + } + if (typeof candidate.description === 'string') { + result.description = candidate.description as string; + } + if (typeof candidate.author === 'string') { + result.author = candidate.author as string; + } + + if ( + typeof candidate.projectType === 'string' && + ['gamemode', 'filterscript', 'library'].includes(candidate.projectType as string) + ) { + result.projectType = candidate.projectType as InitialAnswers['projectType']; + } + + if ( + typeof candidate.editor === 'string' && + ['VS Code', 'Sublime Text', 'Other/None'].includes(candidate.editor as string) + ) { + result.editor = candidate.editor as InitialAnswers['editor']; + } + + if (typeof candidate.initGit === 'boolean') { + result.initGit = candidate.initGit as boolean; + } + + if (typeof candidate.downloadServer === 'boolean') { + result.downloadServer = candidate.downloadServer as boolean; + } + + if (typeof candidate.addStdLib === 'boolean') { + result.addStdLib = candidate.addStdLib as boolean; + } + + return result; +} + +function normalizeCompiler(raw: unknown): Partial { + if (!raw || typeof raw !== 'object') { + return {}; + } + + const candidate = raw as RawPresetObject; + const result: Partial = {}; + + if (typeof candidate.downloadCompiler === 'boolean') { + result.downloadCompiler = candidate.downloadCompiler as boolean; + } + if (typeof candidate.compilerVersion === 'string') { + result.compilerVersion = candidate.compilerVersion as string; + } + if (typeof candidate.keepQawno === 'boolean') { + result.keepQawno = candidate.keepQawno as boolean; + } + if (typeof candidate.downgradeQawno === 'boolean') { + result.downgradeQawno = candidate.downgradeQawno as boolean; + } + if (typeof candidate.installCompilerFolder === 'boolean') { + result.installCompilerFolder = candidate.installCompilerFolder as boolean; + } + if (typeof candidate.useCompilerFolder === 'boolean') { + result.useCompilerFolder = candidate.useCompilerFolder as boolean; + } + if (typeof candidate.downloadStdLib === 'boolean') { + result.downloadStdLib = candidate.downloadStdLib as boolean; + } + if (typeof candidate.compilerDownloadUrl === 'string') { + result.compilerDownloadUrl = candidate.compilerDownloadUrl as string; + } + if (typeof candidate.stdLibDownloadUrl === 'string') { + result.stdLibDownloadUrl = candidate.stdLibDownloadUrl as string; + } + + return result; +} + +function normalizeOptions(raw: unknown): PresetOptions | undefined { + if (!raw || typeof raw !== 'object') { + return undefined; + } + + const candidate = raw as RawPresetObject; + const result: PresetOptions = {}; + + if (typeof candidate.legacySamp === 'boolean') { + result.legacySamp = candidate.legacySamp as boolean; + } + if (typeof candidate.skipCompiler === 'boolean') { + result.skipCompiler = candidate.skipCompiler as boolean; + } + if (typeof candidate.quiet === 'boolean') { + result.quiet = candidate.quiet as boolean; + } + if (typeof candidate.verbose === 'boolean') { + result.verbose = candidate.verbose as boolean; + } + if (typeof candidate.nonInteractive === 'boolean') { + result.nonInteractive = candidate.nonInteractive as boolean; + } + + return result; +}