diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..daef242
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,69 @@
+name: Coverage
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ coverage:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: 9
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: |
+ pnpm install --frozen-lockfile || pnpm install --no-frozen-lockfile
+
+ - name: Run tests with coverage
+ run: pnpm test:coverage
+
+ - name: Generate coverage badge
+ run: node scripts/generate-coverage-badge.js
+
+ - name: Coverage Badge
+ uses: tj-actions/coverage-badge-js@v1
+ if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
+ with:
+ coverage-file: './coverage/coverage-summary.json'
+
+ - name: Verify Changed files
+ uses: tj-actions/verify-changed-files@v12
+ id: verify-changed-files
+ with:
+ files: coverage-badge.json
+
+ - name: Create Coverage Badge
+ if: steps.verify-changed-files.outputs.files_changed == 'true'
+ uses: peter-evans/create-pull-request@v5
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ commit-message: "docs: update coverage badge"
+ title: "๐ Update Coverage Badge"
+ body: |
+ ## Coverage Report Update
+
+ This PR updates the coverage badge based on the latest test results.
+
+ **Coverage Summary:**
+ - Statements: ${{ env.COVERAGE_STATEMENTS }}%
+ - Branches: ${{ env.COVERAGE_BRANCHES }}%
+ - Functions: ${{ env.COVERAGE_FUNCTIONS }}%
+ - Lines: ${{ env.COVERAGE_LINES }}%
+
+ Auto-generated by GitHub Actions.
+ branch: update-coverage-badge
diff --git a/.gitignore b/.gitignore
index c453cb4..f60ee75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,11 @@ node_modules
.DS_Store
.vscode
keep/*
-!keep/README.md
\ No newline at end of file
+!keep/README.md
+
+# Coverage reports
+coverage/
+*.lcov
+
+# Generated files
+coverage-badge.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 4fb3fc8..a4e9b0f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# create commit

+
+
create commit in human way.
@@ -20,6 +22,28 @@ bun install -g OwlTing/cz
> Add `export PATH="$(yarn global bin):$PATH"` to your `~./zshrc` if you installed it by yarn global
+## Development
+
+### Running Tests
+```shell
+# Run tests in watch mode
+pnpm test
+
+# Run tests once
+pnpm test:run
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Generate coverage badge
+pnpm coverage:badge
+```
+
+### Pre-commit Hooks
+This project uses [lefthook](https://github.com/evilmartians/lefthook) for pre-commit hooks that automatically run:
+- Unit tests (`pnpm test:run`)
+- TypeScript compilation check (`pnpm build`)
+
## Update version
```shell
# npm
@@ -179,7 +203,7 @@ input Jira issue ID
name: 'storybook',
emoji: '๐',
description: 'New storybook',
- value: 'story'
+ value: 'storybook'
},
{
name: 'revert',
@@ -193,5 +217,5 @@ input Jira issue ID
## Todo
- [x] adapt for other projects prefix
-- [ ] unit test
+- [x] unit test
- [ ] CLI
diff --git a/dist/index.cjs b/dist/index.cjs
index 88f5762..93e483e 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -88,7 +88,7 @@ const commitTypes = [
name: "storybook",
emoji: "\u{1F4DA}",
description: "New storybook",
- value: "story"
+ value: "storybook"
},
{
name: "revert",
@@ -128,100 +128,147 @@ const projects = [
const __dirname$3 = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
const rootPath$1 = node_path.resolve(__dirname$3, "../");
-let defaultProjectValue = "";
-try {
- const filePath = node_path.resolve(rootPath$1, "keep/cz_config.json");
- const config = fs__default.readFileSync(filePath);
- defaultProjectValue = JSON.parse(config).defaultProject;
-} catch (e) {
- console.log(picocolors__default.yellow(picocolors__default.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. ")));
- defaultProjectValue = "";
-}
-const typesList = commitTypes.map((type) => ({
- title: type.name,
- description: `${type.emoji} ${type.description}`,
- value: type.value,
- emoji: type.emoji
-}));
-const step_type$1 = {
- type: "autocomplete",
- name: "commit_type",
- message: "Pick a commit type.",
- choices: typesList,
- fallback: "No matched type."
-};
-const step_message = {
- type: "text",
- name: "commit_message",
- message: (prev) => {
- const target = typesList.find((type) => type.value === prev);
- return `${target.emoji} ${target.title}`;
- },
- validate: (value) => {
- if (!value) {
- return "Commit message is required.";
- }
- return true;
+const loadProjectConfig = (filePath) => {
+ try {
+ const config = fs__default.readFileSync(filePath, "utf8");
+ return JSON.parse(config);
+ } catch (e) {
+ return null;
}
};
-const step_description = {
- type: "text",
- name: "commit_description",
- message: "Commit description (optional)",
- initial: "",
- validate: (value) => {
- if (value.length > 100) {
- return "Description is too long.";
- }
- return true;
- }
+const getDefaultProjectValue = (config) => {
+ return config?.defaultProject || "";
};
-const step_is_jira = {
- type: "confirm",
- name: "is_jira",
- message: "Tag Jira issue ?",
- initial: false
+const buildCommitTypesList = () => {
+ return commitTypes.map((type) => ({
+ title: type.name,
+ description: `${type.emoji} ${type.description}`,
+ value: type.value,
+ emoji: type.emoji
+ }));
};
-const projectsList$1 = projects.map((project) => ({
- title: project.name,
- description: `[${project.prefix}-13845] title`,
- value: project.value
-}));
-const defaultProject = projectsList$1.find((project) => project.value === defaultProjectValue);
-const step_is_default_project = {
- type: (prev) => prev ? "confirm" : null,
- name: "is_default_project",
- message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
- initial: true
+const buildProjectsList = () => {
+ return projects.map((project) => ({
+ title: project.name,
+ description: `[${project.prefix}-13845] title`,
+ value: project.value
+ }));
};
-const step_project_type = {
- type: (prev, { is_jira }) => {
- return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null;
- },
- name: "project_type",
- message: "Pick a project type.",
- choices: projectsList$1,
- initial: "owlpay",
- fallback: "No matched project."
+const findCommitType = (commitTypeValue) => {
+ return commitTypes.find((type) => type.value === commitTypeValue);
};
-const step_jira_id = {
- type: (prev) => prev ? "number" : null,
- name: "jira_id",
- message: "Jira issue id",
- onRender() {
- this.msg = picocolors__default.bgCyan(picocolors__default.white(" Jira issue ID "));
- },
- validate: (value) => {
- if (!value) {
- return "Jira issue ID is required.";
- }
- return true;
+const findProject = (projectValue) => {
+ return projects.find((project) => project.value === projectValue);
+};
+const buildCommitTitle = (commitType, message) => {
+ const type = findCommitType(commitType);
+ if (!type) {
+ throw new Error(`Invalid commit type: ${commitType}`);
}
+ return `${type.emoji} ${commitType}: ${message}`;
};
-const cli = async () => {
- let isCanceled = false;
- const order = [
- step_type$1,
+const buildFinalCommitMessage = (commitTitle, response, defaultProjectValue) => {
+ const { is_jira, is_default_project, project_type, jira_id } = response;
+ if (!is_jira) {
+ return commitTitle;
+ }
+ const typeResponse = is_default_project ? defaultProjectValue : project_type;
+ const projectType = findProject(typeResponse);
+ if (!projectType) {
+ throw new Error(`Invalid project type: ${typeResponse}`);
+ }
+ if (!jira_id) {
+ throw new Error("Jira ID is required when using Jira integration");
+ }
+ return `[${projectType.prefix}-${jira_id}] ${commitTitle}`;
+};
+const buildGitCommands = (commitMessage, description) => {
+ if (description) {
+ return ["commit", "-m", commitMessage, "-m", description];
+ }
+ return ["commit", "-m", commitMessage];
+};
+const parseCommitResult = (stdout) => {
+ const branchHashName = stdout.match(/\[(.*?)\]/)?.pop();
+ if (!branchHashName) {
+ throw new Error("Could not parse commit result");
+ }
+ const [branchName, branchHash] = branchHashName.split(" ");
+ return { branch: branchName, hash: branchHash };
+};
+const buildPromptSteps = (defaultProjectValue, projectsList, typesList) => {
+ const defaultProject = projectsList.find((project) => project.value === defaultProjectValue);
+ const step_type = {
+ type: "autocomplete",
+ name: "commit_type",
+ message: "Pick a commit type.",
+ choices: typesList,
+ fallback: "No matched type."
+ };
+ const step_message = {
+ type: "text",
+ name: "commit_message",
+ message: (prev) => {
+ const target = typesList.find((type) => type.value === prev);
+ return `${target.emoji} ${target.title}`;
+ },
+ validate: (value) => {
+ if (!value) {
+ return "Commit message is required.";
+ }
+ return true;
+ }
+ };
+ const step_description = {
+ type: "text",
+ name: "commit_description",
+ message: "Commit description (optional)",
+ initial: "",
+ validate: (value) => {
+ if (value.length > 100) {
+ return "Description is too long.";
+ }
+ return true;
+ }
+ };
+ const step_is_jira = {
+ type: "confirm",
+ name: "is_jira",
+ message: "Tag Jira issue ?",
+ initial: false
+ };
+ const step_is_default_project = {
+ type: (prev) => prev ? "confirm" : null,
+ name: "is_default_project",
+ message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
+ initial: true
+ };
+ const step_project_type = {
+ type: (prev, { is_jira }) => {
+ return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null;
+ },
+ name: "project_type",
+ message: "Pick a project type.",
+ choices: projectsList,
+ initial: "owlpay",
+ fallback: "No matched project."
+ };
+ const step_jira_id = {
+ type: (prev) => prev ? "number" : null,
+ name: "jira_id",
+ message: "Jira issue id",
+ onRender() {
+ this.msg = picocolors__default.bgCyan(picocolors__default.white(" Jira issue ID "));
+ },
+ validate: (value) => {
+ if (!value) {
+ return "Jira issue ID is required.";
+ }
+ return true;
+ }
+ };
+ return [
+ step_type,
step_message,
step_description,
step_is_jira,
@@ -229,7 +276,52 @@ const cli = async () => {
step_project_type,
step_jira_id
].filter(Boolean);
- const response = await prompts__default(order, {
+};
+const showConfigMissingWarning = () => {
+ console.log(picocolors__default.yellow(picocolors__default.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. ")));
+};
+const showCancelMessage = () => {
+ console.log(picocolors__default.magenta(" commit abort. "));
+};
+const showCommitResult = (result) => {
+ console.log("-----------------------------------------------------------");
+ console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Title "))} ${picocolors__default.green(result.title)}`);
+ if (result.description) {
+ console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Description "))} ${picocolors__default.green(result.description)}`);
+ }
+ if (result.hash && result.branch) {
+ console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Commit hash "))} ${picocolors__default.bold(picocolors__default.cyan(` ${result.hash} `))} (${picocolors__default.italic(picocolors__default.green(result.branch))})`);
+ }
+};
+const showGitOutput = (stdout, stderr) => {
+ console.log("-----------------------------------------------------------");
+ console.log(picocolors__default.dim(stdout));
+ if (stderr && stderr !== "") {
+ console.log("-----------------------------------------------------------");
+ console.log(picocolors__default.dim(stderr));
+ }
+ console.log("-----------------------------------------------------------");
+};
+const showGitError = (error) => {
+ console.log(picocolors__default.red(error.stderr));
+ if (error.exitCode === 1) {
+ console.log(picocolors__default.bgRed(" No changes added to commit. "));
+ } else {
+ console.error(error);
+ }
+};
+const cli = async () => {
+ const configPath = node_path.resolve(rootPath$1, "keep/cz_config.json");
+ const config = loadProjectConfig(configPath);
+ const defaultProjectValue = getDefaultProjectValue(config);
+ if (!config) {
+ showConfigMissingWarning();
+ }
+ const typesList = buildCommitTypesList();
+ const projectsList = buildProjectsList();
+ const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList);
+ let isCanceled = false;
+ const response = await prompts__default(steps, {
onSubmit: (prompt, answers) => {
if (answers === void 0) {
isCanceled = true;
@@ -242,38 +334,26 @@ const cli = async () => {
}
});
if (isCanceled) {
- console.log(picocolors__default.magenta(" commit abort. "));
+ showCancelMessage();
return false;
}
- const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response;
- const type = typesList.find((type2) => type2.value === commit_type);
- const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}`;
- const typeResponse = is_default_project ? defaultProject?.value : project_type;
- const projectType = projects.find((project) => project.value === typeResponse);
- const result = is_jira ? `[${projectType.prefix}-${jira_id}] ${commitTitle}` : commitTitle;
try {
- const commands = commit_description ? ["commit", "-m", result, "-m", commit_description] : ["commit", "-m", result];
- const commitResult = await execa__default("git", commands);
- const branchHashName = commitResult.stdout.match(/\[(.*?)\]/).pop();
- const [branchName, branchHash] = branchHashName.split(" ");
- console.log("-----------------------------------------------------------");
- console.log(picocolors__default.dim(commitResult.stdout));
- if (commitResult.stderr !== "") {
- console.log("-----------------------------------------------------------");
- console.log(picocolors__default.dim(commitResult.stderr));
- }
- console.log("-----------------------------------------------------------");
- console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Title "))} ${picocolors__default.green(result)}`);
- if (commit_description) {
- console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Description "))} ${picocolors__default.green(commit_description)}`);
- }
- console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Commit hash "))} ${picocolors__default.bold(picocolors__default.cyan(` ${branchHash} `))} (${picocolors__default.italic(picocolors__default.green(branchName))})`);
+ const commitTitle = buildCommitTitle(response.commit_type, response.commit_message);
+ const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue);
+ const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description);
+ const commitResult = await execa__default("git", gitCommands);
+ const { branch, hash } = parseCommitResult(commitResult.stdout);
+ showGitOutput(commitResult.stdout, commitResult.stderr);
+ showCommitResult({
+ title: finalCommitMessage,
+ description: response.commit_description,
+ hash,
+ branch
+ });
+ return true;
} catch (error) {
- console.log(picocolors__default.red(error.stderr));
- if (error.exitCode === 1)
- console.log(picocolors__default.bgRed(" No changes added to commit. "));
- else
- console.error(error);
+ showGitError(error);
+ return false;
}
};
diff --git a/dist/index.mjs b/dist/index.mjs
index 179bf82..6c2010d 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -77,7 +77,7 @@ const commitTypes = [
name: "storybook",
emoji: "\u{1F4DA}",
description: "New storybook",
- value: "story"
+ value: "storybook"
},
{
name: "revert",
@@ -117,100 +117,147 @@ const projects = [
const __dirname$2 = dirname(fileURLToPath(import.meta.url));
const rootPath$1 = resolve(__dirname$2, "../");
-let defaultProjectValue = "";
-try {
- const filePath = resolve(rootPath$1, "keep/cz_config.json");
- const config = fs.readFileSync(filePath);
- defaultProjectValue = JSON.parse(config).defaultProject;
-} catch (e) {
- console.log(picocolors.yellow(picocolors.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. ")));
- defaultProjectValue = "";
-}
-const typesList = commitTypes.map((type) => ({
- title: type.name,
- description: `${type.emoji} ${type.description}`,
- value: type.value,
- emoji: type.emoji
-}));
-const step_type$1 = {
- type: "autocomplete",
- name: "commit_type",
- message: "Pick a commit type.",
- choices: typesList,
- fallback: "No matched type."
-};
-const step_message = {
- type: "text",
- name: "commit_message",
- message: (prev) => {
- const target = typesList.find((type) => type.value === prev);
- return `${target.emoji} ${target.title}`;
- },
- validate: (value) => {
- if (!value) {
- return "Commit message is required.";
- }
- return true;
+const loadProjectConfig = (filePath) => {
+ try {
+ const config = fs.readFileSync(filePath, "utf8");
+ return JSON.parse(config);
+ } catch (e) {
+ return null;
}
};
-const step_description = {
- type: "text",
- name: "commit_description",
- message: "Commit description (optional)",
- initial: "",
- validate: (value) => {
- if (value.length > 100) {
- return "Description is too long.";
- }
- return true;
- }
+const getDefaultProjectValue = (config) => {
+ return config?.defaultProject || "";
};
-const step_is_jira = {
- type: "confirm",
- name: "is_jira",
- message: "Tag Jira issue ?",
- initial: false
+const buildCommitTypesList = () => {
+ return commitTypes.map((type) => ({
+ title: type.name,
+ description: `${type.emoji} ${type.description}`,
+ value: type.value,
+ emoji: type.emoji
+ }));
};
-const projectsList$1 = projects.map((project) => ({
- title: project.name,
- description: `[${project.prefix}-13845] title`,
- value: project.value
-}));
-const defaultProject = projectsList$1.find((project) => project.value === defaultProjectValue);
-const step_is_default_project = {
- type: (prev) => prev ? "confirm" : null,
- name: "is_default_project",
- message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
- initial: true
+const buildProjectsList = () => {
+ return projects.map((project) => ({
+ title: project.name,
+ description: `[${project.prefix}-13845] title`,
+ value: project.value
+ }));
};
-const step_project_type = {
- type: (prev, { is_jira }) => {
- return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null;
- },
- name: "project_type",
- message: "Pick a project type.",
- choices: projectsList$1,
- initial: "owlpay",
- fallback: "No matched project."
+const findCommitType = (commitTypeValue) => {
+ return commitTypes.find((type) => type.value === commitTypeValue);
};
-const step_jira_id = {
- type: (prev) => prev ? "number" : null,
- name: "jira_id",
- message: "Jira issue id",
- onRender() {
- this.msg = picocolors.bgCyan(picocolors.white(" Jira issue ID "));
- },
- validate: (value) => {
- if (!value) {
- return "Jira issue ID is required.";
- }
- return true;
+const findProject = (projectValue) => {
+ return projects.find((project) => project.value === projectValue);
+};
+const buildCommitTitle = (commitType, message) => {
+ const type = findCommitType(commitType);
+ if (!type) {
+ throw new Error(`Invalid commit type: ${commitType}`);
}
+ return `${type.emoji} ${commitType}: ${message}`;
};
-const cli = async () => {
- let isCanceled = false;
- const order = [
- step_type$1,
+const buildFinalCommitMessage = (commitTitle, response, defaultProjectValue) => {
+ const { is_jira, is_default_project, project_type, jira_id } = response;
+ if (!is_jira) {
+ return commitTitle;
+ }
+ const typeResponse = is_default_project ? defaultProjectValue : project_type;
+ const projectType = findProject(typeResponse);
+ if (!projectType) {
+ throw new Error(`Invalid project type: ${typeResponse}`);
+ }
+ if (!jira_id) {
+ throw new Error("Jira ID is required when using Jira integration");
+ }
+ return `[${projectType.prefix}-${jira_id}] ${commitTitle}`;
+};
+const buildGitCommands = (commitMessage, description) => {
+ if (description) {
+ return ["commit", "-m", commitMessage, "-m", description];
+ }
+ return ["commit", "-m", commitMessage];
+};
+const parseCommitResult = (stdout) => {
+ const branchHashName = stdout.match(/\[(.*?)\]/)?.pop();
+ if (!branchHashName) {
+ throw new Error("Could not parse commit result");
+ }
+ const [branchName, branchHash] = branchHashName.split(" ");
+ return { branch: branchName, hash: branchHash };
+};
+const buildPromptSteps = (defaultProjectValue, projectsList, typesList) => {
+ const defaultProject = projectsList.find((project) => project.value === defaultProjectValue);
+ const step_type = {
+ type: "autocomplete",
+ name: "commit_type",
+ message: "Pick a commit type.",
+ choices: typesList,
+ fallback: "No matched type."
+ };
+ const step_message = {
+ type: "text",
+ name: "commit_message",
+ message: (prev) => {
+ const target = typesList.find((type) => type.value === prev);
+ return `${target.emoji} ${target.title}`;
+ },
+ validate: (value) => {
+ if (!value) {
+ return "Commit message is required.";
+ }
+ return true;
+ }
+ };
+ const step_description = {
+ type: "text",
+ name: "commit_description",
+ message: "Commit description (optional)",
+ initial: "",
+ validate: (value) => {
+ if (value.length > 100) {
+ return "Description is too long.";
+ }
+ return true;
+ }
+ };
+ const step_is_jira = {
+ type: "confirm",
+ name: "is_jira",
+ message: "Tag Jira issue ?",
+ initial: false
+ };
+ const step_is_default_project = {
+ type: (prev) => prev ? "confirm" : null,
+ name: "is_default_project",
+ message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
+ initial: true
+ };
+ const step_project_type = {
+ type: (prev, { is_jira }) => {
+ return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null;
+ },
+ name: "project_type",
+ message: "Pick a project type.",
+ choices: projectsList,
+ initial: "owlpay",
+ fallback: "No matched project."
+ };
+ const step_jira_id = {
+ type: (prev) => prev ? "number" : null,
+ name: "jira_id",
+ message: "Jira issue id",
+ onRender() {
+ this.msg = picocolors.bgCyan(picocolors.white(" Jira issue ID "));
+ },
+ validate: (value) => {
+ if (!value) {
+ return "Jira issue ID is required.";
+ }
+ return true;
+ }
+ };
+ return [
+ step_type,
step_message,
step_description,
step_is_jira,
@@ -218,7 +265,52 @@ const cli = async () => {
step_project_type,
step_jira_id
].filter(Boolean);
- const response = await prompts(order, {
+};
+const showConfigMissingWarning = () => {
+ console.log(picocolors.yellow(picocolors.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. ")));
+};
+const showCancelMessage = () => {
+ console.log(picocolors.magenta(" commit abort. "));
+};
+const showCommitResult = (result) => {
+ console.log("-----------------------------------------------------------");
+ console.log(`${picocolors.bgGreen(picocolors.bold(" Title "))} ${picocolors.green(result.title)}`);
+ if (result.description) {
+ console.log(`${picocolors.bgGreen(picocolors.bold(" Description "))} ${picocolors.green(result.description)}`);
+ }
+ if (result.hash && result.branch) {
+ console.log(`${picocolors.bgGreen(picocolors.bold(" Commit hash "))} ${picocolors.bold(picocolors.cyan(` ${result.hash} `))} (${picocolors.italic(picocolors.green(result.branch))})`);
+ }
+};
+const showGitOutput = (stdout, stderr) => {
+ console.log("-----------------------------------------------------------");
+ console.log(picocolors.dim(stdout));
+ if (stderr && stderr !== "") {
+ console.log("-----------------------------------------------------------");
+ console.log(picocolors.dim(stderr));
+ }
+ console.log("-----------------------------------------------------------");
+};
+const showGitError = (error) => {
+ console.log(picocolors.red(error.stderr));
+ if (error.exitCode === 1) {
+ console.log(picocolors.bgRed(" No changes added to commit. "));
+ } else {
+ console.error(error);
+ }
+};
+const cli = async () => {
+ const configPath = resolve(rootPath$1, "keep/cz_config.json");
+ const config = loadProjectConfig(configPath);
+ const defaultProjectValue = getDefaultProjectValue(config);
+ if (!config) {
+ showConfigMissingWarning();
+ }
+ const typesList = buildCommitTypesList();
+ const projectsList = buildProjectsList();
+ const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList);
+ let isCanceled = false;
+ const response = await prompts(steps, {
onSubmit: (prompt, answers) => {
if (answers === void 0) {
isCanceled = true;
@@ -231,38 +323,26 @@ const cli = async () => {
}
});
if (isCanceled) {
- console.log(picocolors.magenta(" commit abort. "));
+ showCancelMessage();
return false;
}
- const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response;
- const type = typesList.find((type2) => type2.value === commit_type);
- const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}`;
- const typeResponse = is_default_project ? defaultProject?.value : project_type;
- const projectType = projects.find((project) => project.value === typeResponse);
- const result = is_jira ? `[${projectType.prefix}-${jira_id}] ${commitTitle}` : commitTitle;
try {
- const commands = commit_description ? ["commit", "-m", result, "-m", commit_description] : ["commit", "-m", result];
- const commitResult = await execa("git", commands);
- const branchHashName = commitResult.stdout.match(/\[(.*?)\]/).pop();
- const [branchName, branchHash] = branchHashName.split(" ");
- console.log("-----------------------------------------------------------");
- console.log(picocolors.dim(commitResult.stdout));
- if (commitResult.stderr !== "") {
- console.log("-----------------------------------------------------------");
- console.log(picocolors.dim(commitResult.stderr));
- }
- console.log("-----------------------------------------------------------");
- console.log(`${picocolors.bgGreen(picocolors.bold(" Title "))} ${picocolors.green(result)}`);
- if (commit_description) {
- console.log(`${picocolors.bgGreen(picocolors.bold(" Description "))} ${picocolors.green(commit_description)}`);
- }
- console.log(`${picocolors.bgGreen(picocolors.bold(" Commit hash "))} ${picocolors.bold(picocolors.cyan(` ${branchHash} `))} (${picocolors.italic(picocolors.green(branchName))})`);
+ const commitTitle = buildCommitTitle(response.commit_type, response.commit_message);
+ const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue);
+ const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description);
+ const commitResult = await execa("git", gitCommands);
+ const { branch, hash } = parseCommitResult(commitResult.stdout);
+ showGitOutput(commitResult.stdout, commitResult.stderr);
+ showCommitResult({
+ title: finalCommitMessage,
+ description: response.commit_description,
+ hash,
+ branch
+ });
+ return true;
} catch (error) {
- console.log(picocolors.red(error.stderr));
- if (error.exitCode === 1)
- console.log(picocolors.bgRed(" No changes added to commit. "));
- else
- console.error(error);
+ showGitError(error);
+ return false;
}
};
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 0000000..f271f2c
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,15 @@
+pre-commit:
+ parallel: true
+ commands:
+ tests:
+ run: pnpm test:run
+ fail_text: "โ Tests failed! Please fix the issues before committing."
+ stage_fixed: false
+
+ type-check:
+ run: pnpm build
+ fail_text: "โ TypeScript compilation failed! Please fix type errors before committing."
+ stage_fixed: false
+
+# Configuration
+assert_lefthook_installed: true
diff --git a/package.json b/package.json
index d991559..369f049 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "owlting_cz",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "create-commit for OwlTing",
"type": "module",
"dependencies": {
@@ -14,7 +14,15 @@
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
- "build": "unbuild"
+ "build": "unbuild",
+ "test": "vitest",
+ "test:run": "vitest run",
+ "test:watch": "vitest --watch",
+ "test:coverage": "vitest run --coverage",
+ "test:ui": "vitest --ui",
+ "coverage:badge": "pnpm test:coverage && node scripts/generate-coverage-badge.js",
+ "hooks:install": "lefthook install",
+ "hooks:uninstall": "lefthook uninstall"
},
"bin": {
"cz": "bin/index.mjs"
@@ -27,9 +35,15 @@
"commit"
],
"author": "Nick",
+ "engines": {
+ "node": ">=14.0.0"
+ },
"devDependencies": {
"@types/node": "^22.1.0",
"@types/prompts": "^2.4.9",
- "@types/yargs": "^17.0.33"
+ "@types/yargs": "^17.0.33",
+ "@vitest/coverage-v8": "^2.0.0",
+ "lefthook": "^1.11.13",
+ "vitest": "^2.0.0"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c7e2e8a..0f79d16 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -33,6 +33,15 @@ importers:
'@types/yargs':
specifier: ^17.0.33
version: 17.0.33
+ '@vitest/coverage-v8':
+ specifier: ^2.0.0
+ version: 2.1.9(vitest@2.1.9(@types/node@22.1.0))
+ lefthook:
+ specifier: ^1.11.13
+ version: 1.11.13
+ vitest:
+ specifier: ^2.0.0
+ version: 2.1.9(@types/node@22.1.0)
packages:
@@ -78,10 +87,18 @@ packages:
resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.24.7':
resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.27.1':
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-option@7.24.8':
resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==}
engines: {node: '>=6.9.0'}
@@ -99,6 +116,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/parser@7.27.5':
+ resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
'@babel/standalone@7.25.3':
resolution: {integrity: sha512-uR+EoBqIIIvKGCG7fOj7HKupu3zVObiMfdEwoPZfVCPpcWJaZ1PkshaP5/6cl6BKAm1Zcv25O1rf+uoQ7V8nqA==}
engines: {node: '>=6.9.0'}
@@ -115,12 +137,25 @@ packages:
resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.27.6':
+ resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@0.2.3':
+ resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
+ '@esbuild/aix-ppc64@0.21.5':
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/aix-ppc64@0.23.0':
resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==}
engines: {node: '>=18'}
@@ -133,6 +168,12 @@ packages:
cpu: [arm64]
os: [android]
+ '@esbuild/android-arm64@0.21.5':
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/android-arm64@0.23.0':
resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==}
engines: {node: '>=18'}
@@ -145,6 +186,12 @@ packages:
cpu: [arm]
os: [android]
+ '@esbuild/android-arm@0.21.5':
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-arm@0.23.0':
resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==}
engines: {node: '>=18'}
@@ -157,6 +204,12 @@ packages:
cpu: [x64]
os: [android]
+ '@esbuild/android-x64@0.21.5':
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/android-x64@0.23.0':
resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==}
engines: {node: '>=18'}
@@ -169,6 +222,12 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-arm64@0.21.5':
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-arm64@0.23.0':
resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==}
engines: {node: '>=18'}
@@ -181,6 +240,12 @@ packages:
cpu: [x64]
os: [darwin]
+ '@esbuild/darwin-x64@0.21.5':
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.23.0':
resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==}
engines: {node: '>=18'}
@@ -193,6 +258,12 @@ packages:
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-arm64@0.21.5':
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-arm64@0.23.0':
resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==}
engines: {node: '>=18'}
@@ -205,6 +276,12 @@ packages:
cpu: [x64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.21.5':
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.23.0':
resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==}
engines: {node: '>=18'}
@@ -217,6 +294,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm64@0.21.5':
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm64@0.23.0':
resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==}
engines: {node: '>=18'}
@@ -229,6 +312,12 @@ packages:
cpu: [arm]
os: [linux]
+ '@esbuild/linux-arm@0.21.5':
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-arm@0.23.0':
resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==}
engines: {node: '>=18'}
@@ -241,6 +330,12 @@ packages:
cpu: [ia32]
os: [linux]
+ '@esbuild/linux-ia32@0.21.5':
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/linux-ia32@0.23.0':
resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==}
engines: {node: '>=18'}
@@ -253,6 +348,12 @@ packages:
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-loong64@0.21.5':
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-loong64@0.23.0':
resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==}
engines: {node: '>=18'}
@@ -265,6 +366,12 @@ packages:
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-mips64el@0.21.5':
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.23.0':
resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==}
engines: {node: '>=18'}
@@ -277,6 +384,12 @@ packages:
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-ppc64@0.21.5':
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.23.0':
resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==}
engines: {node: '>=18'}
@@ -289,6 +402,12 @@ packages:
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-riscv64@0.21.5':
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.23.0':
resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==}
engines: {node: '>=18'}
@@ -301,6 +420,12 @@ packages:
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-s390x@0.21.5':
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-s390x@0.23.0':
resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==}
engines: {node: '>=18'}
@@ -313,6 +438,12 @@ packages:
cpu: [x64]
os: [linux]
+ '@esbuild/linux-x64@0.21.5':
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/linux-x64@0.23.0':
resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==}
engines: {node: '>=18'}
@@ -325,6 +456,12 @@ packages:
cpu: [x64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.21.5':
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.23.0':
resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==}
engines: {node: '>=18'}
@@ -343,6 +480,12 @@ packages:
cpu: [x64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.21.5':
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.23.0':
resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==}
engines: {node: '>=18'}
@@ -355,6 +498,12 @@ packages:
cpu: [x64]
os: [sunos]
+ '@esbuild/sunos-x64@0.21.5':
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/sunos-x64@0.23.0':
resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==}
engines: {node: '>=18'}
@@ -367,6 +516,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-arm64@0.21.5':
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-arm64@0.23.0':
resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==}
engines: {node: '>=18'}
@@ -379,6 +534,12 @@ packages:
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-ia32@0.21.5':
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-ia32@0.23.0':
resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==}
engines: {node: '>=18'}
@@ -391,12 +552,26 @@ packages:
cpu: [x64]
os: [win32]
+ '@esbuild/win32-x64@0.21.5':
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
'@esbuild/win32-x64@0.23.0':
resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@istanbuljs/schema@0.1.3':
+ resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ engines: {node: '>=8'}
+
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
@@ -427,6 +602,10 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@rollup/plugin-alias@5.1.0':
resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
engines: {node: '>=14.0.0'}
@@ -481,6 +660,106 @@ packages:
rollup:
optional: true
+ '@rollup/rollup-android-arm-eabi@4.42.0':
+ resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.42.0':
+ resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.42.0':
+ resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.42.0':
+ resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.42.0':
+ resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.42.0':
+ resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.42.0':
+ resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.42.0':
+ resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.42.0':
+ resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.42.0':
+ resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.42.0':
+ resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
+ resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.42.0':
+ resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.42.0':
+ resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.42.0':
+ resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.42.0':
+ resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.42.0':
+ resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.42.0':
+ resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.42.0':
+ resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.42.0':
+ resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==}
+ cpu: [x64]
+ os: [win32]
+
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -488,6 +767,9 @@ packages:
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
'@types/node@22.1.0':
resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==}
@@ -503,6 +785,44 @@ packages:
'@types/yargs@17.0.33':
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
+ '@vitest/coverage-v8@2.1.9':
+ resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==}
+ peerDependencies:
+ '@vitest/browser': 2.1.9
+ vitest: 2.1.9
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
+ '@vitest/expect@2.1.9':
+ resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
+
+ '@vitest/mocker@2.1.9':
+ resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@2.1.9':
+ resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
+ '@vitest/runner@2.1.9':
+ resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
+
+ '@vitest/snapshot@2.1.9':
+ resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
+
+ '@vitest/spy@2.1.9':
+ resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
+
+ '@vitest/utils@2.1.9':
+ resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
acorn@8.12.1:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
engines: {node: '>=0.4.0'}
@@ -512,6 +832,10 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
+ ansi-regex@6.1.0:
+ resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
+ engines: {node: '>=12'}
+
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@@ -520,6 +844,14 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@@ -549,12 +881,20 @@ packages:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
+ chai@5.2.0:
+ resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
+ engines: {node: '>=12'}
+
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -563,6 +903,10 @@ packages:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ check-error@2.1.1:
+ resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+ engines: {node: '>= 16'}
+
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
@@ -607,6 +951,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
css-declaration-sorter@7.2.0:
resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
engines: {node: ^14 || ^16 || >=18}
@@ -664,6 +1012,19 @@ packages:
supports-color:
optional: true
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -688,21 +1049,35 @@ packages:
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
electron-to-chromium@1.5.5:
resolution: {integrity: sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
esbuild@0.19.12:
resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
engines: {node: '>=12'}
hasBin: true
+ esbuild@0.21.5:
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+ engines: {node: '>=12'}
+ hasBin: true
+
esbuild@0.23.0:
resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==}
engines: {node: '>=18'}
@@ -723,10 +1098,17 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
+ expect-type@1.2.1:
+ resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
+ engines: {node: '>=12.0.0'}
+
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -738,6 +1120,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -768,6 +1154,10 @@ packages:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
+ glob@10.4.5:
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ hasBin: true
+
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
@@ -785,6 +1175,10 @@ packages:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -792,6 +1186,9 @@ packages:
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -844,6 +1241,25 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.1.7:
+ resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
+ engines: {node: '>=8'}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
@@ -865,6 +1281,60 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
+ lefthook-darwin-arm64@1.11.13:
+ resolution: {integrity: sha512-gHwHofXupCtzNLN+8esdWfFTnAEkmBxE/WKA0EwxPPJXdZYa1GUsiG5ipq/CdG/0j8ekYyM9Hzyrrk5BqJ42xw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ lefthook-darwin-x64@1.11.13:
+ resolution: {integrity: sha512-zYxkWNUirmTidhskY9J9AwxvdMi3YKH+TqZ3AQ1EOqkOwPBWJQW5PbnzsXDrd3YnrtZScYm/tE/moXJpEPPIpQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ lefthook-freebsd-arm64@1.11.13:
+ resolution: {integrity: sha512-gJzWnllcMcivusmPorEkXPpEciKotlBHn7QxWwYaSjss/U3YdZu+NTjDO30b3qeiVlyq4RAZ4BPKJODXxHHwUA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ lefthook-freebsd-x64@1.11.13:
+ resolution: {integrity: sha512-689XdchgtDvZQWFFx1szUvm/mqrq/v6laki0odq5FAfcSgUeLu3w+z6UicBS5l55eFJuQTDNKARFqrKJ04e+Vw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ lefthook-linux-arm64@1.11.13:
+ resolution: {integrity: sha512-ujCLbaZg5S/Ao8KZAcNSb+Y3gl898ZEM0YKyiZmZo22dFFpm/5gcV46pF3xaqIw5IpH+3YYDTKDU+qTetmARyQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ lefthook-linux-x64@1.11.13:
+ resolution: {integrity: sha512-O5WdodeBtFOXQlvPcckqp4W/yqVM9DbVQBkvOxwSJlmsxO4sGYK1TqdxH9ihLB85B2kPPssZj9ze36/oizzhVQ==}
+ cpu: [x64]
+ os: [linux]
+
+ lefthook-openbsd-arm64@1.11.13:
+ resolution: {integrity: sha512-SyBpciUfvY/lUDbZu7L6MtL/SVG2+yMTckBgb4PdJQhJlisY0IsyOYdlTw2icPPrY7JnwdsFv8UW0EJOB76W4g==}
+ cpu: [arm64]
+ os: [openbsd]
+
+ lefthook-openbsd-x64@1.11.13:
+ resolution: {integrity: sha512-6+/0j6O2dzo9cjTWUKfL2J6hRR7Krna/ssqnW8cWh8QHZKO9WJn34epto9qgjeHwSysou8byI7Mwv5zOGthLCQ==}
+ cpu: [x64]
+ os: [openbsd]
+
+ lefthook-windows-arm64@1.11.13:
+ resolution: {integrity: sha512-w5TwZ8bsZ17uOMtYGc5oEb4tCHjNTSeSXRy6H9Yic8E7IsPZtZLkaZGnIIwgXFuhhrcCdc6FuTvKt2tyV7EW2g==}
+ cpu: [arm64]
+ os: [win32]
+
+ lefthook-windows-x64@1.11.13:
+ resolution: {integrity: sha512-7lvwnIs8CNOXKU4y3i1Pbqna+QegIORkSD2VCuHBNpIJ8H84NpjoG3tKU91IM/aI1a2eUvCk+dw+1rfMRz7Ytg==}
+ cpu: [x64]
+ os: [win32]
+
+ lefthook@1.11.13:
+ resolution: {integrity: sha512-SDTk3D4nW1XRpR9u9fdYQ/qj1xeZVIwZmIFdJUnyq+w9ZLdCCvIrOmtD8SFiJowSevISjQDC+f9nqyFXUxc0SQ==}
+ hasBin: true
+
lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
@@ -875,12 +1345,28 @@ packages:
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
+ loupe@3.1.3:
+ resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
+
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
magic-string@0.30.11:
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
+ magic-string@0.30.17:
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
@@ -906,6 +1392,14 @@ packages:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
mkdist@1.5.4:
resolution: {integrity: sha512-GEmKYJG5K1YGFIq3t0K3iihZ8FTgXphLf/4UjbmpXIAtBFn4lEjXk3pXNTSfy7EtcEXhp2Nn1vzw5pIus6RY3g==}
hasBin: true
@@ -931,6 +1425,14 @@ packages:
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -957,6 +1459,9 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -964,6 +1469,10 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -971,12 +1480,19 @@ packages:
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+ pathval@2.0.0:
+ resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
+ engines: {node: '>= 14.16'}
+
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@@ -1163,6 +1679,10 @@ packages:
resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.4:
+ resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==}
+ engines: {node: ^10 || ^12 || >=14}
+
pretty-bytes@6.1.1:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
@@ -1198,6 +1718,11 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
+ rollup@4.42.0:
+ resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -1221,9 +1746,16 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
signal-exit@3.0.5:
resolution: {integrity: sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==}
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -1235,14 +1767,32 @@ packages:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.9.0:
+ resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
+ strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+
strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
@@ -1257,6 +1807,10 @@ packages:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -1266,6 +1820,28 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ test-exclude@7.0.1:
+ resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
+ engines: {node: '>=18'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ tinypool@1.1.0:
+ resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@1.2.0:
+ resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@3.0.2:
+ resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+ engines: {node: '>=14.0.0'}
+
to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@@ -1307,15 +1883,85 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ vite-node@2.1.9:
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
+ vite@5.4.19:
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
+ vitest@2.1.9:
+ resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 2.1.9
+ '@vitest/ui': 2.1.9
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -1409,8 +2055,12 @@ snapshots:
'@babel/helper-string-parser@7.24.8': {}
+ '@babel/helper-string-parser@7.27.1': {}
+
'@babel/helper-validator-identifier@7.24.7': {}
+ '@babel/helper-validator-identifier@7.27.1': {}
+
'@babel/helper-validator-option@7.24.8': {}
'@babel/helpers@7.25.0':
@@ -1429,6 +2079,10 @@ snapshots:
dependencies:
'@babel/types': 7.25.2
+ '@babel/parser@7.27.5':
+ dependencies:
+ '@babel/types': 7.27.6
+
'@babel/standalone@7.25.3': {}
'@babel/template@7.25.0':
@@ -1455,111 +2109,172 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
+ '@babel/types@7.27.6':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+
+ '@bcoe/v8-coverage@0.2.3': {}
+
'@esbuild/aix-ppc64@0.19.12':
optional: true
+ '@esbuild/aix-ppc64@0.21.5':
+ optional: true
+
'@esbuild/aix-ppc64@0.23.0':
optional: true
'@esbuild/android-arm64@0.19.12':
optional: true
+ '@esbuild/android-arm64@0.21.5':
+ optional: true
+
'@esbuild/android-arm64@0.23.0':
optional: true
'@esbuild/android-arm@0.19.12':
optional: true
+ '@esbuild/android-arm@0.21.5':
+ optional: true
+
'@esbuild/android-arm@0.23.0':
optional: true
'@esbuild/android-x64@0.19.12':
optional: true
+ '@esbuild/android-x64@0.21.5':
+ optional: true
+
'@esbuild/android-x64@0.23.0':
optional: true
'@esbuild/darwin-arm64@0.19.12':
optional: true
+ '@esbuild/darwin-arm64@0.21.5':
+ optional: true
+
'@esbuild/darwin-arm64@0.23.0':
optional: true
'@esbuild/darwin-x64@0.19.12':
optional: true
+ '@esbuild/darwin-x64@0.21.5':
+ optional: true
+
'@esbuild/darwin-x64@0.23.0':
optional: true
'@esbuild/freebsd-arm64@0.19.12':
optional: true
+ '@esbuild/freebsd-arm64@0.21.5':
+ optional: true
+
'@esbuild/freebsd-arm64@0.23.0':
optional: true
'@esbuild/freebsd-x64@0.19.12':
optional: true
+ '@esbuild/freebsd-x64@0.21.5':
+ optional: true
+
'@esbuild/freebsd-x64@0.23.0':
optional: true
'@esbuild/linux-arm64@0.19.12':
optional: true
+ '@esbuild/linux-arm64@0.21.5':
+ optional: true
+
'@esbuild/linux-arm64@0.23.0':
optional: true
'@esbuild/linux-arm@0.19.12':
optional: true
+ '@esbuild/linux-arm@0.21.5':
+ optional: true
+
'@esbuild/linux-arm@0.23.0':
optional: true
'@esbuild/linux-ia32@0.19.12':
optional: true
+ '@esbuild/linux-ia32@0.21.5':
+ optional: true
+
'@esbuild/linux-ia32@0.23.0':
optional: true
'@esbuild/linux-loong64@0.19.12':
optional: true
+ '@esbuild/linux-loong64@0.21.5':
+ optional: true
+
'@esbuild/linux-loong64@0.23.0':
optional: true
'@esbuild/linux-mips64el@0.19.12':
optional: true
+ '@esbuild/linux-mips64el@0.21.5':
+ optional: true
+
'@esbuild/linux-mips64el@0.23.0':
optional: true
'@esbuild/linux-ppc64@0.19.12':
optional: true
+ '@esbuild/linux-ppc64@0.21.5':
+ optional: true
+
'@esbuild/linux-ppc64@0.23.0':
optional: true
'@esbuild/linux-riscv64@0.19.12':
optional: true
+ '@esbuild/linux-riscv64@0.21.5':
+ optional: true
+
'@esbuild/linux-riscv64@0.23.0':
optional: true
'@esbuild/linux-s390x@0.19.12':
optional: true
+ '@esbuild/linux-s390x@0.21.5':
+ optional: true
+
'@esbuild/linux-s390x@0.23.0':
optional: true
'@esbuild/linux-x64@0.19.12':
optional: true
+ '@esbuild/linux-x64@0.21.5':
+ optional: true
+
'@esbuild/linux-x64@0.23.0':
optional: true
'@esbuild/netbsd-x64@0.19.12':
optional: true
+ '@esbuild/netbsd-x64@0.21.5':
+ optional: true
+
'@esbuild/netbsd-x64@0.23.0':
optional: true
@@ -1569,33 +2284,59 @@ snapshots:
'@esbuild/openbsd-x64@0.19.12':
optional: true
+ '@esbuild/openbsd-x64@0.21.5':
+ optional: true
+
'@esbuild/openbsd-x64@0.23.0':
optional: true
'@esbuild/sunos-x64@0.19.12':
optional: true
+ '@esbuild/sunos-x64@0.21.5':
+ optional: true
+
'@esbuild/sunos-x64@0.23.0':
optional: true
'@esbuild/win32-arm64@0.19.12':
optional: true
+ '@esbuild/win32-arm64@0.21.5':
+ optional: true
+
'@esbuild/win32-arm64@0.23.0':
optional: true
'@esbuild/win32-ia32@0.19.12':
optional: true
+ '@esbuild/win32-ia32@0.21.5':
+ optional: true
+
'@esbuild/win32-ia32@0.23.0':
optional: true
'@esbuild/win32-x64@0.19.12':
optional: true
+ '@esbuild/win32-x64@0.21.5':
+ optional: true
+
'@esbuild/win32-x64@0.23.0':
optional: true
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@istanbuljs/schema@0.1.3': {}
+
'@jridgewell/gen-mapping@0.3.5':
dependencies:
'@jridgewell/set-array': 1.2.1
@@ -1625,6 +2366,9 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@rollup/plugin-alias@5.1.0(rollup@3.29.4)':
dependencies:
slash: 4.0.0
@@ -1674,10 +2418,72 @@ snapshots:
optionalDependencies:
rollup: 3.29.4
+ '@rollup/rollup-android-arm-eabi@4.42.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.42.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.42.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.42.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.42.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.42.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.42.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.42.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.42.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.42.0':
+ optional: true
+
'@trysound/sax@0.2.0': {}
'@types/estree@1.0.5': {}
+ '@types/estree@1.0.7': {}
+
'@types/node@22.1.0':
dependencies:
undici-types: 6.13.0
@@ -1695,10 +2501,70 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
+ '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.1.0))':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@bcoe/v8-coverage': 0.2.3
+ debug: 4.4.1
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.1.7
+ magic-string: 0.30.17
+ magicast: 0.3.5
+ std-env: 3.9.0
+ test-exclude: 7.0.1
+ tinyrainbow: 1.2.0
+ vitest: 2.1.9(@types/node@22.1.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@2.1.9':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.2.0
+ tinyrainbow: 1.2.0
+
+ '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.1.0))':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.17
+ optionalDependencies:
+ vite: 5.4.19(@types/node@22.1.0)
+
+ '@vitest/pretty-format@2.1.9':
+ dependencies:
+ tinyrainbow: 1.2.0
+
+ '@vitest/runner@2.1.9':
+ dependencies:
+ '@vitest/utils': 2.1.9
+ pathe: 1.1.2
+
+ '@vitest/snapshot@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ magic-string: 0.30.17
+ pathe: 1.1.2
+
+ '@vitest/spy@2.1.9':
+ dependencies:
+ tinyspy: 3.0.2
+
+ '@vitest/utils@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ loupe: 3.1.3
+ tinyrainbow: 1.2.0
+
acorn@8.12.1: {}
ansi-regex@5.0.1: {}
+ ansi-regex@6.1.0: {}
+
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
@@ -1707,6 +2573,10 @@ snapshots:
dependencies:
color-convert: 2.0.1
+ ansi-styles@6.2.1: {}
+
+ assertion-error@2.0.1: {}
+
autoprefixer@10.4.20(postcss@8.4.41):
dependencies:
browserslist: 4.23.3
@@ -1738,6 +2608,8 @@ snapshots:
builtin-modules@3.3.0: {}
+ cac@6.7.14: {}
+
caniuse-api@3.0.0:
dependencies:
browserslist: 4.23.3
@@ -1747,6 +2619,14 @@ snapshots:
caniuse-lite@1.0.30001651: {}
+ chai@5.2.0:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.1
+ deep-eql: 5.0.2
+ loupe: 3.1.3
+ pathval: 2.0.0
+
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@@ -1755,6 +2635,8 @@ snapshots:
chalk@5.3.0: {}
+ check-error@2.1.1: {}
+
citty@0.1.6:
dependencies:
consola: 3.2.3
@@ -1795,6 +2677,12 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
css-declaration-sorter@7.2.0(postcss@8.4.41):
dependencies:
postcss: 8.4.41
@@ -1873,6 +2761,12 @@ snapshots:
dependencies:
ms: 2.1.2
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ deep-eql@5.0.2: {}
+
deepmerge@4.3.1: {}
defu@6.1.4: {}
@@ -1899,12 +2793,18 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
+ eastasianwidth@0.2.0: {}
+
electron-to-chromium@1.5.5: {}
emoji-regex@8.0.0: {}
+ emoji-regex@9.2.2: {}
+
entities@4.5.0: {}
+ es-module-lexer@1.7.0: {}
+
esbuild@0.19.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.19.12
@@ -1931,6 +2831,32 @@ snapshots:
'@esbuild/win32-ia32': 0.19.12
'@esbuild/win32-x64': 0.19.12
+ esbuild@0.21.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.21.5
+ '@esbuild/android-arm': 0.21.5
+ '@esbuild/android-arm64': 0.21.5
+ '@esbuild/android-x64': 0.21.5
+ '@esbuild/darwin-arm64': 0.21.5
+ '@esbuild/darwin-x64': 0.21.5
+ '@esbuild/freebsd-arm64': 0.21.5
+ '@esbuild/freebsd-x64': 0.21.5
+ '@esbuild/linux-arm': 0.21.5
+ '@esbuild/linux-arm64': 0.21.5
+ '@esbuild/linux-ia32': 0.21.5
+ '@esbuild/linux-loong64': 0.21.5
+ '@esbuild/linux-mips64el': 0.21.5
+ '@esbuild/linux-ppc64': 0.21.5
+ '@esbuild/linux-riscv64': 0.21.5
+ '@esbuild/linux-s390x': 0.21.5
+ '@esbuild/linux-x64': 0.21.5
+ '@esbuild/netbsd-x64': 0.21.5
+ '@esbuild/openbsd-x64': 0.21.5
+ '@esbuild/sunos-x64': 0.21.5
+ '@esbuild/win32-arm64': 0.21.5
+ '@esbuild/win32-ia32': 0.21.5
+ '@esbuild/win32-x64': 0.21.5
+
esbuild@0.23.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.23.0
@@ -1966,6 +2892,10 @@ snapshots:
estree-walker@2.0.2: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.5
+
execa@5.1.1:
dependencies:
cross-spawn: 7.0.3
@@ -1978,6 +2908,8 @@ snapshots:
signal-exit: 3.0.5
strip-final-newline: 2.0.0
+ expect-type@1.2.1: {}
+
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -1994,6 +2926,11 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
fraction.js@4.3.7: {}
fs.realpath@1.0.0: {}
@@ -2013,6 +2950,15 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob@10.4.5:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
glob@8.1.0:
dependencies:
fs.realpath: 1.0.0
@@ -2033,12 +2979,16 @@ snapshots:
has-flag@3.0.0: {}
+ has-flag@4.0.0: {}
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hookable@5.5.3: {}
+ html-escaper@2.0.2: {}
+
human-signals@2.1.0: {}
ignore@5.3.1: {}
@@ -2078,6 +3028,33 @@ snapshots:
isexe@2.0.0: {}
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.25
+ debug: 4.4.1
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.1.7:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
jiti@1.21.6: {}
js-tokens@4.0.0: {}
@@ -2088,12 +3065,59 @@ snapshots:
kleur@3.0.3: {}
+ lefthook-darwin-arm64@1.11.13:
+ optional: true
+
+ lefthook-darwin-x64@1.11.13:
+ optional: true
+
+ lefthook-freebsd-arm64@1.11.13:
+ optional: true
+
+ lefthook-freebsd-x64@1.11.13:
+ optional: true
+
+ lefthook-linux-arm64@1.11.13:
+ optional: true
+
+ lefthook-linux-x64@1.11.13:
+ optional: true
+
+ lefthook-openbsd-arm64@1.11.13:
+ optional: true
+
+ lefthook-openbsd-x64@1.11.13:
+ optional: true
+
+ lefthook-windows-arm64@1.11.13:
+ optional: true
+
+ lefthook-windows-x64@1.11.13:
+ optional: true
+
+ lefthook@1.11.13:
+ optionalDependencies:
+ lefthook-darwin-arm64: 1.11.13
+ lefthook-darwin-x64: 1.11.13
+ lefthook-freebsd-arm64: 1.11.13
+ lefthook-freebsd-x64: 1.11.13
+ lefthook-linux-arm64: 1.11.13
+ lefthook-linux-x64: 1.11.13
+ lefthook-openbsd-arm64: 1.11.13
+ lefthook-openbsd-x64: 1.11.13
+ lefthook-windows-arm64: 1.11.13
+ lefthook-windows-x64: 1.11.13
+
lilconfig@3.1.2: {}
lodash.memoize@4.1.2: {}
lodash.uniq@4.5.0: {}
+ loupe@3.1.3: {}
+
+ lru-cache@10.4.3: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -2102,6 +3126,20 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ magic-string@0.30.17:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.27.5
+ '@babel/types': 7.27.6
+ source-map-js: 1.2.0
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.6.3
+
mdn-data@2.0.28: {}
mdn-data@2.0.30: {}
@@ -2121,6 +3159,12 @@ snapshots:
dependencies:
brace-expansion: 2.0.1
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ minipass@7.1.2: {}
+
mkdist@1.5.4(typescript@5.5.4):
dependencies:
autoprefixer: 10.4.20(postcss@8.4.41)
@@ -2150,6 +3194,10 @@ snapshots:
ms@2.1.2: {}
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
nanoid@3.3.7: {}
node-releases@2.0.18: {}
@@ -2172,18 +3220,29 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
+ package-json-from-dist@1.0.1: {}
+
path-key@3.1.1: {}
path-parse@1.0.7: {}
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+
path-type@4.0.0: {}
pathe@1.1.2: {}
+ pathval@2.0.0: {}
+
picocolors@1.0.0: {}
picocolors@1.0.1: {}
+ picocolors@1.1.1: {}
+
picomatch@2.3.1: {}
pkg-types@1.1.3:
@@ -2359,6 +3418,12 @@ snapshots:
picocolors: 1.0.1
source-map-js: 1.2.0
+ postcss@8.5.4:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
pretty-bytes@6.1.1: {}
prompts@2.4.2:
@@ -2390,6 +3455,32 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ rollup@4.42.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.42.0
+ '@rollup/rollup-android-arm64': 4.42.0
+ '@rollup/rollup-darwin-arm64': 4.42.0
+ '@rollup/rollup-darwin-x64': 4.42.0
+ '@rollup/rollup-freebsd-arm64': 4.42.0
+ '@rollup/rollup-freebsd-x64': 4.42.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.42.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.42.0
+ '@rollup/rollup-linux-arm64-gnu': 4.42.0
+ '@rollup/rollup-linux-arm64-musl': 4.42.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.42.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.42.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.42.0
+ '@rollup/rollup-linux-riscv64-musl': 4.42.0
+ '@rollup/rollup-linux-s390x-gnu': 4.42.0
+ '@rollup/rollup-linux-x64-gnu': 4.42.0
+ '@rollup/rollup-linux-x64-musl': 4.42.0
+ '@rollup/rollup-win32-arm64-msvc': 4.42.0
+ '@rollup/rollup-win32-ia32-msvc': 4.42.0
+ '@rollup/rollup-win32-x64-msvc': 4.42.0
+ fsevents: 2.3.3
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -2406,24 +3497,44 @@ snapshots:
shebang-regex@3.0.0: {}
+ siginfo@2.0.0: {}
+
signal-exit@3.0.5: {}
+ signal-exit@4.1.0: {}
+
sisteransi@1.0.5: {}
slash@4.0.0: {}
source-map-js@1.2.0: {}
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@3.9.0: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
+ strip-ansi@7.1.0:
+ dependencies:
+ ansi-regex: 6.1.0
+
strip-final-newline@2.0.0: {}
stylehacks@7.0.2(postcss@8.4.41):
@@ -2436,6 +3547,10 @@ snapshots:
dependencies:
has-flag: 3.0.0
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
supports-preserve-symlinks-flag@1.0.0: {}
svgo@3.3.2:
@@ -2448,6 +3563,22 @@ snapshots:
csso: 5.0.5
picocolors: 1.0.0
+ test-exclude@7.0.1:
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 10.4.5
+ minimatch: 9.0.5
+
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinypool@1.1.0: {}
+
+ tinyrainbow@1.2.0: {}
+
+ tinyspy@3.0.2: {}
+
to-fast-properties@2.0.0: {}
to-regex-range@5.0.1:
@@ -2513,16 +3644,89 @@ snapshots:
util-deprecate@1.0.2: {}
+ vite-node@2.1.9(@types/node@22.1.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.1
+ es-module-lexer: 1.7.0
+ pathe: 1.1.2
+ vite: 5.4.19(@types/node@22.1.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vite@5.4.19(@types/node@22.1.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.4
+ rollup: 4.42.0
+ optionalDependencies:
+ '@types/node': 22.1.0
+ fsevents: 2.3.3
+
+ vitest@2.1.9(@types/node@22.1.0):
+ dependencies:
+ '@vitest/expect': 2.1.9
+ '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.1.0))
+ '@vitest/pretty-format': 2.1.9
+ '@vitest/runner': 2.1.9
+ '@vitest/snapshot': 2.1.9
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.2.0
+ debug: 4.4.1
+ expect-type: 1.2.1
+ magic-string: 0.30.17
+ pathe: 1.1.2
+ std-env: 3.9.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinypool: 1.1.0
+ tinyrainbow: 1.2.0
+ vite: 5.4.19(@types/node@22.1.0)
+ vite-node: 2.1.9(@types/node@22.1.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 22.1.0
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
which@2.0.2:
dependencies:
isexe: 2.0.0
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+
wrappy@1.0.2: {}
y18n@5.0.8: {}
diff --git a/scripts/generate-coverage-badge.js b/scripts/generate-coverage-badge.js
new file mode 100644
index 0000000..c2c5e9a
--- /dev/null
+++ b/scripts/generate-coverage-badge.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+
+import { readFileSync, writeFileSync } from 'fs';
+import { join } from 'path';
+
+try {
+ // ่ฎๅ coverage summary
+ const coverageFile = join(process.cwd(), 'coverage', 'coverage-summary.json');
+ const coverageData = JSON.parse(readFileSync(coverageFile, 'utf8'));
+
+ // ๅๅพ็ธฝ้ซ่ฆ่็
+ const totalCoverage = coverageData.total;
+ const statements = totalCoverage.statements.pct;
+ const branches = totalCoverage.branches.pct;
+ const functions = totalCoverage.functions.pct;
+ const lines = totalCoverage.lines.pct;
+
+ // ่จ็ฎๅนณๅ่ฆ่็
+ const avgCoverage = Math.round((statements + branches + functions + lines) / 4);
+
+ // ๆฑบๅฎ badge ้ก่ฒ
+ let color = 'red';
+ if (avgCoverage >= 80) color = 'brightgreen';
+ else if (avgCoverage >= 60) color = 'yellow';
+ else if (avgCoverage >= 40) color = 'orange';
+
+ // ็ๆ badge URL
+ const badgeUrl = `https://img.shields.io/badge/coverage-${avgCoverage}%25-${color}`;
+
+ // ็ๆ coverage ๅ ฑๅ
+ const coverageReport = {
+ timestamp: new Date().toISOString(),
+ coverage: {
+ statements: `${statements}%`,
+ branches: `${branches}%`,
+ functions: `${functions}%`,
+ lines: `${lines}%`,
+ average: `${avgCoverage}%`
+ },
+ badge: {
+ url: badgeUrl,
+ markdown: ``,
+ html: `
`
+ },
+ details: {
+ statements: totalCoverage.statements,
+ branches: totalCoverage.branches,
+ functions: totalCoverage.functions,
+ lines: totalCoverage.lines
+ }
+ };
+
+ // ่ผธๅบๅฐๆชๆก
+ writeFileSync('coverage-badge.json', JSON.stringify(coverageReport, null, 2));
+
+ // ่ผธๅบๅฐ console
+ console.log('๐ Coverage Report Generated:');
+ console.log(`๐ Statements: ${statements}%`);
+ console.log(`๐ฟ Branches: ${branches}%`);
+ console.log(`โก Functions: ${functions}%`);
+ console.log(`๐ Lines: ${lines}%`);
+ console.log(`๐ฏ Average: ${avgCoverage}%`);
+ console.log(`\n๐ท๏ธ Badge Markdown: ${coverageReport.badge.markdown}`);
+ console.log(`๐ Coverage badge saved to: coverage-badge.json`);
+} catch (error) {
+ console.error('โ Error generating coverage badge:', error.message);
+ process.exit(1);
+}
diff --git a/src/commands/index.ts b/src/commands/index.ts
index fe8d12a..d09c37c 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -1,6 +1,13 @@
import prompts from 'prompts'
import execa from 'execa'
import { projects, commitTypes } from '../helper'
+import type {
+ ProjectConfig,
+ CommitResponse,
+ CommitResult,
+ ProjectType,
+ ParsedCommitResult
+} from '../types'
import picocolors from 'picocolors'
import fs from 'fs'
import { dirname, resolve } from 'node:path'
@@ -9,115 +16,180 @@ import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const rootPath = resolve(__dirname, '../')
-let defaultProjectValue = ''
+// Pure functions for business logic
+export const loadProjectConfig = (filePath: string): ProjectConfig | null => {
+ try {
+ const config = fs.readFileSync(filePath, 'utf8')
+ return JSON.parse(config)
+ } catch (e) {
+ return null
+ }
+}
-try {
- const filePath = resolve(rootPath, 'keep/cz_config.json')
- const config = fs.readFileSync(filePath)
- defaultProjectValue = JSON.parse(config as any).defaultProject
-} catch (e) {
- console.log(picocolors.yellow(picocolors.italic(' ๐ก You can try `cz -i` to choose a default project prefix. ')))
- defaultProjectValue = ''
-}
-
-const typesList = commitTypes.map(type => ({
- title: type.name,
- description: `${type.emoji} ${type.description}`,
- value: type.value,
- emoji: type.emoji
-}))
-
-const step_type = {
- type: 'autocomplete',
- name: 'commit_type',
- message: 'Pick a commit type.',
- choices: typesList,
- fallback: 'No matched type.'
-}
-
-const step_message = {
- type: 'text',
- name: 'commit_message',
- message: (prev: string) => {
- const target = typesList.find(type => type.value === prev)!
- return `${target.emoji} ${target.title}`
- },
- validate: (value: string) => {
- if (!value) {
- return 'Commit message is required.'
- }
- return true
+export const getDefaultProjectValue = (config: ProjectConfig | null): string => {
+ return config?.defaultProject || ''
+}
+
+export const buildCommitTypesList = () => {
+ return commitTypes.map(type => ({
+ title: type.name,
+ description: `${type.emoji} ${type.description}`,
+ value: type.value,
+ emoji: type.emoji
+ }))
+}
+
+export const buildProjectsList = () => {
+ return projects.map(project => ({
+ title: project.name,
+ description: `[${project.prefix}-13845] title`,
+ value: project.value
+ }))
+}
+
+export const findCommitType = (commitTypeValue: string) => {
+ return commitTypes.find(type => type.value === commitTypeValue)
+}
+
+export const findProject = (projectValue: string): ProjectType | undefined => {
+ return projects.find(project => project.value === projectValue)
+}
+
+export const buildCommitTitle = (commitType: string, message: string): string => {
+ const type = findCommitType(commitType)
+ if (!type) {
+ throw new Error(`Invalid commit type: ${commitType}`)
}
+ return `${type.emoji} ${commitType}: ${message}`
}
-const step_description = {
- type: 'text',
- name: 'commit_description',
- message: 'Commit description (optional)',
- initial: '',
- validate: (value: string) => {
- if (value.length > 100) {
- return 'Description is too long.'
- }
- return true
+export const buildFinalCommitMessage = (
+ commitTitle: string,
+ response: CommitResponse,
+ defaultProjectValue: string
+): string => {
+ const { is_jira, is_default_project, project_type, jira_id } = response
+
+ if (!is_jira) {
+ return commitTitle
+ }
+
+ const typeResponse = is_default_project ? defaultProjectValue : project_type
+ const projectType = findProject(typeResponse!)
+
+ if (!projectType) {
+ throw new Error(`Invalid project type: ${typeResponse}`)
+ }
+
+ if (!jira_id) {
+ throw new Error('Jira ID is required when using Jira integration')
}
+
+ return `[${projectType.prefix}-${jira_id}] ${commitTitle}`
}
-const step_is_jira = {
- type: 'confirm',
- name: 'is_jira',
- message: 'Tag Jira issue ?',
- initial: false
-}
-
-const projectsList = projects.map(project => ({
- title: project.name,
- description: `[${project.prefix}-13845] title`,
- value: project.value
-}))
-
-const defaultProject = projectsList.find(project => project.value === defaultProjectValue)!
-
-const step_is_default_project = {
- type: (prev: boolean) => prev ? 'confirm' : null,
- name: 'is_default_project',
- message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
- initial: true
-}
-
-const step_project_type = {
- type: (prev: string, { is_jira }: { is_jira: boolean }) => {
- return is_jira
- ? defaultProject?.value && prev
- ? null
- : 'autocomplete'
- : null
- },
- name: 'project_type',
- message: 'Pick a project type.',
- choices: projectsList,
- initial: 'owlpay',
- fallback: 'No matched project.'
-}
-
-const step_jira_id = {
- type: (prev: boolean) => prev ? 'number' : null,
- name: 'jira_id',
- message: 'Jira issue id',
- onRender () {
- (this as any).msg = picocolors.bgCyan(picocolors.white(' Jira issue ID ')) // TODO: fix type
- },
- validate: (value: number) => {
- if (!value) {
- return 'Jira issue ID is required.'
- }
- return true
+export const buildGitCommands = (commitMessage: string, description?: string): string[] => {
+ if (description) {
+ return ['commit', '-m', commitMessage, '-m', description]
}
+ return ['commit', '-m', commitMessage]
}
-export default async () => {
- let isCanceled = false
- const order = [
+export const parseCommitResult = (stdout: string): ParsedCommitResult => {
+ const branchHashName = stdout.match(/\[(.*?)\]/)?.pop()
+ if (!branchHashName) {
+ throw new Error('Could not parse commit result')
+ }
+
+ const [branchName, branchHash] = branchHashName.split(' ')
+ return { branch: branchName, hash: branchHash }
+}
+
+// Step builders for better organization
+export const buildPromptSteps = (defaultProjectValue: string, projectsList: any[], typesList: any[]) => {
+ const defaultProject = projectsList.find(project => project.value === defaultProjectValue)
+
+ const step_type = {
+ type: 'autocomplete',
+ name: 'commit_type',
+ message: 'Pick a commit type.',
+ choices: typesList,
+ fallback: 'No matched type.'
+ }
+
+ const step_message = {
+ type: 'text',
+ name: 'commit_message',
+ message: (prev: string) => {
+ const target = typesList.find(type => type.value === prev)!
+ return `${target.emoji} ${target.title}`
+ },
+ validate: (value: string) => {
+ if (!value) {
+ return 'Commit message is required.'
+ }
+ return true
+ }
+ }
+
+ const step_description = {
+ type: 'text',
+ name: 'commit_description',
+ message: 'Commit description (optional)',
+ initial: '',
+ validate: (value: string) => {
+ if (value.length > 100) {
+ return 'Description is too long.'
+ }
+ return true
+ }
+ }
+
+ const step_is_jira = {
+ type: 'confirm',
+ name: 'is_jira',
+ message: 'Tag Jira issue ?',
+ initial: false
+ }
+
+ const step_is_default_project = {
+ type: (prev: boolean) => prev ? 'confirm' : null,
+ name: 'is_default_project',
+ message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`,
+ initial: true
+ }
+
+ const step_project_type = {
+ type: (prev: string, { is_jira }: { is_jira: boolean }) => {
+ return is_jira
+ ? defaultProject?.value && prev
+ ? null
+ : 'autocomplete'
+ : null
+ },
+ name: 'project_type',
+ message: 'Pick a project type.',
+ choices: projectsList,
+ initial: 'owlpay',
+ fallback: 'No matched project.'
+ }
+
+ const step_jira_id = {
+ type: (prev: boolean) => prev ? 'number' : null,
+ name: 'jira_id',
+ message: 'Jira issue id',
+ onRender () {
+ (this as any).msg = picocolors.bgCyan(picocolors.white(' Jira issue ID '))
+ },
+ validate: (value: number) => {
+ if (!value) {
+ return 'Jira issue ID is required.'
+ }
+ return true
+ }
+ }
+
+ return [
step_type,
step_message,
step_description,
@@ -125,8 +197,67 @@ export default async () => {
defaultProject?.value ? step_is_default_project : null,
step_project_type,
step_jira_id
- ].filter(Boolean) as any // TODO: fix type
- const response = await prompts(order, {
+ ].filter(Boolean)
+}
+
+// Side effect functions
+export const showConfigMissingWarning = () => {
+ console.log(picocolors.yellow(picocolors.italic(' ๐ก You can try `cz -i` to choose a default project prefix. ')))
+}
+
+export const showCancelMessage = () => {
+ console.log(picocolors.magenta(' commit abort. '))
+}
+
+export const showCommitResult = (result: CommitResult) => {
+ console.log('-----------------------------------------------------------')
+ console.log(`${picocolors.bgGreen(picocolors.bold(' Title '))} ${picocolors.green(result.title)}`)
+ if (result.description) {
+ console.log(`${picocolors.bgGreen(picocolors.bold(' Description '))} ${picocolors.green(result.description)}`)
+ }
+ if (result.hash && result.branch) {
+ console.log(`${picocolors.bgGreen(picocolors.bold(' Commit hash '))} ${picocolors.bold(picocolors.cyan(` ${result.hash} `))} (${picocolors.italic(picocolors.green(result.branch))})`)
+ }
+}
+
+export const showGitOutput = (stdout: string, stderr?: string) => {
+ console.log('-----------------------------------------------------------')
+ console.log(picocolors.dim(stdout))
+ if (stderr && stderr !== '') {
+ console.log('-----------------------------------------------------------')
+ console.log(picocolors.dim(stderr))
+ }
+ console.log('-----------------------------------------------------------')
+}
+
+export const showGitError = (error: any) => {
+ console.log(picocolors.red(error.stderr))
+ if (error.exitCode === 1) {
+ console.log(picocolors.bgRed(' No changes added to commit. '))
+ } else {
+ console.error(error)
+ }
+}
+
+// Main CLI function - orchestrates the flow
+export default async () => {
+ // Load configuration
+ const configPath = resolve(rootPath, 'keep/cz_config.json')
+ const config = loadProjectConfig(configPath)
+ const defaultProjectValue = getDefaultProjectValue(config)
+
+ if (!config) {
+ showConfigMissingWarning()
+ }
+
+ // Build choices
+ const typesList = buildCommitTypesList()
+ const projectsList = buildProjectsList()
+ const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList)
+
+ // Handle user input
+ let isCanceled = false
+ const response = await prompts(steps as any, {
onSubmit: (prompt, answers) => {
if (answers === undefined) {
isCanceled = true
@@ -137,42 +268,35 @@ export default async () => {
isCanceled = true
return false
}
- })
+ }) as CommitResponse
if (isCanceled) {
- console.log(picocolors.magenta(' commit abort. '))
+ showCancelMessage()
return false
}
- const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response
- const type = typesList.find(type => type.value === commit_type)!
- const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}`
- const typeResponse = is_default_project ? defaultProject?.value : project_type
- const projectType = projects.find(project => project.value === typeResponse)!
- const result = is_jira
- ? `[${projectType.prefix}-${jira_id}] ${commitTitle}`
- : commitTitle
-
try {
- const commands = commit_description ? ['commit', '-m', result, '-m', commit_description] : ['commit', '-m', result]
- const commitResult = await execa('git', commands)
- const branchHashName = commitResult.stdout.match(/\[(.*?)\]/)!.pop()!
- const [branchName, branchHash] = branchHashName.split(' ')
- console.log('-----------------------------------------------------------')
- console.log(picocolors.dim(commitResult.stdout))
- if (commitResult.stderr !== '') {
- console.log('-----------------------------------------------------------')
- console.log(picocolors.dim(commitResult.stderr))
- }
- console.log('-----------------------------------------------------------')
- console.log(`${picocolors.bgGreen(picocolors.bold(' Title '))} ${picocolors.green(result)}`)
- if (commit_description) {
- console.log(`${picocolors.bgGreen(picocolors.bold(' Description '))} ${picocolors.green(commit_description)}`)
- }
- console.log(`${picocolors.bgGreen(picocolors.bold(' Commit hash '))} ${picocolors.bold(picocolors.cyan(` ${branchHash} `))} (${picocolors.italic(picocolors.green(branchName))})`)
+ // Build commit message
+ const commitTitle = buildCommitTitle(response.commit_type, response.commit_message)
+ const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue)
+ const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description)
+
+ // Execute git commit
+ const commitResult = await execa('git', gitCommands)
+ const { branch, hash } = parseCommitResult(commitResult.stdout)
+
+ // Show results
+ showGitOutput(commitResult.stdout, commitResult.stderr)
+ showCommitResult({
+ title: finalCommitMessage,
+ description: response.commit_description,
+ hash,
+ branch
+ })
+
+ return true
} catch (error: any) {
- console.log(picocolors.red(error.stderr))
- if (error.exitCode === 1) console.log(picocolors.bgRed(' No changes added to commit. '))
- else console.error(error)
+ showGitError(error)
+ return false
}
}
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 3664524..7f065c8 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -48,7 +48,7 @@ export default async () => {
const filePath = resolve(rootPath, 'keep/cz_config.json')
fs.writeFileSync(
filePath,
- `${JSON.stringify({ defaultProject: set_default_project } || {}, null, 2)}`,
+ `${JSON.stringify({ defaultProject: set_default_project }, null, 2)}`
)
console.log(picocolors.green(` default project set: ${set_default_project} `))
} catch (error) {
diff --git a/src/helper/commit-types.ts b/src/helper/commit-types.ts
index 0baaef9..f304b1f 100644
--- a/src/helper/commit-types.ts
+++ b/src/helper/commit-types.ts
@@ -1,4 +1,8 @@
-export default [
+import type { CommitType } from '../types'
+
+export type { CommitType }
+
+export default [
{
name: 'chore',
emoji: '๐งน',
@@ -69,7 +73,7 @@ export default [
name: 'storybook',
emoji: '๐',
description: 'New storybook',
- value: 'story'
+ value: 'storybook'
},
{
name: 'revert',
diff --git a/src/helper/projects.ts b/src/helper/projects.ts
index 38bc7ed..30ff63b 100644
--- a/src/helper/projects.ts
+++ b/src/helper/projects.ts
@@ -1,10 +1,8 @@
-interface Project {
- name: string
- prefix: string
- value: string
-}
+import type { ProjectType } from '../types'
-export default [
+export type { ProjectType }
+
+export default [
{
name: 'OwlPay',
prefix: 'OWLPAY',
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..f7d8ff8
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,74 @@
+// Configuration related types
+export interface ProjectConfig {
+ defaultProject?: string
+}
+
+// CLI response types
+export interface CommitResponse {
+ commit_type: string
+ commit_message: string
+ commit_description?: string
+ is_jira: boolean
+ is_default_project?: boolean
+ project_type?: string
+ jira_id?: number
+}
+
+// Result display types
+export interface CommitResult {
+ title: string
+ description?: string
+ hash?: string
+ branch?: string
+}
+
+// Project structure types
+export interface ProjectType {
+ name: string
+ prefix: string
+ value: string
+}
+
+// Commit type structure
+export interface CommitType {
+ name: string
+ emoji: string
+ description: string
+ value: string
+}
+
+// Git command result types
+export interface GitCommitResult {
+ stdout: string
+ stderr?: string
+}
+
+export interface ParsedCommitResult {
+ branch: string
+ hash: string
+}
+
+// Error types
+export interface GitError extends Error {
+ stderr?: string
+ exitCode?: number
+}
+
+// Prompt step types
+export interface PromptChoice {
+ title: string
+ description: string
+ value: string
+ emoji?: string
+}
+
+export interface PromptStep {
+ type: string
+ name: string
+ message: string | ((prev: any) => string)
+ choices?: PromptChoice[]
+ initial?: any
+ validate?: (value: any) => boolean | string
+ fallback?: string
+ onRender?: () => void
+}
\ No newline at end of file
diff --git a/tests/commands/cli-module-loading.test.ts b/tests/commands/cli-module-loading.test.ts
new file mode 100644
index 0000000..e6c4749
--- /dev/null
+++ b/tests/commands/cli-module-loading.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import fs from 'fs'
+
+// Mock dependencies before importing the module
+vi.mock('fs')
+vi.mock('prompts')
+vi.mock('execa')
+vi.mock('picocolors', () => ({
+ default: {
+ yellow: (text: string) => text,
+ italic: (text: string) => text,
+ magenta: (text: string) => text,
+ bgGreen: (text: string) => text,
+ bold: (text: string) => text,
+ green: (text: string) => text,
+ cyan: (text: string) => text,
+ dim: (text: string) => text,
+ red: (text: string) => text,
+ bgRed: (text: string) => text,
+ bgCyan: (text: string) => text,
+ white: (text: string) => text
+ }
+}))
+
+const mockFs = vi.mocked(fs)
+
+describe('CLI module loading behavior after refactoring', () => {
+ const mockConsoleLog = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ console.log = mockConsoleLog
+ })
+
+ it('should not show warning during module load (warning moved to function execution)', async () => {
+ // Mock config file read error
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ // Import the module - this should NOT trigger any console.log
+ await import('../../src/commands/index')
+
+ expect(mockConsoleLog).not.toHaveBeenCalled()
+ })
+
+ it('should load module successfully when config file exists', async () => {
+ // Mock successful config file read
+ mockFs.readFileSync = vi.fn().mockReturnValue(
+ JSON.stringify({ defaultProject: 'owlpay' })
+ )
+
+ // Import the module
+ await import('../../src/commands/index')
+
+ // No console.log should be called during module loading
+ expect(mockConsoleLog).not.toHaveBeenCalled()
+ })
+})
diff --git a/tests/commands/cli-validation.test.ts b/tests/commands/cli-validation.test.ts
new file mode 100644
index 0000000..9074660
--- /dev/null
+++ b/tests/commands/cli-validation.test.ts
@@ -0,0 +1,192 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import fs from 'fs'
+import prompts from 'prompts'
+import execa from 'execa'
+
+// Mock dependencies
+vi.mock('fs')
+vi.mock('prompts')
+vi.mock('execa')
+vi.mock('picocolors', () => ({
+ default: {
+ yellow: (text: string) => text,
+ italic: (text: string) => text,
+ magenta: (text: string) => text,
+ bgGreen: (text: string) => text,
+ bold: (text: string) => text,
+ green: (text: string) => text,
+ cyan: (text: string) => text,
+ dim: (text: string) => text,
+ red: (text: string) => text,
+ bgRed: (text: string) => text,
+ bgCyan: (text: string) => text,
+ white: (text: string) => text
+ }
+}))
+
+const mockFs = vi.mocked(fs)
+const mockPrompts = vi.mocked(prompts)
+const mockExeca = vi.mocked(execa)
+
+describe('CLI validation and edge cases', () => {
+ const mockConsoleLog = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ console.log = mockConsoleLog
+
+ // Default mock for file not found
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ describe('step type conditions', () => {
+ it('should handle step_is_default_project conditional type', async () => {
+ mockFs.readFileSync = vi.fn().mockReturnValue(
+ JSON.stringify({ defaultProject: 'owlpay' })
+ )
+
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test',
+ commit_description: '',
+ is_jira: true,
+ is_default_project: false, // This should trigger the conditional logic
+ project_type: 'owlnest',
+ jira_id: 123
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] [OW-123] ๐ก feat: test',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[OW-123] ๐ก feat: test'
+ ])
+ })
+
+ it('should handle step_project_type conditional logic when no default project', async () => {
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test',
+ commit_description: '',
+ is_jira: true,
+ project_type: 'market',
+ jira_id: 456
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main def456] [MAR-456] ๐ก feat: test',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[MAR-456] ๐ก feat: test'
+ ])
+ })
+
+ it('should handle onSubmit with undefined answers', async () => {
+ mockPrompts.mockImplementation((questions, options) => {
+ if (options?.onSubmit) {
+ // Simulate onSubmit being called with undefined answers
+ const shouldStop = options.onSubmit({} as any, undefined, {} as any)
+ expect(shouldStop).toBe(true)
+ }
+ return Promise.resolve({})
+ })
+
+ const { default: cli } = await import('../../src/commands/index')
+ const result = await cli()
+
+ expect(result).toBe(false)
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('commit abort')
+ )
+ })
+ })
+
+ describe('edge cases', () => {
+ it('should handle no default project with Jira flow', async () => {
+ // No config file, so no default project
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test',
+ commit_description: '',
+ is_jira: true,
+ project_type: 'paynow',
+ jira_id: 999
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main xyz999] [PN-999] ๐ก feat: test',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[PN-999] ๐ก feat: test'
+ ])
+ })
+
+ it('should handle all project types in Jira flow', async () => {
+ const projects = [
+ { value: 'owlpay', prefix: 'OWLPAY' },
+ { value: 'owlnest', prefix: 'OW' },
+ { value: 'market', prefix: 'MAR' },
+ { value: 'paynow', prefix: 'PN' },
+ { value: 'wallet-pro', prefix: 'WP' }
+ ]
+
+ for (const project of projects) {
+ vi.clearAllMocks()
+
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test',
+ commit_description: '',
+ is_jira: true,
+ project_type: project.value,
+ jira_id: 123
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: `[main abc123] [${project.prefix}-123] ๐ก feat: test`,
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', `[${project.prefix}-123] ๐ก feat: test`
+ ])
+ }
+ })
+ })
+})
diff --git a/tests/commands/cli.test.ts b/tests/commands/cli.test.ts
new file mode 100644
index 0000000..8d021fa
--- /dev/null
+++ b/tests/commands/cli.test.ts
@@ -0,0 +1,362 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import fs from 'fs'
+import prompts from 'prompts'
+import execa from 'execa'
+
+// Mock dependencies
+vi.mock('fs')
+vi.mock('prompts')
+vi.mock('execa')
+vi.mock('picocolors', () => ({
+ default: {
+ yellow: (text: string) => text,
+ italic: (text: string) => text,
+ magenta: (text: string) => text,
+ bgGreen: (text: string) => text,
+ bold: (text: string) => text,
+ green: (text: string) => text,
+ cyan: (text: string) => text,
+ dim: (text: string) => text,
+ red: (text: string) => text,
+ bgRed: (text: string) => text,
+ bgCyan: (text: string) => text,
+ white: (text: string) => text
+ }
+}))
+
+const mockFs = vi.mocked(fs)
+const mockPrompts = vi.mocked(prompts)
+const mockExeca = vi.mocked(execa)
+
+// Custom error type for git errors
+interface GitError extends Error {
+ stderr?: string
+ exitCode?: number
+}
+
+describe('CLI main command', () => {
+ const mockConsoleLog = vi.fn()
+ const mockConsoleError = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ console.log = mockConsoleLog
+ console.error = mockConsoleError
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ describe('config file handling', () => {
+ it('should load default project when config exists', async () => {
+ // Mock config file exists
+ mockFs.readFileSync = vi.fn().mockReturnValue(
+ JSON.stringify({ defaultProject: 'owlpay' })
+ )
+
+ // Mock successful prompts flow
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ // Mock successful git commit
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] ๐ก feat: test feature'
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockFs.readFileSync).toHaveBeenCalled()
+ })
+
+ it('should handle missing config file gracefully', async () => {
+ // Mock config file read error
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ // Mock successful prompts flow
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ // Mock successful git commit
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] ๐ก feat: test feature'
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ // The warning message is shown during module loading, not during execution
+ // So we just verify the function completed successfully
+ expect(mockExeca).toHaveBeenCalledWith('git', ['commit', '-m', '๐ก feat: test feature'])
+ })
+ })
+
+ describe('commit flow without Jira', () => {
+ beforeEach(() => {
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+ })
+
+ it('should handle basic commit without description', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'add new feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] ๐ก feat: add new feature',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', ['commit', '-m', '๐ก feat: add new feature'])
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('๐ก feat: add new feature')
+ )
+ })
+
+ it('should handle commit with description', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'fix',
+ commit_message: 'fix bug',
+ commit_description: 'Fixed critical bug in authentication',
+ is_jira: false
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main def456] ๐ fix: fix bug',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '๐ fix: fix bug', '-m', 'Fixed critical bug in authentication'
+ ])
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('Fixed critical bug in authentication')
+ )
+ })
+
+ it('should handle all commit types', async () => {
+ const commitTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'hotfix', 'release', 'storybook', 'revert']
+
+ for (const type of commitTypes) {
+ vi.clearAllMocks()
+
+ mockPrompts.mockResolvedValue({
+ commit_type: type,
+ commit_message: `test ${type}`,
+ commit_description: '',
+ is_jira: false
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: `[main abc123] ${type}: test ${type}`,
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', expect.stringContaining(`${type}: test ${type}`)
+ ])
+ }
+ })
+ })
+
+ describe('commit flow with Jira', () => {
+ beforeEach(() => {
+ mockFs.readFileSync = vi.fn().mockReturnValue(
+ JSON.stringify({ defaultProject: 'owlpay' })
+ )
+ })
+
+ it('should handle Jira commit with default project', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'new feature',
+ commit_description: '',
+ is_jira: true,
+ is_default_project: true,
+ jira_id: 12345
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] [OWLPAY-12345] ๐ก feat: new feature',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[OWLPAY-12345] ๐ก feat: new feature'
+ ])
+ })
+
+ it('should handle Jira commit with custom project', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'fix',
+ commit_message: 'bug fix',
+ commit_description: '',
+ is_jira: true,
+ is_default_project: false,
+ project_type: 'owlnest',
+ jira_id: 98765
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main def456] [OW-98765] ๐ fix: bug fix',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[OW-98765] ๐ fix: bug fix'
+ ])
+ })
+
+ it('should handle Jira commit with description', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'new feature',
+ commit_description: 'Added user authentication',
+ is_jira: true,
+ is_default_project: true,
+ jira_id: 11111
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main ghi789] [OWLPAY-11111] ๐ก feat: new feature',
+ stderr: ''
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockExeca).toHaveBeenCalledWith('git', [
+ 'commit', '-m', '[OWLPAY-11111] ๐ก feat: new feature', '-m', 'Added user authentication'
+ ])
+ })
+ })
+
+ describe('error handling', () => {
+ beforeEach(() => {
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+ })
+
+ it('should handle user cancellation', async () => {
+ mockPrompts.mockImplementation((questions, options) => {
+ if (options?.onCancel) {
+ options.onCancel({} as any, {} as any)
+ }
+ return Promise.resolve({})
+ })
+
+ const { default: cli } = await import('../../src/commands/index')
+ const result = await cli()
+
+ expect(result).toBe(false)
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('commit abort')
+ )
+ })
+
+ it('should handle git commit errors', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ const gitError: GitError = new Error('Git error')
+ gitError.stderr = 'fatal: not a git repository'
+ gitError.exitCode = 128
+ mockExeca.mockRejectedValue(gitError)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('fatal: not a git repository')
+ )
+ expect(mockConsoleError).toHaveBeenCalledWith(gitError)
+ })
+
+ it('should handle "no changes" git error', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ const gitError: GitError = new Error('No changes')
+ gitError.stderr = 'nothing to commit'
+ gitError.exitCode = 1
+ mockExeca.mockRejectedValue(gitError)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('No changes added to commit')
+ )
+ })
+
+ it('should handle git stderr output', async () => {
+ mockPrompts.mockResolvedValue({
+ commit_type: 'feat',
+ commit_message: 'test feature',
+ commit_description: '',
+ is_jira: false
+ })
+
+ mockExeca.mockResolvedValue({
+ stdout: '[main abc123] ๐ก feat: test feature',
+ stderr: 'warning: some git warning'
+ } as any)
+
+ const { default: cli } = await import('../../src/commands/index')
+ await cli()
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('warning: some git warning')
+ )
+ })
+ })
+
+ describe('validation functions', () => {
+ it('should test commit message validation logic', async () => {
+ // We test validation indirectly by checking the prompts configuration
+ const { default: cli } = await import('../../src/commands/index')
+
+ // This is more of a structural test to ensure the function exists
+ expect(typeof cli).toBe('function')
+ })
+ })
+})
diff --git a/tests/commands/index-refactored.test.ts b/tests/commands/index-refactored.test.ts
new file mode 100644
index 0000000..809e598
--- /dev/null
+++ b/tests/commands/index-refactored.test.ts
@@ -0,0 +1,407 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import fs from 'fs'
+import type { CommitResponse, ProjectConfig, CommitResult } from '../../src/types'
+import {
+ loadProjectConfig,
+ getDefaultProjectValue,
+ buildCommitTypesList,
+ buildProjectsList,
+ findCommitType,
+ findProject,
+ buildCommitTitle,
+ buildFinalCommitMessage,
+ buildGitCommands,
+ parseCommitResult,
+ buildPromptSteps,
+ showConfigMissingWarning,
+ showCancelMessage,
+ showCommitResult,
+ showGitOutput,
+ showGitError
+} from '../../src/commands/index'
+
+// Mock dependencies
+vi.mock('fs')
+vi.mock('picocolors', () => ({
+ default: {
+ yellow: (text: string) => text,
+ italic: (text: string) => text,
+ magenta: (text: string) => text,
+ bgGreen: (text: string) => text,
+ bold: (text: string) => text,
+ green: (text: string) => text,
+ cyan: (text: string) => text,
+ dim: (text: string) => text,
+ red: (text: string) => text,
+ bgRed: (text: string) => text,
+ bgCyan: (text: string) => text,
+ white: (text: string) => text
+ }
+}))
+
+const mockFs = vi.mocked(fs)
+
+describe('Refactored CLI Functions (TDD Style)', () => {
+ const mockConsoleLog = vi.fn()
+ const mockConsoleError = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ console.log = mockConsoleLog
+ console.error = mockConsoleError
+ })
+
+ describe('Configuration Loading Functions', () => {
+ describe('loadProjectConfig', () => {
+ it('should load valid config file', () => {
+ const mockConfig = { defaultProject: 'owlpay' }
+ mockFs.readFileSync = vi.fn().mockReturnValue(JSON.stringify(mockConfig))
+
+ const result = loadProjectConfig('/path/to/config.json')
+
+ expect(result).toEqual(mockConfig)
+ expect(mockFs.readFileSync).toHaveBeenCalledWith('/path/to/config.json', 'utf8')
+ })
+
+ it('should return null for invalid config file', () => {
+ mockFs.readFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('File not found')
+ })
+
+ const result = loadProjectConfig('/path/to/config.json')
+
+ expect(result).toBeNull()
+ })
+
+ it('should return null for invalid JSON', () => {
+ mockFs.readFileSync = vi.fn().mockReturnValue('invalid json')
+
+ const result = loadProjectConfig('/path/to/config.json')
+
+ expect(result).toBeNull()
+ })
+ })
+
+ describe('getDefaultProjectValue', () => {
+ it('should return default project when config exists', () => {
+ const config: ProjectConfig = { defaultProject: 'owlpay' }
+ const result = getDefaultProjectValue(config)
+ expect(result).toBe('owlpay')
+ })
+
+ it('should return empty string when config is null', () => {
+ const result = getDefaultProjectValue(null)
+ expect(result).toBe('')
+ })
+
+ it('should return empty string when defaultProject is undefined', () => {
+ const config: ProjectConfig = {}
+ const result = getDefaultProjectValue(config)
+ expect(result).toBe('')
+ })
+ })
+ })
+
+ describe('List Building Functions', () => {
+ describe('buildCommitTypesList', () => {
+ it('should build commit types list correctly', () => {
+ const result = buildCommitTypesList()
+
+ expect(result).toHaveLength(13)
+ expect(result[0]).toMatchObject({
+ title: 'chore',
+ description: '๐งน Build process or auxiliary tool changes',
+ value: 'chore',
+ emoji: '๐งน'
+ })
+ })
+ })
+
+ describe('buildProjectsList', () => {
+ it('should build projects list correctly', () => {
+ const result = buildProjectsList()
+
+ expect(result).toHaveLength(5)
+ expect(result[0]).toMatchObject({
+ title: 'OwlPay',
+ description: '[OWLPAY-13845] title',
+ value: 'owlpay'
+ })
+ })
+ })
+ })
+
+ describe('Finder Functions', () => {
+ describe('findCommitType', () => {
+ it('should find existing commit type', () => {
+ const result = findCommitType('feat')
+ expect(result).toMatchObject({
+ name: 'feat',
+ emoji: '๐ก',
+ value: 'feat'
+ })
+ })
+
+ it('should return undefined for non-existing commit type', () => {
+ const result = findCommitType('nonexistent')
+ expect(result).toBeUndefined()
+ })
+ })
+
+ describe('findProject', () => {
+ it('should find existing project', () => {
+ const result = findProject('owlpay')
+ expect(result).toMatchObject({
+ name: 'OwlPay',
+ prefix: 'OWLPAY',
+ value: 'owlpay'
+ })
+ })
+
+ it('should return undefined for non-existing project', () => {
+ const result = findProject('nonexistent')
+ expect(result).toBeUndefined()
+ })
+ })
+ })
+
+ describe('Commit Message Building Functions', () => {
+ describe('buildCommitTitle', () => {
+ it('should build commit title correctly', () => {
+ const result = buildCommitTitle('feat', 'add new feature')
+ expect(result).toBe('๐ก feat: add new feature')
+ })
+
+ it('should throw error for invalid commit type', () => {
+ expect(() => buildCommitTitle('invalid', 'message')).toThrow('Invalid commit type: invalid')
+ })
+ })
+
+ describe('buildFinalCommitMessage', () => {
+ it('should return plain commit title when not using Jira', () => {
+ const response: CommitResponse = {
+ commit_type: 'feat',
+ commit_message: 'test',
+ is_jira: false
+ }
+
+ const result = buildFinalCommitMessage('๐ก feat: test', response, 'owlpay')
+ expect(result).toBe('๐ก feat: test')
+ })
+
+ it('should build Jira commit with default project', () => {
+ const response: CommitResponse = {
+ commit_type: 'feat',
+ commit_message: 'test',
+ is_jira: true,
+ is_default_project: true,
+ jira_id: 12345
+ }
+
+ const result = buildFinalCommitMessage('๐ก feat: test', response, 'owlpay')
+ expect(result).toBe('[OWLPAY-12345] ๐ก feat: test')
+ })
+
+ it('should build Jira commit with selected project', () => {
+ const response: CommitResponse = {
+ commit_type: 'feat',
+ commit_message: 'test',
+ is_jira: true,
+ is_default_project: false,
+ project_type: 'owlnest',
+ jira_id: 67890
+ }
+
+ const result = buildFinalCommitMessage('๐ก feat: test', response, 'owlpay')
+ expect(result).toBe('[OW-67890] ๐ก feat: test')
+ })
+
+ it('should throw error for invalid project type', () => {
+ const response: CommitResponse = {
+ commit_type: 'feat',
+ commit_message: 'test',
+ is_jira: true,
+ is_default_project: false,
+ project_type: 'invalid'
+ }
+
+ expect(() => buildFinalCommitMessage('๐ก feat: test', response, 'owlpay'))
+ .toThrow('Invalid project type: invalid')
+ })
+
+ it('should throw error when Jira ID is missing', () => {
+ const response: CommitResponse = {
+ commit_type: 'feat',
+ commit_message: 'test',
+ is_jira: true,
+ is_default_project: true
+ }
+
+ expect(() => buildFinalCommitMessage('๐ก feat: test', response, 'owlpay'))
+ .toThrow('Jira ID is required when using Jira integration')
+ })
+ })
+
+ describe('buildGitCommands', () => {
+ it('should build basic git command without description', () => {
+ const result = buildGitCommands('feat: add feature')
+ expect(result).toEqual(['commit', '-m', 'feat: add feature'])
+ })
+
+ it('should build git command with description', () => {
+ const result = buildGitCommands('feat: add feature', 'detailed description')
+ expect(result).toEqual(['commit', '-m', 'feat: add feature', '-m', 'detailed description'])
+ })
+
+ it('should handle empty description as falsy value', () => {
+ const result = buildGitCommands('feat: add feature', '')
+ // Empty string is falsy, so it should NOT include the second -m flag
+ expect(result).toEqual(['commit', '-m', 'feat: add feature'])
+ })
+ })
+ })
+
+ describe('Result Parsing Functions', () => {
+ describe('parseCommitResult', () => {
+ it('should parse git commit output correctly', () => {
+ const stdout = '[main abc123] feat: add new feature'
+ const result = parseCommitResult(stdout)
+
+ expect(result).toEqual({
+ branch: 'main',
+ hash: 'abc123'
+ })
+ })
+
+ it('should handle different branch names', () => {
+ const stdout = '[feature/test-branch def456] fix: bug fix'
+ const result = parseCommitResult(stdout)
+
+ expect(result).toEqual({
+ branch: 'feature/test-branch',
+ hash: 'def456'
+ })
+ })
+
+ it('should throw error for invalid output format', () => {
+ const stdout = 'invalid git output'
+ expect(() => parseCommitResult(stdout)).toThrow('Could not parse commit result')
+ })
+ })
+ })
+
+ describe('Prompt Step Building Functions', () => {
+ describe('buildPromptSteps', () => {
+ it('should build all steps when default project exists', () => {
+ const projectsList = buildProjectsList()
+ const typesList = buildCommitTypesList()
+
+ const steps = buildPromptSteps('owlpay', projectsList, typesList)
+
+ expect(steps).toHaveLength(7) // All steps including default project step
+ expect(steps[0]).toMatchObject({
+ type: 'autocomplete',
+ name: 'commit_type'
+ })
+ })
+
+ it('should skip default project step when no default project', () => {
+ const projectsList = buildProjectsList()
+ const typesList = buildCommitTypesList()
+
+ const steps = buildPromptSteps('', projectsList, typesList)
+
+ expect(steps).toHaveLength(6) // Missing default project step
+ })
+ })
+ })
+
+ describe('Display Functions', () => {
+ describe('showConfigMissingWarning', () => {
+ it('should display warning message', () => {
+ showConfigMissingWarning()
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('You can try `cz -i` to choose a default project prefix')
+ )
+ })
+ })
+
+ describe('showCancelMessage', () => {
+ it('should display cancel message', () => {
+ showCancelMessage()
+ expect(mockConsoleLog).toHaveBeenCalledWith(' commit abort. ')
+ })
+ })
+
+ describe('showCommitResult', () => {
+ it('should display basic commit result', () => {
+ const result: CommitResult = {
+ title: 'feat: new feature'
+ }
+
+ showCommitResult(result)
+
+ expect(mockConsoleLog).toHaveBeenCalledWith('-----------------------------------------------------------')
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('feat: new feature'))
+ })
+
+ it('should display commit result with description and hash', () => {
+ const result: CommitResult = {
+ title: 'feat: new feature',
+ description: 'detailed description',
+ hash: 'abc123',
+ branch: 'main'
+ }
+
+ showCommitResult(result)
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('feat: new feature'))
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('detailed description'))
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('abc123'))
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('main'))
+ })
+ })
+
+ describe('showGitOutput', () => {
+ it('should display git stdout only', () => {
+ showGitOutput('git output')
+
+ expect(mockConsoleLog).toHaveBeenCalledWith('-----------------------------------------------------------')
+ expect(mockConsoleLog).toHaveBeenCalledWith('git output')
+ })
+
+ it('should display git stdout and stderr', () => {
+ showGitOutput('git output', 'git error')
+
+ expect(mockConsoleLog).toHaveBeenCalledWith('git output')
+ expect(mockConsoleLog).toHaveBeenCalledWith('git error')
+ })
+ })
+
+ describe('showGitError', () => {
+ it('should display git error with no changes message', () => {
+ const error = {
+ stderr: 'git error',
+ exitCode: 1
+ }
+
+ showGitError(error)
+
+ expect(mockConsoleLog).toHaveBeenCalledWith('git error')
+ expect(mockConsoleLog).toHaveBeenCalledWith(' No changes added to commit. ')
+ })
+
+ it('should display git error without special message', () => {
+ const error = {
+ stderr: 'some other error',
+ exitCode: 2
+ }
+
+ showGitError(error)
+
+ expect(mockConsoleLog).toHaveBeenCalledWith('some other error')
+ expect(mockConsoleError).toHaveBeenCalledWith(error)
+ })
+ })
+ })
+})
diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts
new file mode 100644
index 0000000..9bf47ba
--- /dev/null
+++ b/tests/commands/init.test.ts
@@ -0,0 +1,188 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import fs from 'fs'
+import prompts from 'prompts'
+
+// Mock dependencies
+vi.mock('fs')
+vi.mock('prompts')
+vi.mock('picocolors', () => ({
+ default: {
+ green: (text: string) => text,
+ bgRed: (text: string) => text,
+ white: (text: string) => text,
+ magenta: (text: string) => text
+ }
+}))
+
+const mockFs = vi.mocked(fs)
+const mockPrompts = vi.mocked(prompts)
+
+describe('init command', () => {
+ const mockConsoleLog = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ console.log = mockConsoleLog
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('should save selected project to config file', async () => {
+ // Mock prompts response
+ mockPrompts.mockResolvedValue({
+ set_default_project: 'owlpay'
+ })
+
+ // Mock fs.writeFileSync
+ mockFs.writeFileSync = vi.fn()
+
+ const { default: init } = await import('../../src/commands/init')
+
+ await init()
+
+ expect(mockPrompts).toHaveBeenCalled()
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
+ expect.stringContaining('keep/cz_config.json'),
+ JSON.stringify({ defaultProject: 'owlpay' }, null, 2)
+ )
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('default project set: owlpay')
+ )
+ })
+
+ it('should handle user cancellation', async () => {
+ // Mock prompts cancellation
+ mockPrompts.mockImplementation((questions, options) => {
+ if (options?.onCancel) {
+ options.onCancel({} as any, {} as any)
+ }
+ return Promise.resolve({})
+ })
+
+ const { default: init } = await import('../../src/commands/init')
+
+ const result = await init()
+
+ expect(result).toBe(false)
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('init abort')
+ )
+ })
+
+ it('should handle user cancellation via onSubmit with undefined', async () => {
+ // Mock prompts with onSubmit returning undefined
+ mockPrompts.mockImplementation((questions, options) => {
+ if (options?.onSubmit) {
+ // onSubmit should return true to stop, and answers being undefined triggers cancellation
+ options.onSubmit({} as any, undefined, {} as any)
+ }
+ return Promise.resolve({})
+ })
+
+ const { default: init } = await import('../../src/commands/init')
+
+ const result = await init()
+
+ expect(result).toBe(false)
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('init abort')
+ )
+ })
+
+ it('should handle file write errors', async () => {
+ // Mock prompts response
+ mockPrompts.mockResolvedValue({
+ set_default_project: 'owlpay'
+ })
+
+ // Mock fs.writeFileSync to throw error
+ mockFs.writeFileSync = vi.fn().mockImplementation(() => {
+ throw new Error('Permission denied')
+ })
+
+ const { default: init } = await import('../../src/commands/init')
+
+ await init()
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining('init Fail')
+ )
+ })
+
+ it('should present correct project choices', async () => {
+ mockPrompts.mockResolvedValue({
+ set_default_project: 'owlnest'
+ })
+ mockFs.writeFileSync = vi.fn()
+
+ const { default: init } = await import('../../src/commands/init')
+
+ await init()
+
+ expect(mockPrompts).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ expect.objectContaining({
+ type: 'autocomplete',
+ name: 'set_default_project',
+ message: 'Set default project prefix.',
+ choices: expect.arrayContaining([
+ expect.objectContaining({
+ title: 'OwlPay',
+ value: 'owlpay'
+ }),
+ expect.objectContaining({
+ title: 'OwlNest',
+ value: 'owlnest'
+ })
+ ])
+ })
+ ]),
+ expect.any(Object)
+ )
+ })
+
+ it('should test all available projects in choices', async () => {
+ mockPrompts.mockResolvedValue({
+ set_default_project: 'market'
+ })
+ mockFs.writeFileSync = vi.fn()
+
+ const { default: init } = await import('../../src/commands/init')
+
+ await init()
+
+ const callArgs = mockPrompts.mock.calls[0]
+ const stepType = callArgs[0][0]
+ expect(stepType.choices).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ title: 'OwlPay', value: 'owlpay' }),
+ expect.objectContaining({ title: 'OwlNest', value: 'owlnest' }),
+ expect.objectContaining({ title: 'Market', value: 'market' }),
+ expect.objectContaining({ title: 'PayNow', value: 'paynow' }),
+ expect.objectContaining({ title: 'Wallet Pro', value: 'wallet-pro' })
+ ])
+ )
+ })
+
+ it('should handle successful completion with different projects', async () => {
+ const projects = ['owlpay', 'owlnest', 'market', 'paynow', 'wallet-pro']
+
+ for (const project of projects) {
+ vi.clearAllMocks()
+
+ mockPrompts.mockResolvedValue({
+ set_default_project: project
+ })
+ mockFs.writeFileSync = vi.fn()
+
+ const { default: init } = await import('../../src/commands/init')
+ await init()
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(
+ expect.stringContaining(`default project set: ${project}`)
+ )
+ }
+ })
+})
diff --git a/tests/commands/where.test.ts b/tests/commands/where.test.ts
new file mode 100644
index 0000000..9c5c586
--- /dev/null
+++ b/tests/commands/where.test.ts
@@ -0,0 +1,37 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+
+// Mock console.log to capture output
+const mockConsoleLog = vi.fn()
+console.log = mockConsoleLog
+
+describe('where command', () => {
+ beforeEach(() => {
+ mockConsoleLog.mockClear()
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('should show config directory path', async () => {
+ // We need to dynamically import to ensure mocks are in place
+ const { default: where } = await import('../../src/commands/where')
+
+ where()
+
+ expect(mockConsoleLog).toHaveBeenCalled()
+ const output = mockConsoleLog.mock.calls[0][0]
+ expect(output).toContain('Your config will be saved in')
+ expect(output).toContain('src/commands')
+ })
+
+ it('should display a path ending with a period', async () => {
+ const { default: where } = await import('../../src/commands/where')
+
+ where()
+
+ expect(mockConsoleLog).toHaveBeenCalled()
+ const output = mockConsoleLog.mock.calls[0][0]
+ expect(output).toMatch(/.*\.$/)
+ })
+})
diff --git a/tests/helper/commit-types.test.ts b/tests/helper/commit-types.test.ts
new file mode 100644
index 0000000..1a7c8a2
--- /dev/null
+++ b/tests/helper/commit-types.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest'
+import commitTypes from '../../src/helper/commit-types'
+
+describe('commit-types', () => {
+ it('should export an array of commit types', () => {
+ expect(Array.isArray(commitTypes)).toBe(true)
+ expect(commitTypes.length).toBeGreaterThan(0)
+ })
+
+ it('should have all required properties for each commit type', () => {
+ commitTypes.forEach(type => {
+ expect(type).toHaveProperty('name')
+ expect(type).toHaveProperty('emoji')
+ expect(type).toHaveProperty('description')
+ expect(type).toHaveProperty('value')
+
+ expect(typeof type.name).toBe('string')
+ expect(typeof type.emoji).toBe('string')
+ expect(typeof type.description).toBe('string')
+ expect(typeof type.value).toBe('string')
+ })
+ })
+
+ it('should contain expected commit types', () => {
+ const expectedTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'hotfix', 'release', 'storybook', 'revert']
+ const actualTypes = commitTypes.map(type => type.value)
+
+ expectedTypes.forEach(expectedType => {
+ expect(actualTypes).toContain(expectedType)
+ })
+ })
+
+ it('should have unique values', () => {
+ const values = commitTypes.map(type => type.value)
+ const uniqueValues = [...new Set(values)]
+ expect(values.length).toBe(uniqueValues.length)
+ })
+
+ it('should have non-empty descriptions', () => {
+ commitTypes.forEach(type => {
+ expect(type.description.trim()).not.toBe('')
+ })
+ })
+
+ it('should have emoji characters', () => {
+ commitTypes.forEach(type => {
+ expect(type.emoji).toMatch(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u)
+ })
+ })
+})
diff --git a/tests/helper/projects.test.ts b/tests/helper/projects.test.ts
new file mode 100644
index 0000000..f755652
--- /dev/null
+++ b/tests/helper/projects.test.ts
@@ -0,0 +1,62 @@
+import { describe, it, expect } from 'vitest'
+import projects from '../../src/helper/projects'
+
+describe('projects', () => {
+ it('should export an array of projects', () => {
+ expect(Array.isArray(projects)).toBe(true)
+ expect(projects.length).toBeGreaterThan(0)
+ })
+
+ it('should have all required properties for each project', () => {
+ projects.forEach(project => {
+ expect(project).toHaveProperty('name')
+ expect(project).toHaveProperty('prefix')
+ expect(project).toHaveProperty('value')
+
+ expect(typeof project.name).toBe('string')
+ expect(typeof project.prefix).toBe('string')
+ expect(typeof project.value).toBe('string')
+ })
+ })
+
+ it('should contain expected projects', () => {
+ const expectedProjects = ['owlpay', 'owlnest', 'market', 'paynow', 'wallet-pro']
+ const actualProjects = projects.map(project => project.value)
+
+ expectedProjects.forEach(expectedProject => {
+ expect(actualProjects).toContain(expectedProject)
+ })
+ })
+
+ it('should have unique values', () => {
+ const values = projects.map(project => project.value)
+ const uniqueValues = [...new Set(values)]
+ expect(values.length).toBe(uniqueValues.length)
+ })
+
+ it('should have unique prefixes', () => {
+ const prefixes = projects.map(project => project.prefix)
+ const uniquePrefixes = [...new Set(prefixes)]
+ expect(prefixes.length).toBe(uniquePrefixes.length)
+ })
+
+ it('should have non-empty names and prefixes', () => {
+ projects.forEach(project => {
+ expect(project.name.trim()).not.toBe('')
+ expect(project.prefix.trim()).not.toBe('')
+ expect(project.value.trim()).not.toBe('')
+ })
+ })
+
+ it('should have uppercase prefixes', () => {
+ projects.forEach(project => {
+ expect(project.prefix).toBe(project.prefix.toUpperCase())
+ })
+ })
+
+ it('should have lowercase values', () => {
+ projects.forEach(project => {
+ expect(project.value).toBe(project.value.toLowerCase())
+ })
+ })
+})
diff --git a/tests/index.test.ts b/tests/index.test.ts
new file mode 100644
index 0000000..cb82fe0
--- /dev/null
+++ b/tests/index.test.ts
@@ -0,0 +1,93 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+
+// Mock the command modules
+vi.mock('../src/commands', () => ({
+ default: vi.fn()
+}))
+
+vi.mock('../src/commands/init', () => ({
+ default: vi.fn()
+}))
+
+vi.mock('../src/commands/where', () => ({
+ default: vi.fn()
+}))
+
+describe('main index', () => {
+ let mockCli: any
+ let mockInit: any
+ let mockWhere: any
+
+ beforeEach(async () => {
+ vi.clearAllMocks()
+
+ // Import mocked modules
+ mockCli = (await import('../src/commands')).default
+ mockInit = (await import('../src/commands/init')).default
+ mockWhere = (await import('../src/commands/where')).default
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('should call init command when --init flag is provided', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz', '--init'])
+
+ expect(mockInit).toHaveBeenCalled()
+ expect(mockCli).not.toHaveBeenCalled()
+ expect(mockWhere).not.toHaveBeenCalled()
+ })
+
+ it('should call init command when -i flag is provided', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz', '-i'])
+
+ expect(mockInit).toHaveBeenCalled()
+ expect(mockCli).not.toHaveBeenCalled()
+ expect(mockWhere).not.toHaveBeenCalled()
+ })
+
+ it('should call where command when --where flag is provided', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz', '--where'])
+
+ expect(mockWhere).toHaveBeenCalled()
+ expect(mockInit).not.toHaveBeenCalled()
+ expect(mockCli).not.toHaveBeenCalled()
+ })
+
+ it('should call where command when -w flag is provided', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz', '-w'])
+
+ expect(mockWhere).toHaveBeenCalled()
+ expect(mockInit).not.toHaveBeenCalled()
+ expect(mockCli).not.toHaveBeenCalled()
+ })
+
+ it('should call default cli command when no flags are provided', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz'])
+
+ expect(mockCli).toHaveBeenCalled()
+ expect(mockInit).not.toHaveBeenCalled()
+ expect(mockWhere).not.toHaveBeenCalled()
+ })
+
+ it('should handle multiple arguments correctly', async () => {
+ const { run } = await import('../src/index')
+
+ await run(['cz', 'some', 'other', 'args'])
+
+ expect(mockCli).toHaveBeenCalled()
+ expect(mockInit).not.toHaveBeenCalled()
+ expect(mockWhere).not.toHaveBeenCalled()
+ })
+})
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 0000000..bd3b059
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,11 @@
+// Test setup file
+import { vi } from 'vitest'
+
+// Setup global mocks
+global.console = {
+ ...console,
+ log: vi.fn(),
+ error: vi.fn(),
+ warn: vi.fn(),
+ info: vi.fn()
+}
diff --git a/tests/utils/test-helpers.ts b/tests/utils/test-helpers.ts
new file mode 100644
index 0000000..ae144ce
--- /dev/null
+++ b/tests/utils/test-helpers.ts
@@ -0,0 +1,46 @@
+import { vi } from 'vitest'
+
+/**
+ * Creates a mock console object for testing console output
+ */
+export function createMockConsole() {
+ return {
+ log: vi.fn(),
+ error: vi.fn(),
+ warn: vi.fn(),
+ info: vi.fn()
+ }
+}
+
+/**
+ * Helper to mock prompts responses
+ */
+export function mockPromptsResponse(response: any) {
+ const prompts = vi.hoisted(() => vi.fn())
+ prompts.mockResolvedValue(response)
+ return prompts
+}
+
+/**
+ * Helper to mock file system operations
+ */
+export function createMockFs() {
+ return {
+ writeFileSync: vi.fn(),
+ readFileSync: vi.fn(),
+ existsSync: vi.fn()
+ }
+}
+
+/**
+ * Helper to test git command execution
+ */
+export function mockGitExecution(result: { stdout: string; stderr?: string; exitCode?: number }) {
+ const execa = vi.hoisted(() => vi.fn())
+ execa.mockResolvedValue({
+ stdout: result.stdout,
+ stderr: result.stderr || '',
+ exitCode: result.exitCode || 0
+ })
+ return execa
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..1861f49
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ setupFiles: ['./tests/setup.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'json-summary', 'html', 'lcov'],
+ exclude: [
+ 'node_modules/**',
+ 'tests/**',
+ 'dist/**',
+ 'coverage/**',
+ '*.config.{js,ts}',
+ 'bin/**',
+ 'scripts/**',
+ 'src/types/**'
+ ],
+ all: true
+ }
+ },
+ resolve: {
+ alias: {
+ '@': './src'
+ }
+ }
+})