@nitrostack/cli 1.0.13 → 1.0.15
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/assets/canonical.gitignore +57 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +90 -16
- package/dist/commands/pack.d.ts +10 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +82 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/pack/canonical-gitignore.d.ts +14 -0
- package/dist/pack/canonical-gitignore.d.ts.map +1 -0
- package/dist/pack/canonical-gitignore.js +28 -0
- package/dist/pack/exclusions.d.ts +8 -0
- package/dist/pack/exclusions.d.ts.map +1 -0
- package/dist/pack/exclusions.js +84 -0
- package/dist/pack/gitignore.d.ts +21 -0
- package/dist/pack/gitignore.d.ts.map +1 -0
- package/dist/pack/gitignore.js +123 -0
- package/dist/pack/ignore-matcher.d.ts +19 -0
- package/dist/pack/ignore-matcher.d.ts.map +1 -0
- package/dist/pack/ignore-matcher.js +149 -0
- package/dist/pack/index.d.ts +11 -0
- package/dist/pack/index.d.ts.map +1 -0
- package/dist/pack/index.js +8 -0
- package/dist/pack/pack-project.d.ts +6 -0
- package/dist/pack/pack-project.d.ts.map +1 -0
- package/dist/pack/pack-project.js +59 -0
- package/dist/pack/standalone.d.ts +3 -0
- package/dist/pack/standalone.d.ts.map +1 -0
- package/dist/pack/standalone.js +95 -0
- package/dist/pack/tree.d.ts +5 -0
- package/dist/pack/tree.d.ts.map +1 -0
- package/dist/pack/tree.js +70 -0
- package/dist/pack/types.d.ts +35 -0
- package/dist/pack/types.d.ts.map +1 -0
- package/dist/pack/types.js +1 -0
- package/dist/pack/validate-project.d.ts +9 -0
- package/dist/pack/validate-project.d.ts.map +1 -0
- package/dist/pack/validate-project.js +44 -0
- package/dist/pack/zipper.d.ts +20 -0
- package/dist/pack/zipper.d.ts.map +1 -0
- package/dist/pack/zipper.js +121 -0
- package/dist/skills/clone.d.ts +28 -0
- package/dist/skills/clone.d.ts.map +1 -0
- package/dist/skills/clone.js +60 -0
- package/dist/skills/detect-agents.d.ts +8 -0
- package/dist/skills/detect-agents.d.ts.map +1 -0
- package/dist/skills/detect-agents.js +129 -0
- package/dist/skills/discover.d.ts +12 -0
- package/dist/skills/discover.d.ts.map +1 -0
- package/dist/skills/discover.js +45 -0
- package/dist/skills/index.d.ts +21 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +97 -0
- package/dist/skills/installer.d.ts +21 -0
- package/dist/skills/installer.d.ts.map +1 -0
- package/dist/skills/installer.js +48 -0
- package/dist/skills/types.d.ts +39 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +1 -0
- package/dist/skills/ui.d.ts +55 -0
- package/dist/skills/ui.d.ts.map +1 -0
- package/dist/skills/ui.js +102 -0
- package/package.json +6 -3
- package/templates/typescript-oauth/.env.example +1 -1
- package/templates/typescript-oauth/_gitignore +57 -0
- package/templates/typescript-pizzaz/.env.example +1 -1
- package/templates/typescript-pizzaz/_gitignore +57 -0
- package/templates/typescript-starter/.env.example +1 -1
- package/templates/typescript-starter/_gitignore +57 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
export class PackValidationError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'PackValidationError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function collectNitrostackPackages(packageJson) {
|
|
10
|
+
const packages = new Set();
|
|
11
|
+
const sections = [packageJson.dependencies, packageJson.devDependencies];
|
|
12
|
+
for (const section of sections) {
|
|
13
|
+
if (!section)
|
|
14
|
+
continue;
|
|
15
|
+
for (const pkg of Object.keys(section)) {
|
|
16
|
+
if (pkg.startsWith('@nitrostack/')) {
|
|
17
|
+
packages.add(pkg);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Array.from(packages).sort();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate that the target directory is a NitroStack project.
|
|
25
|
+
*/
|
|
26
|
+
export async function validateNitrostackProject(cwd = process.cwd()) {
|
|
27
|
+
const projectRoot = path.resolve(cwd);
|
|
28
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
29
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
30
|
+
throw new PackValidationError('package.json not found in the current directory');
|
|
31
|
+
}
|
|
32
|
+
const packageJson = await fs.readJSON(packageJsonPath);
|
|
33
|
+
const nitrostackPackages = collectNitrostackPackages(packageJson);
|
|
34
|
+
if (nitrostackPackages.length === 0) {
|
|
35
|
+
throw new PackValidationError('No @nitrostack/* dependencies found in package.json. Run this command from a NitroStack project.');
|
|
36
|
+
}
|
|
37
|
+
const projectName = packageJson.name || path.basename(projectRoot);
|
|
38
|
+
return {
|
|
39
|
+
projectRoot,
|
|
40
|
+
packageJsonPath,
|
|
41
|
+
projectName,
|
|
42
|
+
nitrostackPackages,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IgnoreMatcher } from './ignore-matcher.js';
|
|
2
|
+
export interface ZipCollectionResult {
|
|
3
|
+
filesIncluded: number;
|
|
4
|
+
includedPaths: string[];
|
|
5
|
+
excludedPaths: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Collect relative file paths that should be included, and pruned excluded roots.
|
|
9
|
+
* Excluded directories are recorded once and not descended into.
|
|
10
|
+
* Symlinks are resolved via stat/realpath so:
|
|
11
|
+
* - symlink-to-directory is walked (not archived as a file)
|
|
12
|
+
* - cycles and targets outside the project root are skipped
|
|
13
|
+
*/
|
|
14
|
+
export declare function collectFilesToPack(projectRoot: string, matcher: IgnoreMatcher): Promise<ZipCollectionResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Create an optimized zip archive from the project directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createOptimizedZip(projectRoot: string, outputPath: string, matcher: IgnoreMatcher): Promise<ZipCollectionResult>;
|
|
19
|
+
export declare function getZipSizeBytes(outputPath: string): Promise<number>;
|
|
20
|
+
//# sourceMappingURL=zipper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zipper.d.ts","sourceRoot":"","sources":["../../src/pack/zipper.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAQD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC,CAsF9B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC,CAwB9B;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGzE"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { createWriteStream } from 'fs';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
import { isPathIgnored } from './gitignore.js';
|
|
6
|
+
/** True when realPath is the root or a descendant of rootReal. */
|
|
7
|
+
function isInsideRoot(realPath, rootReal) {
|
|
8
|
+
const relative = path.relative(rootReal, realPath);
|
|
9
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Collect relative file paths that should be included, and pruned excluded roots.
|
|
13
|
+
* Excluded directories are recorded once and not descended into.
|
|
14
|
+
* Symlinks are resolved via stat/realpath so:
|
|
15
|
+
* - symlink-to-directory is walked (not archived as a file)
|
|
16
|
+
* - cycles and targets outside the project root are skipped
|
|
17
|
+
*/
|
|
18
|
+
export async function collectFilesToPack(projectRoot, matcher) {
|
|
19
|
+
const includedPaths = [];
|
|
20
|
+
const excludedPaths = [];
|
|
21
|
+
const visitedDirs = new Set();
|
|
22
|
+
let projectRootReal;
|
|
23
|
+
try {
|
|
24
|
+
projectRootReal = await fs.promises.realpath(projectRoot);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
projectRootReal = path.resolve(projectRoot);
|
|
28
|
+
}
|
|
29
|
+
async function walk(currentDir) {
|
|
30
|
+
let currentReal;
|
|
31
|
+
try {
|
|
32
|
+
currentReal = await fs.promises.realpath(currentDir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (visitedDirs.has(currentReal)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isInsideRoot(currentReal, projectRootReal)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
visitedDirs.add(currentReal);
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
54
|
+
const relativePath = path.relative(projectRoot, absolutePath).split(path.sep).join('/');
|
|
55
|
+
let isDirectory = entry.isDirectory();
|
|
56
|
+
let isFile = entry.isFile();
|
|
57
|
+
if (entry.isSymbolicLink()) {
|
|
58
|
+
try {
|
|
59
|
+
const realTarget = await fs.promises.realpath(absolutePath);
|
|
60
|
+
if (!isInsideRoot(realTarget, projectRootReal)) {
|
|
61
|
+
// Symlink escapes the project — skip
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const stats = await fs.promises.stat(absolutePath);
|
|
65
|
+
isDirectory = stats.isDirectory();
|
|
66
|
+
isFile = stats.isFile();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Broken symlink — skip
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (isPathIgnored(matcher, projectRoot, absolutePath, isDirectory)) {
|
|
74
|
+
const displayPath = isDirectory ? `${relativePath}/` : relativePath;
|
|
75
|
+
excludedPaths.push(displayPath);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (isDirectory) {
|
|
79
|
+
await walk(absolutePath);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isFile) {
|
|
83
|
+
includedPaths.push(relativePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await walk(projectRoot);
|
|
88
|
+
includedPaths.sort();
|
|
89
|
+
excludedPaths.sort();
|
|
90
|
+
return {
|
|
91
|
+
filesIncluded: includedPaths.length,
|
|
92
|
+
includedPaths,
|
|
93
|
+
excludedPaths,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create an optimized zip archive from the project directory.
|
|
98
|
+
*/
|
|
99
|
+
export async function createOptimizedZip(projectRoot, outputPath, matcher) {
|
|
100
|
+
const collection = await collectFilesToPack(projectRoot, matcher);
|
|
101
|
+
const outputDir = path.dirname(outputPath);
|
|
102
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
const output = createWriteStream(outputPath);
|
|
105
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
106
|
+
output.on('close', () => resolve());
|
|
107
|
+
output.on('error', reject);
|
|
108
|
+
archive.on('error', reject);
|
|
109
|
+
archive.pipe(output);
|
|
110
|
+
for (const relativePath of collection.includedPaths) {
|
|
111
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
112
|
+
archive.file(absolutePath, { name: relativePath });
|
|
113
|
+
}
|
|
114
|
+
void archive.finalize();
|
|
115
|
+
});
|
|
116
|
+
return collection;
|
|
117
|
+
}
|
|
118
|
+
export async function getZipSizeBytes(outputPath) {
|
|
119
|
+
const stats = await fs.promises.stat(outputPath);
|
|
120
|
+
return stats.size;
|
|
121
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const SKILLS_REPO_URL = "https://github.com/nitrocloudofficial/skills.git";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the skills repository cannot be cloned (git missing, network
|
|
4
|
+
* error, repository not found, etc.). The caller should catch this and
|
|
5
|
+
* display a user-friendly warning rather than crashing the entire init flow.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SkillsCloneError extends Error {
|
|
8
|
+
readonly cause?: unknown | undefined;
|
|
9
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Clones the NitroStack skills repository into a unique temporary directory
|
|
13
|
+
* and returns the absolute path to that directory.
|
|
14
|
+
*
|
|
15
|
+
* The caller is responsible for cleaning up the directory when done:
|
|
16
|
+
* ```ts
|
|
17
|
+
* const tempDir = await cloneSkillsRepo();
|
|
18
|
+
* try {
|
|
19
|
+
* // use tempDir …
|
|
20
|
+
* } finally {
|
|
21
|
+
* await fs.remove(tempDir);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @throws {SkillsCloneError} when git is unavailable or the clone fails.
|
|
26
|
+
*/
|
|
27
|
+
export declare function cloneSkillsRepo(): Promise<string>;
|
|
28
|
+
//# sourceMappingURL=clone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../../src/skills/clone.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,eAAe,qDAAqD,CAAC;AAElF;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aACI,KAAK,CAAC,EAAE,OAAO;gBAAhD,OAAO,EAAE,MAAM,EAAkB,KAAK,CAAC,EAAE,OAAO,YAAA;CAI7D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAiCvD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
export const SKILLS_REPO_URL = 'https://github.com/nitrocloudofficial/skills.git';
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when the skills repository cannot be cloned (git missing, network
|
|
9
|
+
* error, repository not found, etc.). The caller should catch this and
|
|
10
|
+
* display a user-friendly warning rather than crashing the entire init flow.
|
|
11
|
+
*/
|
|
12
|
+
export class SkillsCloneError extends Error {
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.cause = cause;
|
|
17
|
+
this.name = 'SkillsCloneError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Clones the NitroStack skills repository into a unique temporary directory
|
|
22
|
+
* and returns the absolute path to that directory.
|
|
23
|
+
*
|
|
24
|
+
* The caller is responsible for cleaning up the directory when done:
|
|
25
|
+
* ```ts
|
|
26
|
+
* const tempDir = await cloneSkillsRepo();
|
|
27
|
+
* try {
|
|
28
|
+
* // use tempDir …
|
|
29
|
+
* } finally {
|
|
30
|
+
* await fs.remove(tempDir);
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @throws {SkillsCloneError} when git is unavailable or the clone fails.
|
|
35
|
+
*/
|
|
36
|
+
export async function cloneSkillsRepo() {
|
|
37
|
+
const uniqueId = crypto.randomBytes(6).toString('hex');
|
|
38
|
+
const tempDir = path.join(os.tmpdir(), `nitrostack-skills-${uniqueId}`);
|
|
39
|
+
try {
|
|
40
|
+
execSync(`git clone --depth 1 "${SKILLS_REPO_URL}" "${tempDir}"`, {
|
|
41
|
+
stdio: 'pipe',
|
|
42
|
+
timeout: 60_000,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Clean up any partial clone before throwing
|
|
47
|
+
try {
|
|
48
|
+
await fs.remove(tempDir);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// best-effort cleanup
|
|
52
|
+
}
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
if (message.includes('git: command not found') || message.includes("'git' is not recognized")) {
|
|
55
|
+
throw new SkillsCloneError('Git is not installed or not in PATH. Install Git from https://git-scm.com and try again.', err);
|
|
56
|
+
}
|
|
57
|
+
throw new SkillsCloneError(`Failed to clone skills repository: ${message.split('\n')[0]}`, err);
|
|
58
|
+
}
|
|
59
|
+
return tempDir;
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentDescriptor } from './types.js';
|
|
2
|
+
export declare const AGENTS: AgentDescriptor[];
|
|
3
|
+
/**
|
|
4
|
+
* Runs all agent detectors in parallel and returns only the agents that are
|
|
5
|
+
* detected on the current machine.
|
|
6
|
+
*/
|
|
7
|
+
export declare function detectAgents(): Promise<AgentDescriptor[]>;
|
|
8
|
+
//# sourceMappingURL=detect-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-agents.d.ts","sourceRoot":"","sources":["../../src/skills/detect-agents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAkElD,eAAO,MAAM,MAAM,EAAE,eAAe,EAwDnC,CAAC;AAEF;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAa/D"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the given CLI command is available in the system PATH.
|
|
9
|
+
* Works cross-platform: uses `where` on Windows, `which` elsewhere.
|
|
10
|
+
*/
|
|
11
|
+
async function commandExists(cmd) {
|
|
12
|
+
try {
|
|
13
|
+
const whichCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
14
|
+
await execAsync(whichCmd);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when a directory exists at `dirPath`.
|
|
23
|
+
*/
|
|
24
|
+
function dirExists(dirPath) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const HOME = os.homedir();
|
|
33
|
+
/**
|
|
34
|
+
* Helper to build an AgentDescriptor from a simpler specification,
|
|
35
|
+
* reducing duplicate boilerplate for detect() and getSkillsDir().
|
|
36
|
+
*/
|
|
37
|
+
function createAgentDescriptor(spec) {
|
|
38
|
+
return {
|
|
39
|
+
id: spec.id,
|
|
40
|
+
name: spec.name,
|
|
41
|
+
displayPath: `~/${spec.folderName}/skills`,
|
|
42
|
+
async detect() {
|
|
43
|
+
if (spec.customDetect) {
|
|
44
|
+
return spec.customDetect();
|
|
45
|
+
}
|
|
46
|
+
const hasCmd = spec.cmd ? await commandExists(spec.cmd) : false;
|
|
47
|
+
return hasCmd || dirExists(path.join(HOME, spec.folderName));
|
|
48
|
+
},
|
|
49
|
+
getSkillsDir(scope = 'global', projectDir = process.cwd()) {
|
|
50
|
+
if (spec.getSkillsDir) {
|
|
51
|
+
return spec.getSkillsDir(scope, projectDir);
|
|
52
|
+
}
|
|
53
|
+
const base = scope === 'project' ? projectDir : HOME;
|
|
54
|
+
return path.join(base, spec.folderName, 'skills');
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export const AGENTS = [
|
|
59
|
+
// ── Original 5 agents (must keep indices 0-4 for tests) ───────────────────
|
|
60
|
+
createAgentDescriptor({
|
|
61
|
+
id: 'cursor',
|
|
62
|
+
name: 'Cursor Agent',
|
|
63
|
+
folderName: '.cursor',
|
|
64
|
+
}),
|
|
65
|
+
createAgentDescriptor({
|
|
66
|
+
id: 'codex',
|
|
67
|
+
name: 'Codex',
|
|
68
|
+
folderName: '.codex',
|
|
69
|
+
cmd: 'codex',
|
|
70
|
+
}),
|
|
71
|
+
createAgentDescriptor({
|
|
72
|
+
id: 'claude-code',
|
|
73
|
+
name: 'Claude Code',
|
|
74
|
+
folderName: '.claude',
|
|
75
|
+
cmd: 'claude',
|
|
76
|
+
}),
|
|
77
|
+
createAgentDescriptor({
|
|
78
|
+
id: 'gemini-cli',
|
|
79
|
+
name: 'Gemini CLI',
|
|
80
|
+
folderName: '.gemini',
|
|
81
|
+
cmd: 'gemini',
|
|
82
|
+
}),
|
|
83
|
+
createAgentDescriptor({
|
|
84
|
+
id: 'antigravity',
|
|
85
|
+
name: 'Google Antigravity',
|
|
86
|
+
folderName: '.antigravity',
|
|
87
|
+
cmd: 'agy',
|
|
88
|
+
}),
|
|
89
|
+
// ── Additional coding agents (limited to GitHub Copilot and OpenCode) ──────
|
|
90
|
+
createAgentDescriptor({
|
|
91
|
+
id: 'github-copilot',
|
|
92
|
+
name: 'GitHub Copilot Agent',
|
|
93
|
+
folderName: '.copilot',
|
|
94
|
+
}),
|
|
95
|
+
createAgentDescriptor({
|
|
96
|
+
id: 'opencode',
|
|
97
|
+
name: 'OpenCode Agent',
|
|
98
|
+
// OpenCode stores its config at ~/.config/opencode
|
|
99
|
+
folderName: '.config/opencode',
|
|
100
|
+
cmd: 'opencode',
|
|
101
|
+
getSkillsDir(scope = 'global', projectDir = process.cwd()) {
|
|
102
|
+
if (scope === 'project') {
|
|
103
|
+
return path.join(projectDir, '.opencode', 'skills');
|
|
104
|
+
}
|
|
105
|
+
return path.join(HOME, '.config', 'opencode', 'skills');
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
createAgentDescriptor({
|
|
109
|
+
id: 'agents',
|
|
110
|
+
name: 'Workspace Agents',
|
|
111
|
+
folderName: '.agents',
|
|
112
|
+
}),
|
|
113
|
+
];
|
|
114
|
+
/**
|
|
115
|
+
* Runs all agent detectors in parallel and returns only the agents that are
|
|
116
|
+
* detected on the current machine.
|
|
117
|
+
*/
|
|
118
|
+
export async function detectAgents() {
|
|
119
|
+
const results = await Promise.all(AGENTS.map(async (agent) => {
|
|
120
|
+
try {
|
|
121
|
+
const found = await agent.detect();
|
|
122
|
+
return found ? agent : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
return results.filter((a) => a !== null);
|
|
129
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Skill } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discovers all skills inside the cloned repository.
|
|
4
|
+
*
|
|
5
|
+
* A "skill" is any immediate subdirectory of `<cloneDir>/skills/` whose name
|
|
6
|
+
* does not start with a dot. Regular files and hidden directories are ignored.
|
|
7
|
+
*
|
|
8
|
+
* @param cloneDir - Absolute path to the root of the cloned repository.
|
|
9
|
+
* @returns Array of discovered skills, sorted alphabetically by name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverSkills(cloneDir: string): Promise<Skill[]>;
|
|
12
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/skills/discover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAgBxC;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAwBvE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
/**
|
|
4
|
+
* The subdirectory inside the cloned repository that contains individual skill
|
|
5
|
+
* folders. Repository layout:
|
|
6
|
+
*
|
|
7
|
+
* <clone>/
|
|
8
|
+
* skills/
|
|
9
|
+
* remotion/ ← one skill per subdirectory
|
|
10
|
+
* mcp-best-practices/
|
|
11
|
+
* …
|
|
12
|
+
* src/
|
|
13
|
+
* README.md
|
|
14
|
+
*/
|
|
15
|
+
const SKILLS_SUBDIR = 'skills';
|
|
16
|
+
/**
|
|
17
|
+
* Discovers all skills inside the cloned repository.
|
|
18
|
+
*
|
|
19
|
+
* A "skill" is any immediate subdirectory of `<cloneDir>/skills/` whose name
|
|
20
|
+
* does not start with a dot. Regular files and hidden directories are ignored.
|
|
21
|
+
*
|
|
22
|
+
* @param cloneDir - Absolute path to the root of the cloned repository.
|
|
23
|
+
* @returns Array of discovered skills, sorted alphabetically by name.
|
|
24
|
+
*/
|
|
25
|
+
export async function discoverSkills(cloneDir) {
|
|
26
|
+
const skillsRoot = path.join(cloneDir, SKILLS_SUBDIR);
|
|
27
|
+
if (!(await fs.pathExists(skillsRoot))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
31
|
+
const skills = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
// Skip hidden entries (e.g. .DS_Store, .git)
|
|
34
|
+
if (entry.name.startsWith('.'))
|
|
35
|
+
continue;
|
|
36
|
+
// Only directories are treated as skills
|
|
37
|
+
if (!entry.isDirectory())
|
|
38
|
+
continue;
|
|
39
|
+
skills.push({
|
|
40
|
+
name: entry.name,
|
|
41
|
+
sourcePath: path.join(skillsRoot, entry.name),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the full agent-skills installation flow.
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Clone the skills repository into a temporary directory.
|
|
6
|
+
* 2. Discover all skills in the `skills/` subdirectory.
|
|
7
|
+
* 3. Detect supported AI agents installed on the machine.
|
|
8
|
+
* 4. Prompt the user to select which agents to install skills into.
|
|
9
|
+
* 5. Install the skills and report results.
|
|
10
|
+
* 6. Clean up the temporary clone.
|
|
11
|
+
*
|
|
12
|
+
* Graceful fallbacks:
|
|
13
|
+
* - Git unavailable or clone fails → print warning and return.
|
|
14
|
+
* - No agents detected → print warning and return.
|
|
15
|
+
* - User selects no agents → return silently.
|
|
16
|
+
* - Individual skill copy fails → reported per-agent, does not abort the run.
|
|
17
|
+
*
|
|
18
|
+
* @param force - When true, overwrite existing skill files.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runSkillsFlow(force?: boolean, projectDir?: string): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/skills/index.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,aAAa,CAAC,KAAK,GAAE,OAAe,EAAE,UAAU,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4E7G"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { cloneSkillsRepo, SkillsCloneError } from './clone.js';
|
|
5
|
+
import { discoverSkills } from './discover.js';
|
|
6
|
+
import { AGENTS } from './detect-agents.js';
|
|
7
|
+
import { installSkills } from './installer.js';
|
|
8
|
+
import { printSkillsHeader, printCloned, printSkillList, printInstalling, printSuccess, printCloneError, } from './ui.js';
|
|
9
|
+
/**
|
|
10
|
+
* Runs the full agent-skills installation flow.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Clone the skills repository into a temporary directory.
|
|
14
|
+
* 2. Discover all skills in the `skills/` subdirectory.
|
|
15
|
+
* 3. Detect supported AI agents installed on the machine.
|
|
16
|
+
* 4. Prompt the user to select which agents to install skills into.
|
|
17
|
+
* 5. Install the skills and report results.
|
|
18
|
+
* 6. Clean up the temporary clone.
|
|
19
|
+
*
|
|
20
|
+
* Graceful fallbacks:
|
|
21
|
+
* - Git unavailable or clone fails → print warning and return.
|
|
22
|
+
* - No agents detected → print warning and return.
|
|
23
|
+
* - User selects no agents → return silently.
|
|
24
|
+
* - Individual skill copy fails → reported per-agent, does not abort the run.
|
|
25
|
+
*
|
|
26
|
+
* @param force - When true, overwrite existing skill files.
|
|
27
|
+
*/
|
|
28
|
+
export async function runSkillsFlow(force = false, projectDir = process.cwd()) {
|
|
29
|
+
printSkillsHeader();
|
|
30
|
+
// ── Step 1: Clone ──────────────────────────────────────────────────────────
|
|
31
|
+
let tempDir;
|
|
32
|
+
try {
|
|
33
|
+
tempDir = await cloneSkillsRepo();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const message = err instanceof SkillsCloneError
|
|
37
|
+
? err.message
|
|
38
|
+
: err instanceof Error
|
|
39
|
+
? err.message
|
|
40
|
+
: String(err);
|
|
41
|
+
printCloneError(message);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
printCloned();
|
|
46
|
+
// ── Step 2: Discover skills ──────────────────────────────────────────────
|
|
47
|
+
const skills = await discoverSkills(tempDir);
|
|
48
|
+
if (skills.length === 0) {
|
|
49
|
+
console.log(chalk.dim(' No skills found in the repository.\n'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
printSkillList(skills);
|
|
53
|
+
// ── Step 3: Install Project-Level Skills ─────────────────────────────────
|
|
54
|
+
// Installs the skills for all 6 agents by default at project scope
|
|
55
|
+
const results = await installSkills(AGENTS, skills, force, 'project', projectDir);
|
|
56
|
+
printInstalling(results);
|
|
57
|
+
// Read the skills version from the cloned repository's package.json
|
|
58
|
+
let skillsVersion = '1.0.0';
|
|
59
|
+
const skillsPackageJsonPath = path.join(tempDir, 'package.json');
|
|
60
|
+
if (await fs.pathExists(skillsPackageJsonPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const pkgJson = await fs.readJSON(skillsPackageJsonPath);
|
|
63
|
+
if (pkgJson.version) {
|
|
64
|
+
skillsVersion = pkgJson.version;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore read errors
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Write skills version to the project's package.json
|
|
72
|
+
const projectPackageJsonPath = path.join(projectDir, 'package.json');
|
|
73
|
+
if (await fs.pathExists(projectPackageJsonPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const projectPkgJson = await fs.readJSON(projectPackageJsonPath);
|
|
76
|
+
if (!projectPkgJson.nitrostack) {
|
|
77
|
+
projectPkgJson.nitrostack = {};
|
|
78
|
+
}
|
|
79
|
+
projectPkgJson.nitrostack.skillsVersion = skillsVersion;
|
|
80
|
+
await fs.writeJSON(projectPackageJsonPath, projectPkgJson, { spaces: 2 });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore write errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
printSuccess();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
// ── Cleanup: always remove the temp clone ────────────────────────────────
|
|
90
|
+
try {
|
|
91
|
+
await fs.remove(tempDir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// best-effort; temp files will be cleaned by the OS
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentDescriptor, InstallResult, Skill } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Installs all discovered skills into the skills directory of a single agent.
|
|
4
|
+
*
|
|
5
|
+
* - Creates the target directory if it does not exist.
|
|
6
|
+
* - Skips individual skills whose destination directory already exists,
|
|
7
|
+
* unless `force` is true.
|
|
8
|
+
* - Preserves the full folder structure of each skill.
|
|
9
|
+
*
|
|
10
|
+
* @param agent - The agent to install into.
|
|
11
|
+
* @param skills - All skills discovered from the repository.
|
|
12
|
+
* @param force - When true, overwrite existing skill directories.
|
|
13
|
+
* @returns InstallResult describing what was installed vs skipped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function installSkillsForAgent(agent: AgentDescriptor, skills: Skill[], force: boolean, scope?: 'project' | 'global', projectDir?: string): Promise<InstallResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Installs skills into every selected agent sequentially.
|
|
18
|
+
* Sequential (rather than parallel) installation gives cleaner CLI progress output.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installSkills(agents: AgentDescriptor[], skills: Skill[], force: boolean, scope?: 'project' | 'global', projectDir?: string): Promise<InstallResult[]>;
|
|
21
|
+
//# sourceMappingURL=installer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/skills/installer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,eAAe,EACtB,MAAM,EAAE,KAAK,EAAE,EACf,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAAS,GAAG,QAAmB,EACtC,UAAU,GAAE,MAAsB,GACjC,OAAO,CAAC,aAAa,CAAC,CAwBxB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,eAAe,EAAE,EACzB,MAAM,EAAE,KAAK,EAAE,EACf,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAAS,GAAG,QAAmB,EACtC,UAAU,GAAE,MAAsB,GACjC,OAAO,CAAC,aAAa,EAAE,CAAC,CAS1B"}
|