evizi-cli 1.0.0

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/bin/evizi.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { initCommand } from '../src/commands/init.js';
5
+ import { upgradeCommand } from '../src/commands/upgrade.js';
6
+
7
+ program
8
+ .name('evizi')
9
+ .description('Install and manage agent kits from evizi-kit')
10
+ .version('1.0.0');
11
+
12
+ program
13
+ .command('init')
14
+ .description('Install agent kits into your project')
15
+ .option('--agent <kits>', 'Comma-separated kit names (e.g. claude,cursor,github)')
16
+ .option('--all', 'Install all available kits')
17
+ .option('--local <path>', 'Use local evizi-kit directory instead of npm')
18
+ .action(initCommand);
19
+
20
+ program
21
+ .command('upgrade')
22
+ .description('Upgrade installed agent kits to latest version')
23
+ .option('--local <path>', 'Use local evizi-kit directory instead of npm')
24
+ .action(upgradeCommand);
25
+
26
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "evizi-cli",
3
+ "version": "1.0.0",
4
+ "description": "Install and manage agent kits from evizi-kit",
5
+ "type": "module",
6
+ "bin": {
7
+ "evizi": "./bin/evizi.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "inquirer": "^9.0.0"
16
+ },
17
+ "engines": {
18
+ "node": ">=20.0.0"
19
+ },
20
+ "license": "MIT"
21
+ }
@@ -0,0 +1,127 @@
1
+ import inquirer from 'inquirer';
2
+ import { resolveKitSource } from '../lib/resolver.js';
3
+ import {
4
+ listAvailableKits,
5
+ getSelectableKits,
6
+ resolveDependencies,
7
+ loadManifest,
8
+ } from '../lib/manifest.js';
9
+ import { loadIgnorePatterns } from '../lib/ignore.js';
10
+ import { copyKit } from '../lib/copier.js';
11
+ import { readLockfile, writeLockfile } from '../lib/lockfile.js';
12
+
13
+ export async function initCommand(options) {
14
+ const targetDir = process.cwd();
15
+
16
+ // Check for existing lockfile
17
+ const existingLock = readLockfile(targetDir);
18
+ if (existingLock) {
19
+ const { proceed } = await inquirer.prompt([
20
+ {
21
+ type: 'confirm',
22
+ name: 'proceed',
23
+ message: 'evizi kits are already installed. Overwrite?',
24
+ default: false,
25
+ },
26
+ ]);
27
+ if (!proceed) {
28
+ console.log('Aborted.');
29
+ return;
30
+ }
31
+ }
32
+
33
+ // Resolve kit source
34
+ console.log(
35
+ options.local
36
+ ? `Using local kits from: ${options.local}`
37
+ : 'Downloading evizi-kit from npm...',
38
+ );
39
+
40
+ const { kitsDir, cleanup } = resolveKitSource(options);
41
+
42
+ try {
43
+ const allKits = listAvailableKits(kitsDir);
44
+ const selectableKits = getSelectableKits(allKits);
45
+
46
+ // Determine which kits to install
47
+ let selectedNames;
48
+
49
+ if (options.all) {
50
+ selectedNames = selectableKits.map((k) => k.name);
51
+ } else if (options.agent) {
52
+ selectedNames = options.agent.split(',').map((s) => s.trim());
53
+
54
+ // Validate kit names
55
+ const validNames = new Set(selectableKits.map((k) => k.name));
56
+ for (const name of selectedNames) {
57
+ if (!validNames.has(name)) {
58
+ console.error(
59
+ `Unknown kit: "${name}". Available: ${[...validNames].join(', ')}`,
60
+ );
61
+ return;
62
+ }
63
+ }
64
+ } else {
65
+ // Interactive selection
66
+ const { kits } = await inquirer.prompt([
67
+ {
68
+ type: 'checkbox',
69
+ name: 'kits',
70
+ message: 'Which agent kits do you want to install?',
71
+ choices: selectableKits.map((k) => ({
72
+ name: `${k.displayName} — ${k.description}`,
73
+ value: k.name,
74
+ checked: false,
75
+ })),
76
+ },
77
+ ]);
78
+ selectedNames = kits;
79
+
80
+ if (selectedNames.length === 0) {
81
+ console.log('No kits selected. Aborted.');
82
+ return;
83
+ }
84
+ }
85
+
86
+ // Resolve dependencies
87
+ const resolvedNames = resolveDependencies(kitsDir, selectedNames);
88
+ const depOnly = resolvedNames.filter((n) => !selectedNames.includes(n));
89
+ if (depOnly.length > 0) {
90
+ console.log(`Auto-including dependencies: ${depOnly.join(', ')}`);
91
+ }
92
+
93
+ // Load ignore patterns
94
+ const ignorePatterns = loadIgnorePatterns(targetDir);
95
+
96
+ // Copy kits
97
+ const kitsData = {};
98
+ let totalFiles = 0;
99
+
100
+ for (const kitName of resolvedNames) {
101
+ const manifest = loadManifest(kitsDir, kitName);
102
+ const copiedFiles = copyKit(
103
+ kitsDir,
104
+ kitName,
105
+ manifest,
106
+ targetDir,
107
+ ignorePatterns,
108
+ );
109
+
110
+ kitsData[kitName] = {
111
+ version: manifest.version,
112
+ files: copiedFiles,
113
+ };
114
+
115
+ totalFiles += copiedFiles.length;
116
+ console.log(` ✓ ${manifest.displayName} (${copiedFiles.length} files)`);
117
+ }
118
+
119
+ // Write lockfile
120
+ writeLockfile(targetDir, kitsData);
121
+
122
+ console.log(`\nInstalled ${resolvedNames.length} kits (${totalFiles} files).`);
123
+ console.log('Lock file written to .evizi-lock.json');
124
+ } finally {
125
+ cleanup();
126
+ }
127
+ }
@@ -0,0 +1,101 @@
1
+ import { unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { resolveKitSource } from '../lib/resolver.js';
4
+ import { loadManifest } from '../lib/manifest.js';
5
+ import { loadIgnorePatterns } from '../lib/ignore.js';
6
+ import { copyKit } from '../lib/copier.js';
7
+ import { readLockfile, writeLockfile } from '../lib/lockfile.js';
8
+
9
+ export async function upgradeCommand(options) {
10
+ const targetDir = process.cwd();
11
+
12
+ // Read existing lockfile
13
+ const lockfile = readLockfile(targetDir);
14
+ if (!lockfile) {
15
+ console.error('No .evizi-lock.json found. Run `evizi init` first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log(
20
+ options.local
21
+ ? `Using local kits from: ${options.local}`
22
+ : 'Downloading latest evizi-kit from npm...',
23
+ );
24
+
25
+ const { kitsDir, cleanup } = resolveKitSource(options);
26
+
27
+ try {
28
+ const ignorePatterns = loadIgnorePatterns(targetDir);
29
+ const kitsData = {};
30
+ let updatedCount = 0;
31
+ let skippedCount = 0;
32
+
33
+ for (const kitName of Object.keys(lockfile.kits)) {
34
+ const oldKit = lockfile.kits[kitName];
35
+ let manifest;
36
+
37
+ try {
38
+ manifest = loadManifest(kitsDir, kitName);
39
+ } catch {
40
+ console.log(` ⚠ Kit "${kitName}" no longer exists in source, skipping.`);
41
+ skippedCount++;
42
+ continue;
43
+ }
44
+
45
+ if (manifest.version === oldKit.version) {
46
+ console.log(` — ${manifest.displayName} v${manifest.version} (up to date)`);
47
+ kitsData[kitName] = oldKit;
48
+ skippedCount++;
49
+ continue;
50
+ }
51
+
52
+ // Process deletions from manifest
53
+ for (const delPath of manifest.deletions) {
54
+ const fullPath = join(targetDir, delPath);
55
+ if (existsSync(fullPath)) {
56
+ unlinkSync(fullPath);
57
+ console.log(` Deleted: ${delPath}`);
58
+ }
59
+ }
60
+
61
+ // Remove orphaned files (in old lockfile but not in new copy)
62
+ const copiedFiles = copyKit(
63
+ kitsDir,
64
+ kitName,
65
+ manifest,
66
+ targetDir,
67
+ ignorePatterns,
68
+ );
69
+
70
+ const newFileSet = new Set(copiedFiles);
71
+ for (const oldFile of oldKit.files || []) {
72
+ if (!newFileSet.has(oldFile)) {
73
+ const fullPath = join(targetDir, oldFile);
74
+ if (existsSync(fullPath)) {
75
+ unlinkSync(fullPath);
76
+ console.log(` Removed orphan: ${oldFile}`);
77
+ }
78
+ }
79
+ }
80
+
81
+ kitsData[kitName] = {
82
+ version: manifest.version,
83
+ files: copiedFiles,
84
+ };
85
+
86
+ updatedCount++;
87
+ console.log(
88
+ ` ✓ ${manifest.displayName} v${oldKit.version} → v${manifest.version} (${copiedFiles.length} files)`,
89
+ );
90
+ }
91
+
92
+ // Write updated lockfile
93
+ writeLockfile(targetDir, kitsData);
94
+
95
+ console.log(
96
+ `\nUpgrade complete: ${updatedCount} updated, ${skippedCount} up to date.`,
97
+ );
98
+ } finally {
99
+ cleanup();
100
+ }
101
+ }
@@ -0,0 +1,53 @@
1
+ import { readdirSync, copyFileSync, mkdirSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { isIgnored } from './ignore.js';
4
+
5
+ /**
6
+ * Copy a kit's files into the target directory based on manifest file mappings.
7
+ * Returns array of relative paths of copied files.
8
+ */
9
+ export function copyKit(kitsDir, kitName, manifest, targetDir, ignorePatterns) {
10
+ const copiedFiles = [];
11
+ const kitDir = join(kitsDir, kitName);
12
+
13
+ for (const mapping of manifest.files) {
14
+ const srcPath = join(kitDir, mapping.src);
15
+ const destPath = join(targetDir, mapping.dest);
16
+
17
+ const stat = statSync(srcPath, { throwIfNoEntry: false });
18
+ if (!stat) continue;
19
+
20
+ if (stat.isDirectory()) {
21
+ walkAndCopy(srcPath, destPath, mapping.dest, ignorePatterns, copiedFiles);
22
+ } else {
23
+ const relPath = mapping.dest;
24
+ if (isIgnored(relPath, ignorePatterns)) continue;
25
+
26
+ mkdirSync(join(targetDir, relPath, '..'), { recursive: true });
27
+ copyFileSync(srcPath, destPath);
28
+ copiedFiles.push(relPath);
29
+ }
30
+ }
31
+
32
+ return copiedFiles;
33
+ }
34
+
35
+ function walkAndCopy(srcDir, destDir, baseRelPath, ignorePatterns, copiedFiles) {
36
+ const entries = readdirSync(srcDir, { withFileTypes: true });
37
+
38
+ for (const entry of entries) {
39
+ const srcPath = join(srcDir, entry.name);
40
+ const destPath = join(destDir, entry.name);
41
+ const relPath = join(baseRelPath, entry.name);
42
+
43
+ if (entry.isDirectory()) {
44
+ walkAndCopy(srcPath, destPath, relPath, ignorePatterns, copiedFiles);
45
+ } else {
46
+ if (isIgnored(relPath, ignorePatterns)) continue;
47
+
48
+ mkdirSync(destDir, { recursive: true });
49
+ copyFileSync(srcPath, destPath);
50
+ copiedFiles.push(relPath);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,48 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Load .evizi-ignore patterns from the target directory.
6
+ */
7
+ export function loadIgnorePatterns(targetDir) {
8
+ const ignorePath = join(targetDir, '.evizi-ignore');
9
+ if (!existsSync(ignorePath)) return [];
10
+
11
+ const content = readFileSync(ignorePath, 'utf-8');
12
+ return content
13
+ .split('\n')
14
+ .map((line) => line.trim())
15
+ .filter((line) => line && !line.startsWith('#'));
16
+ }
17
+
18
+ /**
19
+ * Check if a relative path matches any ignore pattern.
20
+ * Supports simple glob patterns: * and **
21
+ */
22
+ export function isIgnored(relativePath, patterns) {
23
+ for (const pattern of patterns) {
24
+ if (matchPattern(relativePath, pattern)) return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function matchPattern(filePath, pattern) {
30
+ // Exact match
31
+ if (filePath === pattern) return true;
32
+
33
+ // Check if file is inside an ignored directory
34
+ if (filePath.startsWith(pattern.endsWith('/') ? pattern : pattern + '/')) {
35
+ return true;
36
+ }
37
+
38
+ // Convert glob pattern to regex
39
+ const regexStr = pattern
40
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
41
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
42
+ .replace(/\*/g, '[^/]*')
43
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*')
44
+ .replace(/\?/g, '[^/]');
45
+
46
+ const regex = new RegExp(`^${regexStr}$`);
47
+ return regex.test(filePath);
48
+ }
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const LOCKFILE_NAME = '.evizi-lock.json';
5
+
6
+ /**
7
+ * Read the lockfile from a target directory.
8
+ * Returns null if not found.
9
+ */
10
+ export function readLockfile(targetDir) {
11
+ const lockPath = join(targetDir, LOCKFILE_NAME);
12
+ if (!existsSync(lockPath)) return null;
13
+
14
+ try {
15
+ return JSON.parse(readFileSync(lockPath, 'utf-8'));
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Write the lockfile to a target directory.
23
+ */
24
+ export function writeLockfile(targetDir, kitsData) {
25
+ const lockPath = join(targetDir, LOCKFILE_NAME);
26
+ const lockContent = {
27
+ version: '1.0.0',
28
+ installedAt: new Date().toISOString(),
29
+ kits: kitsData,
30
+ };
31
+
32
+ writeFileSync(lockPath, JSON.stringify(lockContent, null, 2) + '\n');
33
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Load a single kit's manifest.json.
6
+ */
7
+ export function loadManifest(kitsDir, kitName) {
8
+ const manifestPath = join(kitsDir, kitName, 'manifest.json');
9
+ const raw = readFileSync(manifestPath, 'utf-8');
10
+ const manifest = JSON.parse(raw);
11
+
12
+ return {
13
+ name: manifest.name,
14
+ displayName: manifest.displayName || manifest.name,
15
+ version: manifest.version || '1.0.0',
16
+ description: manifest.description || '',
17
+ files: manifest.files || [],
18
+ dependencies: manifest.dependencies || [],
19
+ deletions: manifest.deletions || [],
20
+ };
21
+ }
22
+
23
+ /**
24
+ * List all available kits (reads subdirectories of kitsDir).
25
+ * Returns manifests for all kits, with `shared` last.
26
+ */
27
+ export function listAvailableKits(kitsDir) {
28
+ const entries = readdirSync(kitsDir, { withFileTypes: true });
29
+ const kits = [];
30
+
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue;
33
+ try {
34
+ const manifest = loadManifest(kitsDir, entry.name);
35
+ kits.push(manifest);
36
+ } catch {
37
+ // Skip directories without valid manifest
38
+ }
39
+ }
40
+
41
+ return kits;
42
+ }
43
+
44
+ /**
45
+ * Get user-selectable kits (excludes shared/dependency-only kits).
46
+ */
47
+ export function getSelectableKits(kits) {
48
+ return kits.filter((k) => k.name !== 'shared');
49
+ }
50
+
51
+ /**
52
+ * Resolve dependencies for selected kit names.
53
+ * Returns ordered list with dependencies first.
54
+ */
55
+ export function resolveDependencies(kitsDir, selectedNames) {
56
+ const resolved = new Set();
57
+ const result = [];
58
+
59
+ function resolve(name) {
60
+ if (resolved.has(name)) return;
61
+ resolved.add(name);
62
+
63
+ const manifest = loadManifest(kitsDir, name);
64
+ for (const dep of manifest.dependencies) {
65
+ resolve(dep);
66
+ }
67
+ result.push(name);
68
+ }
69
+
70
+ for (const name of selectedNames) {
71
+ resolve(name);
72
+ }
73
+
74
+ return result;
75
+ }
@@ -0,0 +1,52 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { mkdtempSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ /**
7
+ * Resolve the path to the kits/ directory from evizi-kit.
8
+ * Supports local path (for dev) or npm registry (for production).
9
+ */
10
+ export function resolveKitSource(options = {}) {
11
+ if (options.local) {
12
+ const kitsDir = join(options.local, 'kits');
13
+ if (!existsSync(kitsDir)) {
14
+ throw new Error(`Local kits directory not found: ${kitsDir}`);
15
+ }
16
+ return { kitsDir, cleanup: () => {} };
17
+ }
18
+
19
+ // Download from npm registry
20
+ const tmpDir = mkdtempSync(join(tmpdir(), 'evizi-'));
21
+
22
+ try {
23
+ execSync(`npm pack evizi-kit --pack-destination "${tmpDir}"`, {
24
+ stdio: 'pipe',
25
+ cwd: tmpDir,
26
+ });
27
+
28
+ execSync(`tar -xzf evizi-kit-*.tgz`, {
29
+ stdio: 'pipe',
30
+ cwd: tmpDir,
31
+ });
32
+
33
+ const kitsDir = join(tmpDir, 'package', 'kits');
34
+ if (!existsSync(kitsDir)) {
35
+ throw new Error('Downloaded evizi-kit package does not contain kits/ directory');
36
+ }
37
+
38
+ return {
39
+ kitsDir,
40
+ cleanup: () => {
41
+ try {
42
+ execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
43
+ } catch {}
44
+ },
45
+ };
46
+ } catch (err) {
47
+ try {
48
+ execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
49
+ } catch {}
50
+ throw new Error(`Failed to download evizi-kit from npm: ${err.message}`);
51
+ }
52
+ }