install-agent-skill 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.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @fileoverview Configuration and argument parsing
3
+ */
4
+
5
+ import path from "path";
6
+ import os from "os";
7
+
8
+ /** Current working directory */
9
+ export const cwd = process.cwd();
10
+
11
+ /** Local workspace skills directory */
12
+ export const WORKSPACE = path.join(cwd, ".agent", "skills");
13
+
14
+ /** Global skills directory */
15
+ export const GLOBAL_DIR = path.join(os.homedir(), ".gemini", "antigravity", "skills");
16
+
17
+ /** Cache root directory */
18
+ export const CACHE_ROOT = process.env.ADD_SKILL_CACHE_DIR || path.join(os.homedir(), ".cache", "add-skill");
19
+
20
+ /** Registry cache directory */
21
+ export const REGISTRY_CACHE = path.join(CACHE_ROOT, "registries");
22
+
23
+ /** Registries file path */
24
+ export const REGISTRIES_FILE = path.join(CACHE_ROOT, "registries.json");
25
+
26
+ /** Backup directory */
27
+ export const BACKUP_DIR = path.join(CACHE_ROOT, "backups");
28
+
29
+ // --- Argument Parsing ---
30
+
31
+ const args = process.argv.slice(2);
32
+
33
+ /** Command name (first non-flag argument) */
34
+ export const command = args[0] || "help";
35
+
36
+ /** All flags (starting with --) */
37
+ export const flags = new Set(args.filter((a) => a.startsWith("--")));
38
+
39
+ /** Command parameters (non-flag arguments after command) */
40
+ export const params = args.filter((a) => !a.startsWith("--")).slice(1);
41
+
42
+ // --- Flag Shortcuts ---
43
+
44
+ /** Use global scope */
45
+ export const GLOBAL = flags.has("--global") || flags.has("-g");
46
+
47
+ /** Verbose output */
48
+ export const VERBOSE = flags.has("--verbose") || flags.has("-v");
49
+
50
+ /** JSON output mode */
51
+ export const JSON_OUTPUT = flags.has("--json");
52
+
53
+ /** Force operation */
54
+ export const FORCE = flags.has("--force") || flags.has("-f");
55
+
56
+ /** Dry run mode */
57
+ export const DRY = flags.has("--dry-run");
58
+
59
+ /** Strict mode */
60
+ export const STRICT = flags.has("--strict");
61
+
62
+ /** Auto-fix mode */
63
+ export const FIX = flags.has("--fix");
64
+
65
+ /** Locked mode */
66
+ export const LOCKED = flags.has("--locked");
67
+
68
+ /** Offline mode */
69
+ export const OFFLINE = flags.has("--offline");
70
+
71
+ // --- Package Info ---
72
+
73
+ import { createRequire } from "module";
74
+ const require = createRequire(import.meta.url);
75
+
76
+ /** Package version */
77
+ export const VERSION = (() => {
78
+ try { return require("../../package.json").version; }
79
+ catch { return "5.0.0"; }
80
+ })();
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @fileoverview Utility helper functions
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import crypto from "crypto";
8
+ import { BACKUP_DIR, DRY, cwd, GLOBAL, WORKSPACE, GLOBAL_DIR, REGISTRIES_FILE } from "./config.js";
9
+
10
+ /**
11
+ * Get directory size recursively
12
+ * @param {string} dir - Directory path
13
+ * @returns {number} Size in bytes
14
+ */
15
+ export function getDirSize(dir) {
16
+ let size = 0;
17
+ try {
18
+ const walk = (p) => {
19
+ for (const f of fs.readdirSync(p)) {
20
+ const full = path.join(p, f);
21
+ const stat = fs.statSync(full);
22
+ if (stat.isDirectory()) walk(full);
23
+ else size += stat.size;
24
+ }
25
+ };
26
+ walk(dir);
27
+ } catch { }
28
+ return size;
29
+ }
30
+
31
+ /**
32
+ * Format bytes to human readable
33
+ * @param {number} bytes
34
+ * @returns {string}
35
+ */
36
+ export function formatBytes(bytes) {
37
+ if (bytes < 1024) return bytes + " B";
38
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
39
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
40
+ }
41
+
42
+ /**
43
+ * Format ISO date string
44
+ * @param {string} [iso]
45
+ * @returns {string}
46
+ */
47
+ export function formatDate(iso) {
48
+ return iso ? new Date(iso).toLocaleDateString() : "unknown";
49
+ }
50
+
51
+ /**
52
+ * Calculate merkle hash of directory
53
+ * @param {string} dir - Directory path
54
+ * @returns {string} SHA256 hash
55
+ */
56
+ export function merkleHash(dir) {
57
+ const files = [];
58
+ const walk = (p) => {
59
+ for (const f of fs.readdirSync(p)) {
60
+ if (f === ".skill-source.json") continue;
61
+ const full = path.join(p, f);
62
+ const stat = fs.statSync(full);
63
+ if (stat.isDirectory()) walk(full);
64
+ else {
65
+ const h = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
66
+ files.push(`${path.relative(dir, full)}:${h}`);
67
+ }
68
+ }
69
+ };
70
+ walk(dir);
71
+ files.sort();
72
+ return crypto.createHash("sha256").update(files.join("|")).digest("hex");
73
+ }
74
+
75
+ /**
76
+ * Parse skill spec string
77
+ * @param {string} spec - Spec like org/repo#skill@ref
78
+ * @returns {import('./types.js').ParsedSpec}
79
+ */
80
+ export function parseSkillSpec(spec) {
81
+ const [repoPart, skillPart] = spec.split("#");
82
+ const [org, repo] = repoPart.split("/");
83
+ const [skill, ref] = (skillPart || "").split("@");
84
+ return { org, repo, skill, ref };
85
+ }
86
+
87
+ /**
88
+ * Resolve scope based on flags and cwd
89
+ * @returns {string} Skills directory path
90
+ */
91
+ export function resolveScope() {
92
+ if (GLOBAL) return GLOBAL_DIR;
93
+ if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
94
+ return GLOBAL_DIR;
95
+ }
96
+
97
+ /**
98
+ * Create backup of skill directory
99
+ * @param {string} skillDir - Source directory
100
+ * @param {string} skillName - Skill name for backup naming
101
+ * @returns {string|null} Backup path or null if dry run
102
+ */
103
+ export function createBackup(skillDir, skillName) {
104
+ if (DRY) return null;
105
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
106
+ const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
107
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
108
+ fs.cpSync(skillDir, bp, { recursive: true });
109
+ return bp;
110
+ }
111
+
112
+ /**
113
+ * List backups for a skill
114
+ * @param {string} [skillName] - Filter by skill name
115
+ * @returns {import('./types.js').Backup[]}
116
+ */
117
+ export function listBackups(skillName = null) {
118
+ if (!fs.existsSync(BACKUP_DIR)) return [];
119
+ const backups = [];
120
+ for (const name of fs.readdirSync(BACKUP_DIR)) {
121
+ if (skillName && !name.startsWith(skillName + "_")) continue;
122
+ const bp = path.join(BACKUP_DIR, name);
123
+ backups.push({ name, path: bp, createdAt: fs.statSync(bp).mtime, size: getDirSize(bp) });
124
+ }
125
+ return backups.sort((a, b) => b.createdAt - a.createdAt);
126
+ }
127
+
128
+ /**
129
+ * Load skill lock file
130
+ * @returns {import('./types.js').SkillLock}
131
+ */
132
+ export function loadSkillLock() {
133
+ const f = path.join(cwd, ".agent", "skill-lock.json");
134
+ if (!fs.existsSync(f)) throw new Error("skill-lock.json not found");
135
+ return JSON.parse(fs.readFileSync(f, "utf-8"));
136
+ }
137
+
138
+ /**
139
+ * Load registries list
140
+ * @returns {string[]}
141
+ */
142
+ export function loadRegistries() {
143
+ try { return fs.existsSync(REGISTRIES_FILE) ? JSON.parse(fs.readFileSync(REGISTRIES_FILE, "utf-8")) : []; }
144
+ catch { return []; }
145
+ }
146
+
147
+ /**
148
+ * Save registries list
149
+ * @param {string[]} regs
150
+ */
151
+ export function saveRegistries(regs) {
152
+ fs.mkdirSync(path.dirname(REGISTRIES_FILE), { recursive: true });
153
+ fs.writeFileSync(REGISTRIES_FILE, JSON.stringify(regs, null, 2));
154
+ }
155
+
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { GLOBAL_DIR } from "./config.js";
4
+ import { merkleHash } from "./helpers.js";
5
+
6
+ /**
7
+ * Install a skill to the destination using the specified method.
8
+ * @param {string} src - Source directory (temp)
9
+ * @param {string} dest - Destination directory (project)
10
+ * @param {string} method - 'symlink' or 'copy'
11
+ * @param {Object} metadata - Metadata for .skill-source.json
12
+ */
13
+ export async function installSkill(src, dest, method, metadata) {
14
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
15
+
16
+ if (method === "symlink") {
17
+ // For symlink: Move to global persistent storage first
18
+ // Storage path: ~/.gemini/antigravity/skills/storage/<org>/<repo>/<skill>
19
+ // Metadata must contain org, repo, skill
20
+ const { repo: repoStr, skill } = metadata;
21
+ const [org, repo] = repoStr.split("/");
22
+
23
+ const storageBase = path.join(GLOBAL_DIR, "storage", org, repo, skill);
24
+
25
+ // Ensure fresh copy in storage
26
+ if (fs.existsSync(storageBase)) fs.rmSync(storageBase, { recursive: true, force: true });
27
+ fs.mkdirSync(path.dirname(storageBase), { recursive: true });
28
+
29
+ // Copy from tmp to storage
30
+ await fs.promises.cp(src, storageBase, { recursive: true });
31
+
32
+ // Create junction
33
+ fs.symlinkSync(storageBase, dest, "junction");
34
+ } else {
35
+ // Copy directly
36
+ await fs.promises.cp(src, dest, { recursive: true });
37
+ }
38
+
39
+ // Write metadata
40
+ const hash = merkleHash(dest);
41
+ const metaFile = path.join(dest, ".skill-source.json");
42
+
43
+ fs.writeFileSync(metaFile, JSON.stringify({
44
+ ...metadata,
45
+ checksum: hash,
46
+ installedAt: new Date().toISOString(),
47
+ method: method
48
+ }, null, 2));
49
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @fileoverview Skill detection and parsing
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { resolveScope } from "./helpers.js";
8
+ import { getDirSize } from "./helpers.js";
9
+
10
+ /**
11
+ * Parse SKILL.md YAML frontmatter
12
+ * @param {string} p - Path to SKILL.md
13
+ * @returns {import('./types.js').SkillMeta}
14
+ */
15
+ export function parseSkillMdFrontmatter(p) {
16
+ try {
17
+ const content = fs.readFileSync(p, "utf-8");
18
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
19
+ if (!match) return {};
20
+
21
+ /** @type {import('./types.js').SkillMeta} */
22
+ const meta = {};
23
+
24
+ for (const line of match[1].split(/\r?\n/)) {
25
+ const i = line.indexOf(":");
26
+ if (i === -1) continue;
27
+ const key = line.substring(0, i).trim();
28
+ const val = line.substring(i + 1).trim();
29
+ if (key === "tags") meta.tags = val.split(",").map(t => t.trim()).filter(Boolean);
30
+ else if (key && val) meta[key] = val;
31
+ }
32
+ return meta;
33
+ } catch { return {}; }
34
+ }
35
+
36
+ /**
37
+ * Detect skill directory structure
38
+ * @param {string} dir - Skill directory
39
+ * @returns {import('./types.js').SkillStructure}
40
+ */
41
+ export function detectSkillStructure(dir) {
42
+ /** @type {import('./types.js').SkillStructure} */
43
+ const s = {
44
+ hasResources: false,
45
+ hasExamples: false,
46
+ hasScripts: false,
47
+ hasConstitution: false,
48
+ hasDoctrines: false,
49
+ hasEnforcement: false,
50
+ hasProposals: false,
51
+ directories: [],
52
+ files: []
53
+ };
54
+
55
+ try {
56
+ for (const item of fs.readdirSync(dir)) {
57
+ const full = path.join(dir, item);
58
+ if (fs.statSync(full).isDirectory()) {
59
+ s.directories.push(item);
60
+ const l = item.toLowerCase();
61
+ if (l === "resources") s.hasResources = true;
62
+ if (l === "examples") s.hasExamples = true;
63
+ if (l === "scripts") s.hasScripts = true;
64
+ if (l === "constitution") s.hasConstitution = true;
65
+ if (l === "doctrines") s.hasDoctrines = true;
66
+ if (l === "enforcement") s.hasEnforcement = true;
67
+ if (l === "proposals") s.hasProposals = true;
68
+ } else {
69
+ s.files.push(item);
70
+ }
71
+ }
72
+ } catch { }
73
+ return s;
74
+ }
75
+
76
+ /**
77
+ * Get all installed skills
78
+ * @returns {import('./types.js').Skill[]}
79
+ */
80
+ export function getInstalledSkills() {
81
+ const scope = resolveScope();
82
+ /** @type {import('./types.js').Skill[]} */
83
+ const skills = [];
84
+
85
+ if (!fs.existsSync(scope)) return skills;
86
+
87
+ for (const name of fs.readdirSync(scope)) {
88
+ const dir = path.join(scope, name);
89
+ if (!fs.statSync(dir).isDirectory()) continue;
90
+
91
+ const metaFile = path.join(dir, ".skill-source.json");
92
+ const skillFile = path.join(dir, "SKILL.md");
93
+
94
+ if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
95
+ const meta = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, "utf-8")) : {};
96
+ const hasSkillMd = fs.existsSync(skillFile);
97
+ const skillMeta = hasSkillMd ? parseSkillMdFrontmatter(skillFile) : {};
98
+
99
+ skills.push({
100
+ name,
101
+ path: dir,
102
+ ...meta,
103
+ hasSkillMd,
104
+ description: skillMeta.description || meta.description || "",
105
+ tags: skillMeta.tags || [],
106
+ author: skillMeta.author || meta.publisher || "",
107
+ version: skillMeta.version || meta.ref || "unknown",
108
+ structure: detectSkillStructure(dir),
109
+ size: getDirSize(dir)
110
+ });
111
+ }
112
+ }
113
+ return skills;
114
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview JSDoc Type Definitions for add-skill CLI
3
+ * Provides IDE autocomplete support
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} SkillStructure
8
+ * @property {boolean} hasResources
9
+ * @property {boolean} hasExamples
10
+ * @property {boolean} hasScripts
11
+ * @property {boolean} hasConstitution
12
+ * @property {boolean} hasDoctrines
13
+ * @property {boolean} hasEnforcement
14
+ * @property {boolean} hasProposals
15
+ * @property {string[]} directories
16
+ * @property {string[]} files
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} Skill
21
+ * @property {string} name - Skill folder name
22
+ * @property {string} path - Absolute path to skill
23
+ * @property {boolean} hasSkillMd - Has SKILL.md file
24
+ * @property {string} description - From SKILL.md frontmatter
25
+ * @property {string[]} tags - From SKILL.md frontmatter
26
+ * @property {string} author - Author or publisher
27
+ * @property {string} version - Version or ref
28
+ * @property {SkillStructure} structure - Directory structure
29
+ * @property {number} size - Total size in bytes
30
+ * @property {string} [repo] - Source repository
31
+ * @property {string} [skill] - Skill name in repo
32
+ * @property {string} [ref] - Git ref
33
+ * @property {string} [checksum] - Merkle hash
34
+ * @property {string} [installedAt] - ISO timestamp
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} SkillMeta
39
+ * @property {string} [name]
40
+ * @property {string} [description]
41
+ * @property {string} [version]
42
+ * @property {string} [author]
43
+ * @property {string[]} [tags]
44
+ * @property {string} [type]
45
+ * @property {string} [authority]
46
+ * @property {string} [parent]
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} ParsedSpec
51
+ * @property {string} org - GitHub org
52
+ * @property {string} repo - GitHub repo
53
+ * @property {string} [skill] - Skill name
54
+ * @property {string} [ref] - Git ref
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} Backup
59
+ * @property {string} name
60
+ * @property {string} path
61
+ * @property {Date} createdAt
62
+ * @property {number} size
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} SkillLock
67
+ * @property {number} lockVersion
68
+ * @property {string} generatedAt
69
+ * @property {string} generator
70
+ * @property {Object.<string, SkillLockEntry>} skills
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} SkillLockEntry
75
+ * @property {string} repo
76
+ * @property {string} skill
77
+ * @property {string} ref
78
+ * @property {string} checksum
79
+ * @property {string} [publisher]
80
+ */
81
+
82
+ export { };
package/bin/lib/ui.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @fileoverview UI components - Install Agent Skill theme
3
+ */
4
+
5
+ import kleur from "kleur";
6
+ import { intro, outro, multiselect, select, confirm, isCancel, cancel, text } from "@clack/prompts";
7
+ import ora from "ora";
8
+
9
+ export { intro, outro, multiselect, select, confirm, isCancel, cancel, text };
10
+
11
+ /**
12
+ * Create a spinner
13
+ */
14
+ export function spinner() {
15
+ return {
16
+ _s: null,
17
+ start(msg) {
18
+ this._s = ora({
19
+ text: " " + msg,
20
+ prefixText: "",
21
+ color: "blue",
22
+ spinner: {
23
+ interval: 80,
24
+ frames: ['◒', '◐', '◓', '◑']
25
+ }
26
+ }).start();
27
+ },
28
+ stop(msg) {
29
+ if (this._s) {
30
+ this._s.stopAndPersist({
31
+ symbol: c.cyan(S.diamond),
32
+ text: " " + msg
33
+ });
34
+ }
35
+ },
36
+ fail(msg) {
37
+ if (this._s) {
38
+ this._s.stopAndPersist({
39
+ symbol: c.red(S.cross),
40
+ text: " " + msg
41
+ });
42
+ }
43
+ },
44
+ message(msg) {
45
+ if (this._s) this._s.text = " " + msg;
46
+ }
47
+ };
48
+ }
49
+
50
+ // --- Symbols ---
51
+
52
+ /** UI symbols for tree structure */
53
+ export const S = {
54
+ branch: "│",
55
+ diamond: "◇",
56
+ diamondFilled: "◆",
57
+ check: "✓",
58
+ cross: "x",
59
+ arrow: "→"
60
+ };
61
+
62
+ // --- Colors ---
63
+
64
+ /** Color helper functions */
65
+ export const c = {
66
+ cyan: kleur.cyan,
67
+ gray: kleur.gray,
68
+ green: kleur.green,
69
+ red: kleur.red,
70
+ yellow: kleur.yellow,
71
+ magenta: kleur.magenta,
72
+ blue: kleur.blue,
73
+ white: kleur.white,
74
+ bgBlue: kleur.bgBlue,
75
+ bold: kleur.bold,
76
+ dim: kleur.dim,
77
+ inverse: kleur.inverse
78
+ };
79
+
80
+ // --- UI Functions ---
81
+
82
+ /**
83
+ * Print a step in the tree
84
+ * @param {string} text - Step text
85
+ * @param {string} [icon] - Icon to use
86
+ * @param {keyof typeof c} [color] - Color name
87
+ */
88
+ export function step(text, icon = S.diamond, color = "cyan") {
89
+ const colorFn = c[color] || c.cyan;
90
+ console.log(`${colorFn(icon)} ${text}`);
91
+ }
92
+
93
+ /**
94
+ * Print an active step (Blue Filled Diamond)
95
+ * @param {string} text - Step text
96
+ */
97
+ export function activeStep(text) {
98
+ console.log(`${c.blue(S.diamondFilled)} ${text}`);
99
+ }
100
+
101
+ /**
102
+ * Print empty branch line
103
+ */
104
+ export function stepLine() {
105
+ console.log(`${c.gray(S.branch)}`);
106
+ }
107
+
108
+ /**
109
+ * Print fatal error and exit
110
+ * @param {string} msg - Error message
111
+ */
112
+ export function fatal(msg) {
113
+ console.log(`${c.red(S.cross)} ${c.red(msg)}`);
114
+ process.exit(1);
115
+ }
116
+
117
+ /**
118
+ * Print success message
119
+ * @param {string} msg - Success message
120
+ */
121
+ export function success(msg) {
122
+ console.log(`${c.cyan(S.diamond)} ${c.cyan(msg)}`);
123
+ }
124
+
125
+ /**
126
+ * Output JSON if JSON_OUTPUT mode
127
+ * @param {any} data - Data to output
128
+ * @param {boolean} jsonMode - Whether to output JSON
129
+ */
130
+ export function outputJSON(data, jsonMode) {
131
+ if (jsonMode) console.log(JSON.stringify(data, null, 2));
132
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "install-agent-skill",
3
+ "version": "1.0.0",
4
+ "description": "Enterprise-grade Agent Skill Manager with Antigravity Skills support, Progressive Disclosure detection, and semantic routing validation",
5
+ "license": "MIT",
6
+ "author": "DataGuruIn <contact@dataguruin.com>",
7
+ "homepage": "https://github.com/dataguruin/add-skill",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/dataguruin/add-skill.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/dataguruin/add-skill/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "add-skill": "./bin/cli.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "specs/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "keywords": [
29
+ "agent",
30
+ "ai",
31
+ "skills",
32
+ "cli",
33
+ "tooling",
34
+ "registry",
35
+ "security",
36
+ "devops",
37
+ "automation",
38
+ "antigravity"
39
+ ],
40
+ "scripts": {
41
+ "lint": "echo \"No lint configured\"",
42
+ "test": "echo \"No tests yet\"",
43
+ "ci": "node bin/add-skill.js verify --strict && node bin/add-skill.js doctor --strict"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@clack/prompts": "^0.9.1",
50
+ "boxen": "^8.0.1",
51
+ "chalk": "^5.4.1",
52
+ "kleur": "^4.1.5",
53
+ "ora": "^9.1.0",
54
+ "prompts": "^2.4.2"
55
+ }
56
+ }