aem-ext-daemon 0.2.0 → 0.3.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,23 @@
1
+ /**
2
+ * Skills capabilities — sync skills from local .skills folder,
3
+ * seed default skills from GitHub if none exist.
4
+ */
5
+ export interface SkillEntry {
6
+ name: string;
7
+ content: string;
8
+ scripts?: Array<{
9
+ name: string;
10
+ content: string;
11
+ }>;
12
+ }
13
+ /**
14
+ * Read all skills from the .skills folder in the given workspace root.
15
+ * Returns an array of skill entries with name, SKILL.md content, and any scripts.
16
+ */
17
+ export declare function syncSkills(workspaceRoot: string): string;
18
+ /**
19
+ * Seed default skills into the workspace .skills folder by cloning from GitHub.
20
+ * Only seeds if .skills folder doesn't already exist.
21
+ * Uses sparse checkout to only pull the skills subfolder.
22
+ */
23
+ export declare function seedSkills(workspaceRoot: string): string;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Skills capabilities — sync skills from local .skills folder,
3
+ * seed default skills from GitHub if none exist.
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { execSync } from "node:child_process";
8
+ const DEFAULT_SKILLS_REPO = "https://github.com/znikolovski/skills.git";
9
+ const DEFAULT_SKILLS_PATH = "skills/aem/ui-extensibility/skills";
10
+ /**
11
+ * Read all skills from the .skills folder in the given workspace root.
12
+ * Returns an array of skill entries with name, SKILL.md content, and any scripts.
13
+ */
14
+ export function syncSkills(workspaceRoot) {
15
+ const skillsDir = path.join(workspaceRoot, ".skills");
16
+ if (!fs.existsSync(skillsDir)) {
17
+ return JSON.stringify({ found: false, skills: [] });
18
+ }
19
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
20
+ const skills = [];
21
+ for (const entry of entries) {
22
+ if (!entry.isDirectory())
23
+ continue;
24
+ const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
25
+ if (!fs.existsSync(skillMdPath))
26
+ continue;
27
+ const skill = {
28
+ name: entry.name,
29
+ content: fs.readFileSync(skillMdPath, "utf-8"),
30
+ };
31
+ // Check for scripts subfolder
32
+ const scriptsDir = path.join(skillsDir, entry.name, "scripts");
33
+ if (fs.existsSync(scriptsDir)) {
34
+ const scriptFiles = fs.readdirSync(scriptsDir).filter((f) => fs.statSync(path.join(scriptsDir, f)).isFile());
35
+ skill.scripts = scriptFiles.map((f) => ({
36
+ name: f,
37
+ content: fs.readFileSync(path.join(scriptsDir, f), "utf-8"),
38
+ }));
39
+ }
40
+ skills.push(skill);
41
+ }
42
+ return JSON.stringify({ found: true, skills });
43
+ }
44
+ /**
45
+ * Seed default skills into the workspace .skills folder by cloning from GitHub.
46
+ * Only seeds if .skills folder doesn't already exist.
47
+ * Uses sparse checkout to only pull the skills subfolder.
48
+ */
49
+ export function seedSkills(workspaceRoot) {
50
+ const skillsDir = path.join(workspaceRoot, ".skills");
51
+ if (fs.existsSync(skillsDir)) {
52
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
53
+ const skillCount = entries.filter((e) => e.isDirectory()).length;
54
+ if (skillCount > 0) {
55
+ return JSON.stringify({
56
+ seeded: false,
57
+ reason: "Skills folder already exists with " + skillCount + " skills",
58
+ });
59
+ }
60
+ }
61
+ // Clone into a temp dir, then copy the skills subfolder
62
+ const tmpDir = path.join(workspaceRoot, ".skills-tmp-" + Date.now());
63
+ try {
64
+ // Shallow clone the repo
65
+ execSync(`git clone --depth 1 --filter=blob:none --sparse "${DEFAULT_SKILLS_REPO}" "${tmpDir}"`, { timeout: 60_000, stdio: "pipe" });
66
+ // Sparse checkout just the skills path
67
+ execSync(`git -C "${tmpDir}" sparse-checkout set "${DEFAULT_SKILLS_PATH}"`, {
68
+ timeout: 30_000,
69
+ stdio: "pipe",
70
+ });
71
+ // Copy skills into .skills
72
+ const srcDir = path.join(tmpDir, DEFAULT_SKILLS_PATH);
73
+ if (!fs.existsSync(srcDir)) {
74
+ throw new Error("Skills path not found in cloned repo: " + DEFAULT_SKILLS_PATH);
75
+ }
76
+ fs.mkdirSync(skillsDir, { recursive: true });
77
+ const skillDirs = fs.readdirSync(srcDir, { withFileTypes: true });
78
+ for (const dir of skillDirs) {
79
+ if (!dir.isDirectory())
80
+ continue;
81
+ copyDirRecursive(path.join(srcDir, dir.name), path.join(skillsDir, dir.name));
82
+ }
83
+ // Count what we seeded
84
+ const seededSkills = fs
85
+ .readdirSync(skillsDir, { withFileTypes: true })
86
+ .filter((e) => e.isDirectory())
87
+ .map((e) => e.name);
88
+ return JSON.stringify({ seeded: true, skills: seededSkills });
89
+ }
90
+ catch (err) {
91
+ return JSON.stringify({
92
+ seeded: false,
93
+ reason: err.message,
94
+ });
95
+ }
96
+ finally {
97
+ // Clean up temp dir
98
+ try {
99
+ fs.rmSync(tmpDir, { recursive: true, force: true });
100
+ }
101
+ catch {
102
+ // ignore cleanup errors
103
+ }
104
+ }
105
+ }
106
+ function copyDirRecursive(src, dest) {
107
+ fs.mkdirSync(dest, { recursive: true });
108
+ const entries = fs.readdirSync(src, { withFileTypes: true });
109
+ for (const entry of entries) {
110
+ const srcPath = path.join(src, entry.name);
111
+ const destPath = path.join(dest, entry.name);
112
+ if (entry.isDirectory()) {
113
+ copyDirRecursive(srcPath, destPath);
114
+ }
115
+ else {
116
+ fs.copyFileSync(srcPath, destPath);
117
+ }
118
+ }
119
+ }
@@ -9,6 +9,7 @@ import { getWorkspaceRoot, setWorkspaceRoot } from "./identity.js";
9
9
  import * as fsCap from "./capabilities/fs.js";
10
10
  import * as gitCap from "./capabilities/git.js";
11
11
  import * as shellCap from "./capabilities/shell.js";
12
+ import * as skillsCap from "./capabilities/skills.js";
12
13
  /**
13
14
  * Validate that a given path is within the workspace root.
14
15
  * Prevents path traversal attacks.
@@ -90,6 +91,19 @@ export async function dispatch(command, payload, connection) {
90
91
  : getWorkspaceRoot();
91
92
  return shellCap.exec(payload.command, cwd);
92
93
  }
94
+ // ─── Skills ────────────────────────────────────────
95
+ case "skills:sync": {
96
+ const workspace = getWorkspaceRoot();
97
+ if (!workspace)
98
+ throw new Error("No workspace root configured.");
99
+ return skillsCap.syncSkills(workspace);
100
+ }
101
+ case "skills:seed": {
102
+ const workspace = getWorkspaceRoot();
103
+ if (!workspace)
104
+ throw new Error("No workspace root configured.");
105
+ return skillsCap.seedSkills(workspace);
106
+ }
93
107
  default:
94
108
  throw new Error(`Unknown command: ${command}`);
95
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aem-ext-daemon",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local daemon for AEM Extension Builder — connects your machine to the cloud UI",
5
5
  "type": "module",
6
6
  "bin": {