@harness-lab/cli 0.2.1 → 0.2.2

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.
Files changed (47) hide show
  1. package/README.md +14 -5
  2. package/assets/workshop-bundle/SKILL.md +295 -0
  3. package/assets/workshop-bundle/bundle-manifest.json +172 -0
  4. package/assets/workshop-bundle/content/challenge-cards/.gitkeep +0 -0
  5. package/assets/workshop-bundle/content/challenge-cards/deck.md +38 -0
  6. package/assets/workshop-bundle/content/challenge-cards/print-spec.md +28 -0
  7. package/assets/workshop-bundle/content/facilitation/.gitkeep +0 -0
  8. package/assets/workshop-bundle/content/facilitation/codex-setup-verification.md +54 -0
  9. package/assets/workshop-bundle/content/facilitation/master-guide.md +131 -0
  10. package/assets/workshop-bundle/content/project-briefs/.gitkeep +0 -0
  11. package/assets/workshop-bundle/content/project-briefs/code-review-helper.md +31 -0
  12. package/assets/workshop-bundle/content/project-briefs/devtoolbox-cli.md +31 -0
  13. package/assets/workshop-bundle/content/project-briefs/doc-generator.md +31 -0
  14. package/assets/workshop-bundle/content/project-briefs/metrics-dashboard.md +31 -0
  15. package/assets/workshop-bundle/content/project-briefs/standup-bot.md +31 -0
  16. package/assets/workshop-bundle/content/style-examples.md +127 -0
  17. package/assets/workshop-bundle/content/style-guide.md +106 -0
  18. package/assets/workshop-bundle/content/talks/.gitkeep +0 -0
  19. package/assets/workshop-bundle/content/talks/codex-demo-script.md +43 -0
  20. package/assets/workshop-bundle/content/talks/context-is-king.md +42 -0
  21. package/assets/workshop-bundle/docs/harness-cli-foundation.md +112 -0
  22. package/assets/workshop-bundle/docs/learner-reference-gallery.md +82 -0
  23. package/assets/workshop-bundle/docs/learner-resource-kit.md +126 -0
  24. package/assets/workshop-bundle/docs/workshop-event-context-contract.md +123 -0
  25. package/assets/workshop-bundle/materials/participant-resource-kit.md +72 -0
  26. package/assets/workshop-bundle/workshop-blueprint/README.md +48 -0
  27. package/assets/workshop-bundle/workshop-blueprint/agenda.json +70 -0
  28. package/assets/workshop-bundle/workshop-blueprint/control-surfaces.md +101 -0
  29. package/assets/workshop-bundle/workshop-blueprint/day-structure.md +129 -0
  30. package/assets/workshop-bundle/workshop-blueprint/edit-boundaries.md +64 -0
  31. package/assets/workshop-bundle/workshop-blueprint/operator-guide.md +74 -0
  32. package/assets/workshop-bundle/workshop-blueprint/teaching-spine.md +134 -0
  33. package/assets/workshop-bundle/workshop-skill/.gitkeep +0 -0
  34. package/assets/workshop-bundle/workshop-skill/analyze-checklist.md +21 -0
  35. package/assets/workshop-bundle/workshop-skill/closing-skill.md +30 -0
  36. package/assets/workshop-bundle/workshop-skill/commands.md +44 -0
  37. package/assets/workshop-bundle/workshop-skill/facilitator.md +416 -0
  38. package/assets/workshop-bundle/workshop-skill/follow-up-package.md +35 -0
  39. package/assets/workshop-bundle/workshop-skill/install.md +58 -0
  40. package/assets/workshop-bundle/workshop-skill/recap.md +22 -0
  41. package/assets/workshop-bundle/workshop-skill/reference.md +80 -0
  42. package/assets/workshop-bundle/workshop-skill/setup.md +84 -0
  43. package/assets/workshop-bundle/workshop-skill/template-agents.md +35 -0
  44. package/package.json +8 -3
  45. package/src/run-cli.js +16 -9
  46. package/src/skill-install.js +98 -57
  47. package/src/workshop-bundle.js +233 -0
@@ -0,0 +1,35 @@
1
+ # AGENTS.md Starter
2
+
3
+ Použijte to jako krátkou mapu repa. Ne jako encyklopedii. Když detail patří jinam, odkažte na konkrétní soubor místo kopírování dlouhého textu.
4
+
5
+ Nejdůležitější pravidlo:
6
+ - napište, kam má agent sáhnout jako první
7
+ - napište, co je source of truth
8
+ - napište, jak se práce ověří
9
+ - když se text nafukuje, přidejte deeper doc a odkažte na něj
10
+
11
+ ## Goal
12
+
13
+ Popiš, co má agent v tomto repozitáři vytvořit nebo udržovat.
14
+
15
+ ## Context
16
+
17
+ - Klíčové soubory a složky
18
+ - Rozhodnutí, která už padla
19
+ - Systémy nebo integrace, na které se navazuje
20
+ - Kam má agent sáhnout jako první
21
+ - Které docs nebo runbooky jsou source of truth
22
+
23
+ ## Constraints
24
+
25
+ - Build/test/lint příkazy
26
+ - Jazykové, architektonické a bezpečnostní standardy
27
+ - Co agent nesmí dělat bez explicitního souhlasu
28
+ - Public/private nebo auth boundary, pokud existuje
29
+
30
+ ## Done When
31
+
32
+ - Jak poznáme, že je práce hotová
33
+ - Jaké ověření musí proběhnout
34
+ - Jak má vypadat handoff pro dalšího člověka nebo agenta
35
+ - Jaký bude další safe move, pokud práce zůstane rozdělaná
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -20,12 +20,13 @@
20
20
  "ai-agents"
21
21
  ],
22
22
  "engines": {
23
- "node": ">=24"
23
+ "node": "22.x"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"
27
27
  },
28
28
  "files": [
29
+ "assets",
29
30
  "bin",
30
31
  "src",
31
32
  "README.md"
@@ -37,6 +38,10 @@
37
38
  "chalk": "^5.6.2"
38
39
  },
39
40
  "scripts": {
40
- "test": "node --test"
41
+ "preverify:workshop-bundle": "node ./scripts/sync-workshop-bundle.mjs --with-repo-bundle",
42
+ "prepack": "node ./scripts/sync-workshop-bundle.mjs",
43
+ "sync:workshop-bundle": "node ./scripts/sync-workshop-bundle.mjs --with-repo-bundle",
44
+ "verify:workshop-bundle": "node ./scripts/verify-workshop-bundle.mjs",
45
+ "test": "npm run verify:workshop-bundle && node --test"
41
46
  }
42
47
  }
package/src/run-cli.js CHANGED
@@ -173,7 +173,7 @@ function printUsage(io, ui) {
173
173
  "harness auth login [--auth device|basic|neon] [--dashboard-url URL] [--username USER] [--email EMAIL] [--password PASS] [--no-open]",
174
174
  "harness auth logout",
175
175
  "harness auth status",
176
- "harness skill install [--force]",
176
+ "harness skill install [--target PATH] [--force]",
177
177
  "harness workshop status",
178
178
  "harness workshop archive [--notes TEXT]",
179
179
  "harness workshop create-instance [<instance-id>] [--template-id ID] [--event-title TEXT] [--city CITY]",
@@ -190,23 +190,30 @@ function printVersion(io) {
190
190
 
191
191
  async function handleSkillInstall(io, ui, deps, flags) {
192
192
  try {
193
- const result = await installWorkshopSkill(deps.cwd ?? process.cwd(), { force: flags.force === true });
193
+ const result = await installWorkshopSkill(deps.cwd ?? process.cwd(), {
194
+ force: flags.force === true,
195
+ target: readStringFlag(flags, "target"),
196
+ });
194
197
  ui.heading("Workshop Skill");
195
- if (result.mode === "already_bundled") {
196
- ui.status("ok", "Harness Lab workshop skill is already bundled in this repo.");
198
+ if (result.mode === "already_current") {
199
+ ui.status("ok", "Harness Lab workshop skill is already current at the target path.");
200
+ } else if (result.mode === "refreshed") {
201
+ ui.status("ok", "Refreshed the installed Harness Lab workshop skill bundle.");
197
202
  } else {
198
203
  ui.status("ok", "Installed the Harness Lab workshop skill bundle.");
199
204
  }
205
+ ui.keyValue("Target", result.targetRoot);
200
206
  ui.keyValue("Location", result.installPath);
201
207
  ui.keyValue("Discovery", ".agents/skills");
208
+ ui.keyValue("Bundle source", result.sourceMode === "packaged_bundle" ? "packaged portable bundle" : "source checkout fallback");
202
209
  ui.blank();
203
210
  ui.section("Next steps");
204
211
  ui.numberedList([
205
- "Open Codex or pi in this repo.",
206
- "Start with the workshop reference card.",
207
- "Codex: `$workshop reference`.",
208
- "pi: `/skill:workshop`, then ask for the workshop reference card.",
209
- "Need setup help? Codex: `$workshop setup`. pi: `/skill:workshop`, then ask for setup help.",
212
+ "Open Codex or pi in the target repo.",
213
+ "Start with the workshop command menu.",
214
+ "Codex: `$workshop commands`.",
215
+ "pi: `/skill:workshop`, then ask for the workshop commands.",
216
+ "Next: `$workshop reference`, `$workshop brief`, and `$workshop resources`.",
210
217
  ]);
211
218
  return 0;
212
219
  } catch (error) {
@@ -1,5 +1,16 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import {
4
+ createWorkshopBundleManifestFromDirectory,
5
+ createWorkshopBundleManifestFromSource,
6
+ createWorkshopBundleFromSource,
7
+ getInstalledSkillPath,
8
+ getPackagedWorkshopBundlePath,
9
+ getRepoWorkshopSourceRoot,
10
+ pathExists,
11
+ readWorkshopBundleManifest,
12
+ WORKSHOP_SKILL_NAME,
13
+ } from "./workshop-bundle.js";
3
14
 
4
15
  export class SkillInstallError extends Error {
5
16
  constructor(message, options = {}) {
@@ -9,94 +20,124 @@ export class SkillInstallError extends Error {
9
20
  }
10
21
  }
11
22
 
12
- const SKILL_NAME = "harness-lab-workshop";
23
+ async function resolveBundleSource() {
24
+ const packagedBundlePath = getPackagedWorkshopBundlePath();
25
+ if (await pathExists(path.join(packagedBundlePath, "SKILL.md"))) {
26
+ return {
27
+ mode: "packaged_bundle",
28
+ sourcePath: packagedBundlePath,
29
+ sourceRoot: packagedBundlePath,
30
+ };
31
+ }
13
32
 
14
- async function pathExists(targetPath) {
15
- try {
16
- await fs.access(targetPath);
17
- return true;
18
- } catch {
19
- return false;
33
+ const repoSourceRoot = getRepoWorkshopSourceRoot();
34
+ if (await pathExists(path.join(repoSourceRoot, "workshop-skill", "SKILL.md"))) {
35
+ return {
36
+ mode: "repo_source",
37
+ sourcePath: repoSourceRoot,
38
+ sourceRoot: repoSourceRoot,
39
+ };
20
40
  }
41
+
42
+ return null;
21
43
  }
22
44
 
23
- export async function findHarnessLabRepoRoot(startDir) {
24
- let currentDir = path.resolve(startDir);
45
+ async function ensureDirectory(targetPath) {
46
+ await fs.mkdir(targetPath, { recursive: true });
47
+ const stat = await fs.stat(targetPath);
48
+ if (!stat.isDirectory()) {
49
+ throw new SkillInstallError(`Install target is not a directory: ${targetPath}`, {
50
+ code: "invalid_target",
51
+ });
52
+ }
53
+ }
25
54
 
26
- while (true) {
27
- if (await pathExists(path.join(currentDir, "workshop-skill", "SKILL.md"))) {
28
- return currentDir;
29
- }
55
+ async function hasInstalledSkill(targetRoot) {
56
+ return pathExists(path.join(getInstalledSkillPath(targetRoot), "SKILL.md"));
57
+ }
30
58
 
31
- const parentDir = path.dirname(currentDir);
32
- if (parentDir === currentDir) {
33
- return null;
59
+ async function getSourceBundleManifest(resolvedBundle) {
60
+ if (resolvedBundle.mode === "packaged_bundle") {
61
+ const manifest = await readWorkshopBundleManifest(resolvedBundle.sourcePath);
62
+ if (manifest) {
63
+ return manifest;
34
64
  }
35
65
 
36
- currentDir = parentDir;
66
+ return createWorkshopBundleManifestFromDirectory(resolvedBundle.sourcePath);
37
67
  }
38
- }
39
68
 
40
- export function getInstalledSkillPath(repoRoot) {
41
- return path.join(repoRoot, ".agents", "skills", SKILL_NAME);
69
+ return createWorkshopBundleManifestFromSource(resolvedBundle.sourceRoot);
42
70
  }
43
71
 
44
- async function hasBundledRepoSkill(repoRoot) {
45
- return pathExists(path.join(getInstalledSkillPath(repoRoot), "SKILL.md"));
72
+ async function installFromResolvedBundle(resolvedBundle, installPath) {
73
+ if (resolvedBundle.mode === "packaged_bundle") {
74
+ await fs.cp(resolvedBundle.sourcePath, installPath, { recursive: true });
75
+ return;
76
+ }
77
+
78
+ await createWorkshopBundleFromSource(resolvedBundle.sourceRoot, installPath);
46
79
  }
47
80
 
48
81
  export async function installWorkshopSkill(startDir, options = {}) {
49
- const repoRoot = await findHarnessLabRepoRoot(startDir);
50
- if (!repoRoot) {
82
+ const resolvedBundle = await resolveBundleSource();
83
+ if (!resolvedBundle) {
51
84
  throw new SkillInstallError(
52
- "Harness CLI could not find `workshop-skill/SKILL.md`. Run this command inside a Harness Lab repo checkout.",
53
- { code: "repo_not_found" },
85
+ "Harness CLI could not find a portable workshop bundle. Reinstall `@harness-lab/cli` or run from a Harness Lab source checkout.",
86
+ { code: "bundle_not_found" },
54
87
  );
55
88
  }
56
89
 
57
- const installPath = getInstalledSkillPath(repoRoot);
58
- const bundledRepoSkill = await hasBundledRepoSkill(repoRoot);
90
+ const targetRoot = path.resolve(startDir, options.target ?? ".");
91
+ await ensureDirectory(targetRoot);
92
+
93
+ const installPath = getInstalledSkillPath(targetRoot);
94
+ const existingInstall = await hasInstalledSkill(targetRoot);
95
+ const sourceManifest = await getSourceBundleManifest(resolvedBundle);
96
+
97
+ if (existingInstall && options.force !== true) {
98
+ const installedManifest = await createWorkshopBundleManifestFromDirectory(installPath);
99
+ if (installedManifest.contentHash === sourceManifest.contentHash) {
100
+ return {
101
+ installPath,
102
+ skillName: WORKSHOP_SKILL_NAME,
103
+ mode: "already_current",
104
+ sourceMode: resolvedBundle.mode,
105
+ targetRoot,
106
+ };
107
+ }
108
+
109
+ await fs.rm(installPath, { recursive: true, force: true });
59
110
 
60
- if (bundledRepoSkill) {
61
111
  return {
62
- repoRoot,
63
- installPath,
64
- skillName: SKILL_NAME,
65
- mode: "already_bundled",
112
+ ...(await installFreshBundle(resolvedBundle, installPath, targetRoot)),
113
+ mode: "refreshed",
66
114
  };
67
115
  }
68
116
 
69
- if ((await pathExists(installPath)) && options.force !== true) {
70
- throw new SkillInstallError(
71
- `Skill already installed at ${installPath}. Re-run with --force to replace it.`,
72
- { code: "already_installed" },
73
- );
117
+ if (existingInstall && options.force === true) {
118
+ await fs.rm(installPath, { recursive: true, force: true });
74
119
  }
75
120
 
76
- if (options.force === true) {
77
- await fs.rm(installPath, { recursive: true, force: true });
121
+ const result = await installFreshBundle(resolvedBundle, installPath, targetRoot);
122
+ if (existingInstall && options.force === true) {
123
+ return {
124
+ ...result,
125
+ mode: "refreshed",
126
+ };
78
127
  }
79
128
 
80
- await fs.mkdir(installPath, { recursive: true });
81
- await fs.cp(path.join(repoRoot, "workshop-skill"), path.join(installPath, "workshop-skill"), { recursive: true });
82
- await fs.cp(path.join(repoRoot, "content"), path.join(installPath, "content"), { recursive: true });
83
- await fs.cp(path.join(repoRoot, "workshop-blueprint"), path.join(installPath, "workshop-blueprint"), { recursive: true });
84
- await fs.rm(path.join(installPath, "workshop-skill", "SKILL.md"), { force: true });
85
- await fs.mkdir(path.join(installPath, "docs"), { recursive: true });
86
- await fs.copyFile(path.join(repoRoot, "workshop-skill", "SKILL.md"), path.join(installPath, "SKILL.md"));
87
- await fs.copyFile(
88
- path.join(repoRoot, "docs", "workshop-event-context-contract.md"),
89
- path.join(installPath, "docs", "workshop-event-context-contract.md"),
90
- );
91
- await fs.copyFile(
92
- path.join(repoRoot, "docs", "harness-cli-foundation.md"),
93
- path.join(installPath, "docs", "harness-cli-foundation.md"),
94
- );
129
+ return result;
130
+ }
131
+
132
+ async function installFreshBundle(resolvedBundle, installPath, targetRoot) {
133
+ await fs.mkdir(path.dirname(installPath), { recursive: true });
134
+ await installFromResolvedBundle(resolvedBundle, installPath);
95
135
 
96
136
  return {
97
- repoRoot,
98
137
  installPath,
99
- skillName: SKILL_NAME,
138
+ skillName: WORKSHOP_SKILL_NAME,
100
139
  mode: "installed",
140
+ sourceMode: resolvedBundle.mode,
141
+ targetRoot,
101
142
  };
102
143
  }
@@ -0,0 +1,233 @@
1
+ import fs from "node:fs/promises";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const WORKSHOP_SKILL_NAME = "harness-lab-workshop";
7
+ export const WORKSHOP_BUNDLE_MANIFEST = "bundle-manifest.json";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const packageRoot = path.resolve(__dirname, "..");
12
+
13
+ const DIRECTORY_COPIES = [
14
+ ["workshop-skill", "workshop-skill"],
15
+ ["content", "content"],
16
+ ["workshop-blueprint", "workshop-blueprint"],
17
+ ];
18
+
19
+ const FILE_COPIES = [
20
+ ["workshop-skill/SKILL.md", "SKILL.md"],
21
+ ["docs/workshop-event-context-contract.md", "docs/workshop-event-context-contract.md"],
22
+ ["docs/harness-cli-foundation.md", "docs/harness-cli-foundation.md"],
23
+ ["docs/learner-resource-kit.md", "docs/learner-resource-kit.md"],
24
+ ["docs/learner-reference-gallery.md", "docs/learner-reference-gallery.md"],
25
+ ["materials/participant-resource-kit.md", "materials/participant-resource-kit.md"],
26
+ ];
27
+
28
+ export function getPackageRoot() {
29
+ return packageRoot;
30
+ }
31
+
32
+ export function getPackagedWorkshopBundlePath() {
33
+ return path.join(packageRoot, "assets", "workshop-bundle");
34
+ }
35
+
36
+ export function getRepoWorkshopSourceRoot() {
37
+ return path.resolve(packageRoot, "..");
38
+ }
39
+
40
+ export function getRepoBundledWorkshopSkillPath() {
41
+ return path.join(getRepoWorkshopSourceRoot(), ".agents", "skills", WORKSHOP_SKILL_NAME);
42
+ }
43
+
44
+ export function getInstalledSkillPath(targetRoot) {
45
+ return path.join(targetRoot, ".agents", "skills", WORKSHOP_SKILL_NAME);
46
+ }
47
+
48
+ export function getBundleManifestPath(bundleRoot) {
49
+ return path.join(bundleRoot, WORKSHOP_BUNDLE_MANIFEST);
50
+ }
51
+
52
+ export async function pathExists(targetPath) {
53
+ try {
54
+ await fs.access(targetPath);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function copyDirectoryTree(sourceRoot, targetRoot) {
62
+ for (const [sourceRelativePath, targetRelativePath] of DIRECTORY_COPIES) {
63
+ await fs.cp(path.join(sourceRoot, sourceRelativePath), path.join(targetRoot, targetRelativePath), { recursive: true });
64
+ }
65
+ }
66
+
67
+ async function copyBundleFiles(sourceRoot, targetRoot) {
68
+ for (const [sourceRelativePath, targetRelativePath] of FILE_COPIES) {
69
+ const targetPath = path.join(targetRoot, targetRelativePath);
70
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
71
+ await fs.copyFile(path.join(sourceRoot, sourceRelativePath), targetPath);
72
+ }
73
+ }
74
+
75
+ function normalizePathForManifest(targetPath) {
76
+ return targetPath.split(path.sep).join("/");
77
+ }
78
+
79
+ async function listFilesRecursive(rootPath) {
80
+ const entries = [];
81
+
82
+ async function walk(currentPath, relativePrefix = "") {
83
+ const dirEntries = await fs.readdir(currentPath, { withFileTypes: true });
84
+ dirEntries.sort((left, right) => left.name.localeCompare(right.name));
85
+
86
+ for (const entry of dirEntries) {
87
+ const entryRelativePath = relativePrefix ? path.join(relativePrefix, entry.name) : entry.name;
88
+ const entryPath = path.join(currentPath, entry.name);
89
+
90
+ if (entry.isDirectory()) {
91
+ await walk(entryPath, entryRelativePath);
92
+ continue;
93
+ }
94
+
95
+ if (entry.isFile()) {
96
+ entries.push({
97
+ absolutePath: entryPath,
98
+ relativePath: normalizePathForManifest(entryRelativePath),
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ await walk(rootPath);
105
+ return entries;
106
+ }
107
+
108
+ async function readPackageVersion() {
109
+ const packageJson = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
110
+ return String(packageJson.version);
111
+ }
112
+
113
+ async function createManifestFromEntries(entries) {
114
+ const bundleVersion = await readPackageVersion();
115
+ const files = [];
116
+
117
+ for (const entry of entries) {
118
+ const contents = await fs.readFile(entry.absolutePath);
119
+ const sha256 = crypto.createHash("sha256").update(contents).digest("hex");
120
+ files.push({
121
+ path: entry.relativePath,
122
+ sha256,
123
+ });
124
+ }
125
+
126
+ files.sort((left, right) => left.path.localeCompare(right.path));
127
+
128
+ const contentHash = crypto
129
+ .createHash("sha256")
130
+ .update(
131
+ files
132
+ .map((file) => `${file.path}:${file.sha256}`)
133
+ .join("\n"),
134
+ )
135
+ .digest("hex");
136
+
137
+ return {
138
+ manifestVersion: 1,
139
+ bundleName: WORKSHOP_SKILL_NAME,
140
+ bundleVersion,
141
+ contentHash,
142
+ files,
143
+ };
144
+ }
145
+
146
+ export async function createWorkshopBundleManifestFromDirectory(bundleRoot) {
147
+ const files = await listFilesRecursive(bundleRoot);
148
+ return createManifestFromEntries(
149
+ files.filter((file) => file.relativePath !== WORKSHOP_BUNDLE_MANIFEST),
150
+ );
151
+ }
152
+
153
+ export async function createWorkshopBundleManifestFromSource(sourceRoot) {
154
+ const entries = [];
155
+
156
+ for (const [sourceRelativePath, targetRelativePath] of DIRECTORY_COPIES) {
157
+ const sourceDirectory = path.join(sourceRoot, sourceRelativePath);
158
+ const sourceFiles = await listFilesRecursive(sourceDirectory);
159
+
160
+ for (const file of sourceFiles) {
161
+ const targetRelative = normalizePathForManifest(path.join(targetRelativePath, file.relativePath));
162
+ if (targetRelative === "workshop-skill/SKILL.md") {
163
+ continue;
164
+ }
165
+
166
+ entries.push({
167
+ absolutePath: file.absolutePath,
168
+ relativePath: targetRelative,
169
+ });
170
+ }
171
+ }
172
+
173
+ for (const [sourceRelativePath, targetRelativePath] of FILE_COPIES) {
174
+ entries.push({
175
+ absolutePath: path.join(sourceRoot, sourceRelativePath),
176
+ relativePath: normalizePathForManifest(targetRelativePath),
177
+ });
178
+ }
179
+
180
+ return createManifestFromEntries(entries);
181
+ }
182
+
183
+ export async function readWorkshopBundleManifest(bundleRoot) {
184
+ try {
185
+ const manifest = JSON.parse(await fs.readFile(getBundleManifestPath(bundleRoot), "utf8"));
186
+ if (!manifest || typeof manifest.contentHash !== "string") {
187
+ return null;
188
+ }
189
+ return manifest;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ async function writeWorkshopBundleManifest(bundleRoot, manifest) {
196
+ await fs.writeFile(
197
+ getBundleManifestPath(bundleRoot),
198
+ `${JSON.stringify(manifest, null, 2)}\n`,
199
+ "utf8",
200
+ );
201
+ }
202
+
203
+ export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, options = {}) {
204
+ if (options.clean === true) {
205
+ await fs.rm(targetRoot, { recursive: true, force: true });
206
+ }
207
+ await fs.mkdir(targetRoot, { recursive: true });
208
+ await copyDirectoryTree(sourceRoot, targetRoot);
209
+ await fs.rm(path.join(targetRoot, "workshop-skill", "SKILL.md"), { force: true });
210
+ await copyBundleFiles(sourceRoot, targetRoot);
211
+ const manifest = await createWorkshopBundleManifestFromSource(sourceRoot);
212
+ await writeWorkshopBundleManifest(targetRoot, manifest);
213
+ }
214
+
215
+ export async function syncPackagedWorkshopBundle() {
216
+ const sourceRoot = getRepoWorkshopSourceRoot();
217
+ const bundleRoot = getPackagedWorkshopBundlePath();
218
+ await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { clean: true });
219
+ return {
220
+ sourceRoot,
221
+ bundleRoot,
222
+ };
223
+ }
224
+
225
+ export async function syncRepoBundledWorkshopSkill() {
226
+ const sourceRoot = getRepoWorkshopSourceRoot();
227
+ const bundleRoot = getRepoBundledWorkshopSkillPath();
228
+ await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { clean: true });
229
+ return {
230
+ sourceRoot,
231
+ bundleRoot,
232
+ };
233
+ }