yggtree 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -87
- package/dist/commands/wt/create-branch.js +21 -35
- package/dist/commands/wt/create-multi.js +11 -2
- package/dist/commands/wt/create-sandbox.js +21 -35
- package/dist/commands/wt/create.js +109 -58
- package/dist/commands/wt/enter.js +3 -2
- package/dist/commands/wt/list.js +23 -6
- package/dist/commands/wt/open.js +207 -53
- package/dist/index.js +133 -111
- package/dist/lib/env-files.js +72 -0
- package/dist/lib/git.js +89 -6
- package/dist/lib/registry.js +81 -0
- package/dist/lib/repo-context.js +46 -0
- package/dist/lib/update-check.js +55 -0
- package/dist/lib/worktree.js +12 -0
- package/package.json +12 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { log } from './ui.js';
|
|
6
|
+
const EXAMPLE_ENV_FILES = new Set([
|
|
7
|
+
'.env.example',
|
|
8
|
+
'.env.sample',
|
|
9
|
+
'.env.template',
|
|
10
|
+
'.env.defaults',
|
|
11
|
+
]);
|
|
12
|
+
export function canPromptForEnvFiles() {
|
|
13
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
|
|
14
|
+
}
|
|
15
|
+
async function confirmEnvFileCopy(files, defaultMessage, options) {
|
|
16
|
+
const isInteractive = options.interactive ?? canPromptForEnvFiles();
|
|
17
|
+
if (!isInteractive) {
|
|
18
|
+
log.dim(`Skipped local env file${files.length === 1 ? '' : 's'} in non-interactive mode.`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const { shouldCopyEnvFiles } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'shouldCopyEnvFiles',
|
|
25
|
+
message: options.promptMessage || defaultMessage,
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
if (!shouldCopyEnvFiles) {
|
|
30
|
+
log.dim('Skipped local env files.');
|
|
31
|
+
}
|
|
32
|
+
return shouldCopyEnvFiles;
|
|
33
|
+
}
|
|
34
|
+
export async function findLocalEnvFiles(repoRoot) {
|
|
35
|
+
const entries = await fs.readdir(repoRoot, { withFileTypes: true });
|
|
36
|
+
return entries
|
|
37
|
+
.filter(entry => entry.isFile())
|
|
38
|
+
.map(entry => entry.name)
|
|
39
|
+
.filter(name => name === '.env' || name.startsWith('.env.'))
|
|
40
|
+
.filter(name => !EXAMPLE_ENV_FILES.has(name))
|
|
41
|
+
.sort((a, b) => a.localeCompare(b));
|
|
42
|
+
}
|
|
43
|
+
export async function copyEnvFiles(repoRoot, wtPath, envFiles) {
|
|
44
|
+
if (envFiles.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
for (const envFile of envFiles) {
|
|
47
|
+
await fs.copy(path.join(repoRoot, envFile), path.join(wtPath, envFile));
|
|
48
|
+
}
|
|
49
|
+
log.info(`Copied ${envFiles.map(file => chalk.cyan(file)).join(', ')} to the worktree.`);
|
|
50
|
+
}
|
|
51
|
+
export async function promptAndCopyEnvFiles(repoRoot, wtPath, envFiles, options = {}) {
|
|
52
|
+
const files = envFiles || await findLocalEnvFiles(repoRoot);
|
|
53
|
+
if (files.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const shouldCopyEnvFiles = await confirmEnvFileCopy(files, `Copy local env file${files.length === 1 ? '' : 's'} to this worktree? (${files.join(', ')})`, options);
|
|
56
|
+
if (!shouldCopyEnvFiles) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await copyEnvFiles(repoRoot, wtPath, files);
|
|
60
|
+
}
|
|
61
|
+
export async function promptAndCopyEnvFilesToWorktrees(repoRoot, wtPaths, envFiles, options = {}) {
|
|
62
|
+
const files = envFiles || await findLocalEnvFiles(repoRoot);
|
|
63
|
+
if (files.length === 0 || wtPaths.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
const shouldCopyEnvFiles = await confirmEnvFileCopy(files, `Copy local env file${files.length === 1 ? '' : 's'} to these worktrees? (${files.join(', ')})`, options);
|
|
66
|
+
if (!shouldCopyEnvFiles) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const wtPath of wtPaths) {
|
|
70
|
+
await copyEnvFiles(repoRoot, wtPath, files);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/lib/git.js
CHANGED
|
@@ -3,10 +3,18 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { log } from './ui.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { registerRepo } from './registry.js';
|
|
7
|
+
let cachedRepoRoot = null;
|
|
6
8
|
export async function getRepoRoot() {
|
|
9
|
+
if (cachedRepoRoot)
|
|
10
|
+
return cachedRepoRoot;
|
|
7
11
|
try {
|
|
8
12
|
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
|
|
9
|
-
|
|
13
|
+
cachedRepoRoot = stdout.trim();
|
|
14
|
+
// Silently register the repo in the background
|
|
15
|
+
const name = path.basename(cachedRepoRoot);
|
|
16
|
+
registerRepo(name, cachedRepoRoot).catch(() => { });
|
|
17
|
+
return cachedRepoRoot;
|
|
10
18
|
}
|
|
11
19
|
catch (error) {
|
|
12
20
|
throw new Error('Not a git repository');
|
|
@@ -46,6 +54,9 @@ export async function removeWorktree(wtPath) {
|
|
|
46
54
|
}
|
|
47
55
|
export async function listWorktrees() {
|
|
48
56
|
const { stdout } = await execa('git', ['worktree', 'list', '--porcelain']);
|
|
57
|
+
return parseWorktreeList(stdout);
|
|
58
|
+
}
|
|
59
|
+
export function parseWorktreeList(stdout) {
|
|
49
60
|
const worktrees = [];
|
|
50
61
|
let currentWt = {};
|
|
51
62
|
for (const line of stdout.split('\n')) {
|
|
@@ -63,6 +74,8 @@ export async function listWorktrees() {
|
|
|
63
74
|
currentWt.HEAD = value;
|
|
64
75
|
if (key === 'branch')
|
|
65
76
|
currentWt.branch = value.replace('refs/heads/', '');
|
|
77
|
+
if (key === 'prunable')
|
|
78
|
+
currentWt.prunable = value;
|
|
66
79
|
}
|
|
67
80
|
// Push the last one if active
|
|
68
81
|
if (currentWt.path)
|
|
@@ -130,13 +143,83 @@ export async function publishBranch(wtPath, branchName) {
|
|
|
130
143
|
log.info(`Publishing ${chalk.cyan(branchName)} → ${chalk.cyan(desiredUpstream)}...`);
|
|
131
144
|
await execa('git', ['push', '-u', 'origin', 'HEAD'], { cwd: wtPath });
|
|
132
145
|
}
|
|
146
|
+
let ghAvailableCache = null;
|
|
133
147
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
* Check whether `gh` CLI is installed and authenticated.
|
|
149
|
+
* Result is cached for the process lifetime.
|
|
150
|
+
*/
|
|
151
|
+
export async function isGhAvailable() {
|
|
152
|
+
if (ghAvailableCache !== null)
|
|
153
|
+
return ghAvailableCache;
|
|
154
|
+
try {
|
|
155
|
+
await execa('gh', ['auth', 'status'], { stdio: 'ignore' });
|
|
156
|
+
ghAvailableCache = true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
ghAvailableCache = false;
|
|
160
|
+
}
|
|
161
|
+
return ghAvailableCache;
|
|
162
|
+
}
|
|
163
|
+
function derivePrLabel(state, reviewDecision) {
|
|
164
|
+
if (state === 'merged')
|
|
165
|
+
return 'MERGED';
|
|
166
|
+
if (state === 'closed')
|
|
167
|
+
return 'CLOSED';
|
|
168
|
+
if (state === 'draft')
|
|
169
|
+
return 'DRAFT';
|
|
170
|
+
// state === 'open'
|
|
171
|
+
if (reviewDecision === 'approved')
|
|
172
|
+
return 'APPROVED';
|
|
173
|
+
if (reviewDecision === 'changes_requested')
|
|
174
|
+
return 'CHANGES';
|
|
175
|
+
if (reviewDecision === 'review_required')
|
|
176
|
+
return 'IN REVIEW';
|
|
177
|
+
return 'OPEN';
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetch PR status for a branch using `gh pr view`.
|
|
181
|
+
* Returns null when `gh` is unavailable or the branch has no PR.
|
|
139
182
|
*/
|
|
183
|
+
export async function getPrStatusForBranch(branch, cwd) {
|
|
184
|
+
if (!branch || branch === 'detached')
|
|
185
|
+
return null;
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execa('gh', ['pr', 'view', branch, '--json', 'state,reviewDecision,isDraft,number'], { cwd, timeout: 10_000 });
|
|
188
|
+
const data = JSON.parse(stdout);
|
|
189
|
+
const isDraft = data.isDraft === true;
|
|
190
|
+
const rawState = (data.state || '').toLowerCase();
|
|
191
|
+
const state = isDraft && rawState === 'open' ? 'draft' : rawState;
|
|
192
|
+
const reviewDecision = (data.reviewDecision || '').toLowerCase().replace(/ /g, '_') || null;
|
|
193
|
+
return {
|
|
194
|
+
state,
|
|
195
|
+
reviewDecision,
|
|
196
|
+
label: derivePrLabel(state, reviewDecision),
|
|
197
|
+
number: data.number,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Batch-fetch PR statuses for all provided branches (parallelized).
|
|
206
|
+
* Returns a Map<branchName, PrStatus>.
|
|
207
|
+
* If `gh` is unavailable, returns an empty map immediately.
|
|
208
|
+
*/
|
|
209
|
+
export async function getPrStatusBatch(branches, cwd) {
|
|
210
|
+
const map = new Map();
|
|
211
|
+
if (!(await isGhAvailable()))
|
|
212
|
+
return map;
|
|
213
|
+
const results = await Promise.all(branches.map(async (branch) => {
|
|
214
|
+
const status = await getPrStatusForBranch(branch, cwd);
|
|
215
|
+
return { branch, status };
|
|
216
|
+
}));
|
|
217
|
+
for (const { branch, status } of results) {
|
|
218
|
+
if (status)
|
|
219
|
+
map.set(branch, status);
|
|
220
|
+
}
|
|
221
|
+
return map;
|
|
222
|
+
}
|
|
140
223
|
export async function getLastActivity(wtPath) {
|
|
141
224
|
const dates = [];
|
|
142
225
|
// Signal 1 — last commit epoch
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { YGG_ROOT } from './paths.js';
|
|
4
|
+
const REGISTRY_PATH = path.join(YGG_ROOT, 'registry.json');
|
|
5
|
+
/**
|
|
6
|
+
* Read the registry, initializing if missing.
|
|
7
|
+
*/
|
|
8
|
+
export async function readRegistry() {
|
|
9
|
+
try {
|
|
10
|
+
if (await fs.pathExists(REGISTRY_PATH)) {
|
|
11
|
+
return await fs.readJson(REGISTRY_PATH);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Ignore JSON read errors, just return empty
|
|
16
|
+
}
|
|
17
|
+
return { repos: {} };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Write the registry back to disk.
|
|
21
|
+
*/
|
|
22
|
+
async function writeRegistry(registry) {
|
|
23
|
+
await fs.ensureDir(YGG_ROOT);
|
|
24
|
+
await fs.writeJson(REGISTRY_PATH, registry, { spaces: 2 });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Silently register a repository path.
|
|
28
|
+
* This is meant to be called in the background (fire-and-forget).
|
|
29
|
+
*/
|
|
30
|
+
export async function registerRepo(repoName, repoRoot) {
|
|
31
|
+
try {
|
|
32
|
+
const registry = await readRegistry();
|
|
33
|
+
// Only write if it actually changed
|
|
34
|
+
if (registry.repos[repoName] !== repoRoot) {
|
|
35
|
+
registry.repos[repoName] = repoRoot;
|
|
36
|
+
await writeRegistry(registry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Fail silently — registry is a nice-to-have, shouldn't crash commands
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get a specific repo path from the registry.
|
|
45
|
+
* Also verifies the path still exists on disk, removing it if not.
|
|
46
|
+
*/
|
|
47
|
+
export async function getRegisteredRepoPath(repoName) {
|
|
48
|
+
const registry = await readRegistry();
|
|
49
|
+
const repoPath = registry.repos[repoName];
|
|
50
|
+
if (!repoPath)
|
|
51
|
+
return null;
|
|
52
|
+
// Prune dead links
|
|
53
|
+
if (!(await fs.pathExists(repoPath))) {
|
|
54
|
+
delete registry.repos[repoName];
|
|
55
|
+
await writeRegistry(registry).catch(() => { });
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return repoPath;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get all registered repos that still exist on disk.
|
|
62
|
+
*/
|
|
63
|
+
export async function getValidRegisteredRepos() {
|
|
64
|
+
const registry = await readRegistry();
|
|
65
|
+
const valid = {};
|
|
66
|
+
let changed = false;
|
|
67
|
+
// Validate in parallel
|
|
68
|
+
await Promise.all(Object.entries(registry.repos).map(async ([name, rPath]) => {
|
|
69
|
+
if (await fs.pathExists(rPath)) {
|
|
70
|
+
valid[name] = rPath;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
delete registry.repos[name];
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
if (changed) {
|
|
78
|
+
await writeRegistry(registry).catch(() => { });
|
|
79
|
+
}
|
|
80
|
+
return valid;
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { getRepoRoot } from './git.js';
|
|
4
|
+
import { getValidRegisteredRepos } from './registry.js';
|
|
5
|
+
import { log } from './ui.js';
|
|
6
|
+
import { formatWorktreeDisplayPath } from './worktree.js';
|
|
7
|
+
export async function ensureRepoContext() {
|
|
8
|
+
try {
|
|
9
|
+
return await getRepoRoot();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
const validRepos = await getValidRegisteredRepos();
|
|
13
|
+
const repoEntries = Object.entries(validRepos);
|
|
14
|
+
if (repoEntries.length === 0) {
|
|
15
|
+
log.error('Not inside a git repository and no registered realms found.');
|
|
16
|
+
log.dim('Run `yggtree` inside an existing git project first to register it.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
if (repoEntries.length === 1 && (process.env.CI === 'true' || !process.stdin.isTTY)) {
|
|
20
|
+
const [, selectedRepoPath] = repoEntries[0];
|
|
21
|
+
process.chdir(selectedRepoPath);
|
|
22
|
+
return await getRepoRoot();
|
|
23
|
+
}
|
|
24
|
+
if (!process.stdin.isTTY) {
|
|
25
|
+
log.error('Not inside a git repository and multiple registered realms are available.');
|
|
26
|
+
log.dim('Run yggtree from the repo you want to use, or run interactive mode from a real terminal to choose one.');
|
|
27
|
+
log.dim(`Available realms: ${repoEntries.map(([name]) => name).join(', ')}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.bold('\n Not inside a realm. Pick a known one:'));
|
|
31
|
+
const { selectedRepoPath } = await inquirer.prompt([
|
|
32
|
+
{
|
|
33
|
+
type: 'list',
|
|
34
|
+
name: 'selectedRepoPath',
|
|
35
|
+
message: 'Select a realm:',
|
|
36
|
+
choices: repoEntries.map(([name, repoPath]) => ({
|
|
37
|
+
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(repoPath))}`,
|
|
38
|
+
value: repoPath,
|
|
39
|
+
})),
|
|
40
|
+
pageSize: 10,
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
process.chdir(selectedRepoPath);
|
|
44
|
+
return await getRepoRoot();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import { getVersion } from './version.js';
|
|
4
|
+
import { log } from './ui.js';
|
|
5
|
+
function parseVersion(version) {
|
|
6
|
+
const [core] = version.trim().replace(/^v/, '').split('-');
|
|
7
|
+
return core.split('.').map(part => {
|
|
8
|
+
const parsed = Number.parseInt(part, 10);
|
|
9
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function isNewerVersion(latestVersion, currentVersion) {
|
|
13
|
+
const latestParts = parseVersion(latestVersion);
|
|
14
|
+
const currentParts = parseVersion(currentVersion);
|
|
15
|
+
const maxLength = Math.max(latestParts.length, currentParts.length);
|
|
16
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
17
|
+
const latestPart = latestParts[index] ?? 0;
|
|
18
|
+
const currentPart = currentParts[index] ?? 0;
|
|
19
|
+
if (latestPart > currentPart)
|
|
20
|
+
return true;
|
|
21
|
+
if (latestPart < currentPart)
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export async function fetchLatestPackageVersion() {
|
|
27
|
+
const { stdout } = await execa('npm', ['view', 'yggtree', 'version', '--silent'], {
|
|
28
|
+
timeout: 3_000,
|
|
29
|
+
});
|
|
30
|
+
return stdout.trim();
|
|
31
|
+
}
|
|
32
|
+
export async function checkForUpdate(options = {}) {
|
|
33
|
+
const currentVersion = getVersion();
|
|
34
|
+
const fetchLatestVersion = options.fetchLatestVersion ?? fetchLatestPackageVersion;
|
|
35
|
+
try {
|
|
36
|
+
const latestVersion = await fetchLatestVersion();
|
|
37
|
+
if (!latestVersion)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
currentVersion,
|
|
41
|
+
latestVersion,
|
|
42
|
+
updateAvailable: isNewerVersion(latestVersion, currentVersion),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function notifyIfUpdateAvailable() {
|
|
50
|
+
const result = await checkForUpdate();
|
|
51
|
+
if (!result?.updateAvailable)
|
|
52
|
+
return;
|
|
53
|
+
log.warning(`A newer yggtree is available: ${chalk.dim(`v${result.currentVersion}`)} -> ${chalk.green(`v${result.latestVersion}`)}`);
|
|
54
|
+
log.dim('Update with: npm install -g yggtree');
|
|
55
|
+
}
|
package/dist/lib/worktree.js
CHANGED
|
@@ -50,3 +50,15 @@ export function formatWorktreeType(type) {
|
|
|
50
50
|
return chalk.blue('MAIN ');
|
|
51
51
|
return chalk.cyan('LINKED ');
|
|
52
52
|
}
|
|
53
|
+
export function formatPrStatus(pr) {
|
|
54
|
+
switch (pr.label) {
|
|
55
|
+
case 'MERGED': return chalk.magenta(`#${pr.number} MERGED`);
|
|
56
|
+
case 'APPROVED': return chalk.green(`#${pr.number} APPROVED`);
|
|
57
|
+
case 'CHANGES': return chalk.red(`#${pr.number} CHANGES`);
|
|
58
|
+
case 'IN REVIEW': return chalk.yellow(`#${pr.number} IN REVIEW`);
|
|
59
|
+
case 'DRAFT': return chalk.dim(`#${pr.number} DRAFT`);
|
|
60
|
+
case 'OPEN': return chalk.cyan(`#${pr.number} OPEN`);
|
|
61
|
+
case 'CLOSED': return chalk.dim(`#${pr.number} CLOSED`);
|
|
62
|
+
default: return chalk.dim(`#${pr.number} ${pr.label}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yggtree",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
|
+
"packageManager": "pnpm@11.0.9",
|
|
4
5
|
"description": "Interactive CLI for managing git worktrees and configs",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "git+https://github.com/logbookfordevs/yggdrasil-worktree.git",
|
|
10
|
+
"type": "git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "yggtree.logbookfordevs.com",
|
|
7
13
|
"bin": {
|
|
8
14
|
"yggtree": "./bin/yggtree"
|
|
9
15
|
},
|
|
@@ -16,7 +22,9 @@
|
|
|
16
22
|
"build": "tsc",
|
|
17
23
|
"start": "node dist/index.js",
|
|
18
24
|
"dev": "tsc --watch",
|
|
19
|
-
"
|
|
25
|
+
"site:dev": "pnpm --dir apps/site dev",
|
|
26
|
+
"test": "pnpm build && pnpm test:typecheck && vitest run",
|
|
27
|
+
"test:typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
20
28
|
},
|
|
21
29
|
"keywords": [
|
|
22
30
|
"cli",
|
|
@@ -44,6 +52,7 @@
|
|
|
44
52
|
"@types/gradient-string": "^1.1.6",
|
|
45
53
|
"@types/inquirer": "^9.0.7",
|
|
46
54
|
"@types/node": "^20.11.19",
|
|
47
|
-
"typescript": "^5.3.3"
|
|
55
|
+
"typescript": "^5.3.3",
|
|
56
|
+
"vitest": "^4.1.7"
|
|
48
57
|
}
|
|
49
58
|
}
|