skill-flow 1.0.8 → 1.3.3

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 (143) hide show
  1. package/README.md +209 -131
  2. package/README.zh.md +170 -131
  3. package/dist/bridge-command.d.ts +9 -0
  4. package/dist/bridge-command.js +422 -0
  5. package/dist/bridge-command.js.map +1 -0
  6. package/dist/cli.js +68 -8
  7. package/dist/cli.js.map +1 -1
  8. package/package.json +11 -2
  9. package/dist/adapters/channel-adapters.d.ts +0 -8
  10. package/dist/adapters/channel-adapters.js +0 -64
  11. package/dist/adapters/channel-adapters.js.map +0 -1
  12. package/dist/domain/types.d.ts +0 -234
  13. package/dist/domain/types.js +0 -2
  14. package/dist/domain/types.js.map +0 -1
  15. package/dist/services/config-coordinator.d.ts +0 -38
  16. package/dist/services/config-coordinator.js +0 -83
  17. package/dist/services/config-coordinator.js.map +0 -1
  18. package/dist/services/deployment-applier.d.ts +0 -10
  19. package/dist/services/deployment-applier.js +0 -84
  20. package/dist/services/deployment-applier.js.map +0 -1
  21. package/dist/services/deployment-planner.d.ts +0 -16
  22. package/dist/services/deployment-planner.js +0 -366
  23. package/dist/services/deployment-planner.js.map +0 -1
  24. package/dist/services/doctor-service.d.ts +0 -7
  25. package/dist/services/doctor-service.js +0 -204
  26. package/dist/services/doctor-service.js.map +0 -1
  27. package/dist/services/inventory-service.d.ts +0 -17
  28. package/dist/services/inventory-service.js +0 -216
  29. package/dist/services/inventory-service.js.map +0 -1
  30. package/dist/services/skill-flow.d.ts +0 -136
  31. package/dist/services/skill-flow.js +0 -1210
  32. package/dist/services/skill-flow.js.map +0 -1
  33. package/dist/services/source-service.d.ts +0 -57
  34. package/dist/services/source-service.js +0 -809
  35. package/dist/services/source-service.js.map +0 -1
  36. package/dist/services/workflow-service.d.ts +0 -5
  37. package/dist/services/workflow-service.js +0 -45
  38. package/dist/services/workflow-service.js.map +0 -1
  39. package/dist/services/workspace-bootstrap-service.d.ts +0 -25
  40. package/dist/services/workspace-bootstrap-service.js +0 -140
  41. package/dist/services/workspace-bootstrap-service.js.map +0 -1
  42. package/dist/state/store.d.ts +0 -35
  43. package/dist/state/store.js +0 -151
  44. package/dist/state/store.js.map +0 -1
  45. package/dist/tests/add-flow-model.test.d.ts +0 -1
  46. package/dist/tests/add-flow-model.test.js +0 -108
  47. package/dist/tests/add-flow-model.test.js.map +0 -1
  48. package/dist/tests/add-flow-ui.test.d.ts +0 -1
  49. package/dist/tests/add-flow-ui.test.js +0 -16
  50. package/dist/tests/add-flow-ui.test.js.map +0 -1
  51. package/dist/tests/add-prepare-flow.test.d.ts +0 -1
  52. package/dist/tests/add-prepare-flow.test.js +0 -166
  53. package/dist/tests/add-prepare-flow.test.js.map +0 -1
  54. package/dist/tests/add-selection-and-find-command.test.d.ts +0 -1
  55. package/dist/tests/add-selection-and-find-command.test.js +0 -89
  56. package/dist/tests/add-selection-and-find-command.test.js.map +0 -1
  57. package/dist/tests/clawhub.test.d.ts +0 -1
  58. package/dist/tests/clawhub.test.js +0 -63
  59. package/dist/tests/clawhub.test.js.map +0 -1
  60. package/dist/tests/cli-utils.test.d.ts +0 -1
  61. package/dist/tests/cli-utils.test.js +0 -24
  62. package/dist/tests/cli-utils.test.js.map +0 -1
  63. package/dist/tests/config-coordinator.test.d.ts +0 -1
  64. package/dist/tests/config-coordinator.test.js +0 -219
  65. package/dist/tests/config-coordinator.test.js.map +0 -1
  66. package/dist/tests/config-integration.test.d.ts +0 -1
  67. package/dist/tests/config-integration.test.js +0 -276
  68. package/dist/tests/config-integration.test.js.map +0 -1
  69. package/dist/tests/config-ui-utils.test.d.ts +0 -1
  70. package/dist/tests/config-ui-utils.test.js +0 -523
  71. package/dist/tests/config-ui-utils.test.js.map +0 -1
  72. package/dist/tests/find-and-naming-utils.test.d.ts +0 -1
  73. package/dist/tests/find-and-naming-utils.test.js +0 -127
  74. package/dist/tests/find-and-naming-utils.test.js.map +0 -1
  75. package/dist/tests/inventory-service-precedence.test.d.ts +0 -1
  76. package/dist/tests/inventory-service-precedence.test.js +0 -42
  77. package/dist/tests/inventory-service-precedence.test.js.map +0 -1
  78. package/dist/tests/skill-flow.test.d.ts +0 -1
  79. package/dist/tests/skill-flow.test.js +0 -991
  80. package/dist/tests/skill-flow.test.js.map +0 -1
  81. package/dist/tests/source-lifecycle.test.d.ts +0 -1
  82. package/dist/tests/source-lifecycle.test.js +0 -644
  83. package/dist/tests/source-lifecycle.test.js.map +0 -1
  84. package/dist/tests/source-parsing-compatibility.test.d.ts +0 -1
  85. package/dist/tests/source-parsing-compatibility.test.js +0 -72
  86. package/dist/tests/source-parsing-compatibility.test.js.map +0 -1
  87. package/dist/tests/target-definitions.test.d.ts +0 -1
  88. package/dist/tests/target-definitions.test.js +0 -51
  89. package/dist/tests/target-definitions.test.js.map +0 -1
  90. package/dist/tests/test-helpers.d.ts +0 -18
  91. package/dist/tests/test-helpers.js +0 -123
  92. package/dist/tests/test-helpers.js.map +0 -1
  93. package/dist/tui/add-flow-model.d.ts +0 -62
  94. package/dist/tui/add-flow-model.js +0 -206
  95. package/dist/tui/add-flow-model.js.map +0 -1
  96. package/dist/tui/add-flow.d.ts +0 -25
  97. package/dist/tui/add-flow.js +0 -534
  98. package/dist/tui/add-flow.js.map +0 -1
  99. package/dist/tui/config-app.d.ts +0 -178
  100. package/dist/tui/config-app.js +0 -1551
  101. package/dist/tui/config-app.js.map +0 -1
  102. package/dist/tui/find-app.d.ts +0 -9
  103. package/dist/tui/find-app.js +0 -150
  104. package/dist/tui/find-app.js.map +0 -1
  105. package/dist/tui/selection-state.d.ts +0 -8
  106. package/dist/tui/selection-state.js +0 -32
  107. package/dist/tui/selection-state.js.map +0 -1
  108. package/dist/utils/builtin-git-sources.d.ts +0 -5
  109. package/dist/utils/builtin-git-sources.js +0 -23
  110. package/dist/utils/builtin-git-sources.js.map +0 -1
  111. package/dist/utils/clawhub.d.ts +0 -41
  112. package/dist/utils/clawhub.js +0 -94
  113. package/dist/utils/clawhub.js.map +0 -1
  114. package/dist/utils/cli.d.ts +0 -2
  115. package/dist/utils/cli.js +0 -19
  116. package/dist/utils/cli.js.map +0 -1
  117. package/dist/utils/constants.d.ts +0 -23
  118. package/dist/utils/constants.js +0 -195
  119. package/dist/utils/constants.js.map +0 -1
  120. package/dist/utils/find-command.d.ts +0 -2
  121. package/dist/utils/find-command.js +0 -29
  122. package/dist/utils/find-command.js.map +0 -1
  123. package/dist/utils/format.d.ts +0 -7
  124. package/dist/utils/format.js +0 -68
  125. package/dist/utils/format.js.map +0 -1
  126. package/dist/utils/fs.d.ts +0 -16
  127. package/dist/utils/fs.js +0 -144
  128. package/dist/utils/fs.js.map +0 -1
  129. package/dist/utils/git.d.ts +0 -3
  130. package/dist/utils/git.js +0 -12
  131. package/dist/utils/git.js.map +0 -1
  132. package/dist/utils/github-catalog.d.ts +0 -1
  133. package/dist/utils/github-catalog.js +0 -25
  134. package/dist/utils/github-catalog.js.map +0 -1
  135. package/dist/utils/naming.d.ts +0 -29
  136. package/dist/utils/naming.js +0 -115
  137. package/dist/utils/naming.js.map +0 -1
  138. package/dist/utils/result.d.ts +0 -4
  139. package/dist/utils/result.js +0 -15
  140. package/dist/utils/result.js.map +0 -1
  141. package/dist/utils/source-id.d.ts +0 -2
  142. package/dist/utils/source-id.js +0 -49
  143. package/dist/utils/source-id.js.map +0 -1
@@ -1,644 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { describe, expect, test, vi } from "vitest";
4
- import { InventoryService } from "../services/inventory-service.js";
5
- import { SourceService } from "../services/source-service.js";
6
- import { SkillFlowApp } from "../services/skill-flow.js";
7
- import { StateStore } from "../state/store.js";
8
- import * as clawhubUtils from "../utils/clawhub.js";
9
- import * as builtinGitSources from "../utils/builtin-git-sources.js";
10
- import * as githubCatalog from "../utils/github-catalog.js";
11
- import { buildFindCommand } from "../utils/find-command.js";
12
- import { deriveSourceId } from "../utils/source-id.js";
13
- import { createRepo, pathExists, seedBuiltinCatalog, skillDoc, writeRepoFiles, useSkillFlowSandbox, } from "./test-helpers.js";
14
- describe.sequential("source lifecycle", () => {
15
- const sandbox = useSkillFlowSandbox();
16
- function createSourceService() {
17
- return new SourceService(new StateStore(), new InventoryService());
18
- }
19
- test("adds a git source and discovers valid skills", async () => {
20
- const repoPath = await createRepo(sandbox.sandboxRoot, {
21
- "frontend/SKILL.md": skillDoc("frontend", "Build frontend flows."),
22
- "ops/SKILL.md": skillDoc("ops", "Run operator workflows."),
23
- });
24
- const app = new SkillFlowApp();
25
- const result = await app.addSource(repoPath);
26
- expect(result.ok).toBe(true);
27
- if (!result.ok) {
28
- return;
29
- }
30
- expect(result.data.leafCount).toBe(2);
31
- expect(result.warnings).toHaveLength(0);
32
- const manifest = await app.store.readManifest();
33
- const binding = manifest.bindings[result.data.manifest.id];
34
- expect(Object.keys(binding?.targets ?? {})).toEqual([
35
- "claude-code",
36
- "codex",
37
- "cursor",
38
- "github-copilot",
39
- "gemini-cli",
40
- "opencode",
41
- "openclaw",
42
- "pi",
43
- "windsurf",
44
- "roo-code",
45
- "cline",
46
- "amp",
47
- "kiro",
48
- ]);
49
- expect(binding?.targets["claude-code"]?.leafIds).toEqual([
50
- `${result.data.manifest.id}:frontend`,
51
- `${result.data.manifest.id}:ops`,
52
- ]);
53
- });
54
- test("adds a git source with path filtering but only preselects matching skills", async () => {
55
- const repoPath = await createRepo(sandbox.sandboxRoot, {
56
- "skills/find-skills/SKILL.md": skillDoc("find-skills", "Find skills."),
57
- "skills/review/SKILL.md": skillDoc("review", "Review code."),
58
- });
59
- const app = new SkillFlowApp();
60
- const result = await app.addSource(repoPath, { path: "skills/find-skills" });
61
- expect(result.ok).toBe(true);
62
- if (!result.ok) {
63
- return;
64
- }
65
- expect(result.data.leafCount).toBe(2);
66
- const list = await app.listWorkflows();
67
- expect(list.ok).toBe(true);
68
- if (!list.ok) {
69
- return;
70
- }
71
- expect(list.data.summaries[0]?.leafs.map((leaf) => leaf.relativePath)).toEqual([
72
- "skills/find-skills",
73
- "skills/review",
74
- ]);
75
- const manifest = await app.store.readManifest();
76
- const binding = manifest.bindings[result.data.manifest.id];
77
- expect(binding?.targets["claude-code"]?.leafIds).toEqual([
78
- `${result.data.manifest.id}:skills/find-skills`,
79
- ]);
80
- });
81
- test("keeps ssh git locators unchanged during normalization", async () => {
82
- const service = createSourceService();
83
- await expect(service.normalizeLocator("git@github.com:JimLiu/baoyu-skills.git")).resolves.toBe("git@github.com:JimLiu/baoyu-skills.git");
84
- });
85
- test("rejects add path when it does not resolve to a valid skill", async () => {
86
- const repoPath = await createRepo(sandbox.sandboxRoot, {
87
- "skills/find-skills/SKILL.md": skillDoc("find-skills", "Find skills."),
88
- "docs/readme.md": "hello",
89
- });
90
- const app = new SkillFlowApp();
91
- const result = await app.addSource(repoPath, { path: "docs" });
92
- expect(result.ok).toBe(false);
93
- if (result.ok) {
94
- return;
95
- }
96
- expect(result.errors[0]?.code).toBe("SOURCE_PATH_NOT_FOUND");
97
- });
98
- test("parses GitHub tree URLs as repo sources with path filtering", async () => {
99
- const app = new SkillFlowApp();
100
- const result = await app.addSource("https://github.com/vercel-labs/skills/tree/main/skills/find-skills");
101
- expect(result.ok).toBe(true);
102
- if (!result.ok) {
103
- return;
104
- }
105
- expect(result.data.manifest.id).toBe("vercel-labs-skills");
106
- expect(result.data.manifest.requestedPath).toBe("skills/find-skills");
107
- }, 30000);
108
- test("combines GitHub tree paths with --path relative to the tree location", async () => {
109
- const app = new SkillFlowApp();
110
- const result = await app.addSource("https://github.com/vercel-labs/skills/tree/main/skills", { path: "find-skills" });
111
- expect(result.ok).toBe(true);
112
- if (!result.ok) {
113
- return;
114
- }
115
- expect(result.data.manifest.id).toBe("vercel-labs-skills");
116
- expect(result.data.manifest.requestedPath).toBe("skills/find-skills");
117
- }, 30000);
118
- test("adds a ClawHub source and stores ClawHub lock metadata", async () => {
119
- const app = new SkillFlowApp();
120
- const result = await app.addSource("clawhub:find-skills-skill");
121
- expect(result.ok).toBe(true);
122
- if (!result.ok) {
123
- return;
124
- }
125
- expect(result.data.manifest.kind).toBe("clawhub");
126
- expect(result.data.lock.kind).toBe("clawhub");
127
- expect(result.data.lock.packageSlug).toBe("find-skills-skill");
128
- expect(result.data.lock.resolvedVersion).toBeTruthy();
129
- expect(result.data.leafCount).toBeGreaterThan(0);
130
- }, 20000);
131
- test("keeps a pinned ClawHub source unchanged on update", async () => {
132
- const app = new SkillFlowApp();
133
- const added = await app.addSource("clawhub:find-skills-skill@1.0.0");
134
- expect(added.ok).toBe(true);
135
- if (!added.ok) {
136
- return;
137
- }
138
- expect(added.data.lock.versionMode).toBe("pinned");
139
- const updated = await app.updateSources([added.data.manifest.id]);
140
- expect(updated.ok).toBe(true);
141
- if (!updated.ok) {
142
- return;
143
- }
144
- expect(updated.data.updated[0]?.changed).toBe(false);
145
- }, 20000);
146
- test("keeps a floating ClawHub source unchanged when no newer version exists", async () => {
147
- const app = new SkillFlowApp();
148
- const added = await app.addSource("clawhub:find-skills-skill");
149
- expect(added.ok).toBe(true);
150
- if (!added.ok) {
151
- return;
152
- }
153
- expect(added.data.lock.versionMode).toBe("floating");
154
- const updated = await app.updateSources([added.data.manifest.id]);
155
- expect(updated.ok).toBe(true);
156
- if (!updated.ok) {
157
- return;
158
- }
159
- expect(updated.data.updated[0]?.changed).toBe(false);
160
- }, 40000);
161
- test("find prefers local results, then built-in git, then ClawHub", async () => {
162
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockResolvedValueOnce([
163
- { slug: "browse-skill", title: "Browse Skill", score: 0.92 },
164
- ]);
165
- const repoPath = await createRepo(sandbox.sandboxRoot, {
166
- "browse/SKILL.md": skillDoc("browse", "Local browse skill."),
167
- });
168
- const app = new SkillFlowApp();
169
- const added = await app.addSource(repoPath);
170
- expect(added.ok).toBe(true);
171
- await seedBuiltinCatalog(app);
172
- const builtin = builtinGitSources.getBuiltinGitSources()[0];
173
- const builtinSourceId = deriveSourceId(builtin.locator);
174
- await fs.mkdir(path.join(app.store.getCatalogCheckoutPath(builtinSourceId), "skills", "browse"), { recursive: true });
175
- await fs.writeFile(path.join(app.store.getCatalogCheckoutPath(builtinSourceId), "skills", "browse", "SKILL.md"), skillDoc("browse", "Built-in browse skill."), "utf8");
176
- const result = await app.findSkills("browse");
177
- expect(result.ok).toBe(true);
178
- if (!result.ok) {
179
- return;
180
- }
181
- expect(result.data.candidates.map((candidate) => candidate.source)).toEqual([
182
- "local",
183
- "builtin-git",
184
- "clawhub",
185
- ]);
186
- }, 10000);
187
- test("find falls back to stale built-in catalog cache with a warning", async () => {
188
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockResolvedValueOnce([]);
189
- vi.spyOn(builtinGitSources, "getBuiltinGitSources").mockReturnValue([
190
- { locator: "https://github.com/example/catalog.git", branch: "main" },
191
- ]);
192
- vi.spyOn(githubCatalog, "fetchGitHubSkillPaths").mockRejectedValueOnce(new Error("offline"));
193
- const app = new SkillFlowApp();
194
- const sourceId = deriveSourceId("https://github.com/example/catalog.git");
195
- await fs.mkdir(app.store.catalogRoot, { recursive: true });
196
- await fs.writeFile(app.store.getCatalogIndexPath(sourceId), `${JSON.stringify({
197
- locator: "https://github.com/example/catalog.git",
198
- branch: "main",
199
- skillPaths: ["skills/browse/SKILL.md"],
200
- updatedAt: "2020-01-01T00:00:00.000Z",
201
- }, null, 2)}\n`, "utf8");
202
- const result = await app.findSkills("browse");
203
- expect(result.ok).toBe(true);
204
- if (!result.ok) {
205
- return;
206
- }
207
- expect(result.data.candidates[0]?.source).toBe("builtin-git");
208
- expect(result.warnings.some((warning) => warning.code === "BUILTIN_SOURCE_STALE_CACHE_USED")).toBe(true);
209
- });
210
- test("find builds repo-level add commands for built-in Git results", async () => {
211
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockResolvedValueOnce([]);
212
- const app = new SkillFlowApp();
213
- await seedBuiltinCatalog(app);
214
- const builtin = builtinGitSources.getBuiltinGitSources()[0];
215
- const builtinSourceId = deriveSourceId(builtin.locator);
216
- await fs.mkdir(path.join(app.store.getCatalogCheckoutPath(builtinSourceId), "skills", "find-skills"), { recursive: true });
217
- await fs.writeFile(path.join(app.store.getCatalogCheckoutPath(builtinSourceId), "skills", "find-skills", "SKILL.md"), skillDoc("find-skills", "Find skills from a built-in repo."), "utf8");
218
- const result = await app.findSkills("find skills");
219
- expect(result.ok).toBe(true);
220
- if (!result.ok) {
221
- return;
222
- }
223
- const candidate = result.data.candidates[0];
224
- expect(candidate?.source).toBe("builtin-git");
225
- expect(buildFindCommand(candidate)).toBe(`skill-flow add ${builtin.locator} --path skills/find-skills`);
226
- }, 10000);
227
- test("find normalizes spaces, hyphens, and underscores in query matching", async () => {
228
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockResolvedValueOnce([]);
229
- const repoPath = await createRepo(sandbox.sandboxRoot, {
230
- "agent-browser/SKILL.md": skillDoc("agent-browser", "Agent browser flow."),
231
- });
232
- const app = new SkillFlowApp();
233
- const added = await app.addSource(repoPath);
234
- expect(added.ok).toBe(true);
235
- const result = await app.findSkills("agent browser");
236
- expect(result.ok).toBe(true);
237
- if (!result.ok) {
238
- return;
239
- }
240
- expect(result.data.candidates[0]?.title).toBe("agent-browser");
241
- }, 10000);
242
- test("find falls back to linkName when local skill title is a placeholder heading", async () => {
243
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockResolvedValueOnce([]);
244
- const repoPath = await createRepo(sandbox.sandboxRoot, {
245
- "templated/SKILL.md": `---
246
- name: templated
247
- description: |
248
- Templated description.
249
- ---
250
-
251
- # {Title}
252
- `,
253
- });
254
- const app = new SkillFlowApp();
255
- const added = await app.addSource(repoPath);
256
- expect(added.ok).toBe(true);
257
- const result = await app.findSkills("templated");
258
- expect(result.ok).toBe(true);
259
- if (!result.ok) {
260
- return;
261
- }
262
- expect(result.data.candidates[0]?.title).toBe("templated");
263
- });
264
- test("returns a clear error when git fetch fails", async () => {
265
- const app = new SkillFlowApp();
266
- const result = await app.addSource(path.join(sandbox.sandboxRoot, "missing-repo"));
267
- expect(result.ok).toBe(false);
268
- if (result.ok) {
269
- return;
270
- }
271
- expect(result.errors[0]?.code).toBe("GIT_CLONE_FAILED");
272
- });
273
- test("keeps an existing checkout directory when git fetch fails", async () => {
274
- const app = new SkillFlowApp();
275
- const sourceId = "existing-checkout";
276
- const checkoutPath = app.store.getSourceCheckoutPath("git", sourceId);
277
- await fs.mkdir(checkoutPath, { recursive: true });
278
- await fs.writeFile(path.join(checkoutPath, "keep.txt"), "keep", "utf8");
279
- const result = await app.addSource(path.join(sandbox.sandboxRoot, "missing-repo"), {
280
- sourceIdOverride: sourceId,
281
- });
282
- expect(result.ok).toBe(false);
283
- expect(result.errors[0]?.code).toBe("GIT_CLONE_FAILED");
284
- expect(await fs.readFile(path.join(checkoutPath, "keep.txt"), "utf8")).toBe("keep");
285
- });
286
- test("refuses to delete the managed source root itself during uninstall", async () => {
287
- const repoPath = await createRepo(sandbox.sandboxRoot, {
288
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
289
- });
290
- const app = new SkillFlowApp();
291
- const added = await app.addSource(repoPath, { project: false });
292
- expect(added.ok).toBe(true);
293
- if (!added.ok) {
294
- return;
295
- }
296
- const lock = await app.store.readLock();
297
- const source = lock.sources.find((item) => item.id === added.data.manifest.id);
298
- expect(source).toBeTruthy();
299
- if (!source) {
300
- return;
301
- }
302
- source.checkoutPath = app.store.getSourceRoot("local");
303
- await app.store.writeLock(lock);
304
- const removed = await app.uninstall([added.data.manifest.id]);
305
- expect(removed.ok).toBe(false);
306
- if (removed.ok) {
307
- return;
308
- }
309
- expect(removed.errors[0]?.code).toBe("SOURCE_CHECKOUT_PATH_INVALID");
310
- expect(await pathExists(app.store.getSourceRoot("local"))).toBe(true);
311
- });
312
- test("fails find when no local results exist and all remote search backends are unavailable", async () => {
313
- vi.spyOn(builtinGitSources, "getBuiltinGitSources").mockReturnValue([]);
314
- vi.spyOn(clawhubUtils, "searchClawHubSkills").mockRejectedValueOnce(new Error("offline"));
315
- const app = new SkillFlowApp();
316
- const result = await app.findSkills("browse");
317
- expect(result.ok).toBe(false);
318
- if (result.ok) {
319
- return;
320
- }
321
- expect(result.errors[0]?.code).toBe("FIND_UNAVAILABLE");
322
- expect(result.warnings[0]?.code).toBe("CLAWHUB_SEARCH_FAILED");
323
- });
324
- test("rejects uninstall for an unknown skills group", async () => {
325
- const app = new SkillFlowApp();
326
- const result = await app.uninstall(["missing-source"]);
327
- expect(result.ok).toBe(false);
328
- if (result.ok) {
329
- return;
330
- }
331
- expect(result.errors[0]?.code).toBe("SOURCE_NOT_FOUND");
332
- });
333
- test("rejects a source with zero valid skills", async () => {
334
- const repoPath = await createRepo(sandbox.sandboxRoot, {
335
- "broken/SKILL.md": "No heading here",
336
- });
337
- const app = new SkillFlowApp();
338
- const result = await app.addSource(repoPath);
339
- expect(result.ok).toBe(false);
340
- if (result.ok) {
341
- return;
342
- }
343
- expect(result.errors[0]?.code).toBe("NO_VALID_LEAFS");
344
- });
345
- test("keeps valid skills and warns about invalid ones", async () => {
346
- const repoPath = await createRepo(sandbox.sandboxRoot, {
347
- "good/SKILL.md": skillDoc("good", "Good description."),
348
- "bad/SKILL.md": "Broken file",
349
- });
350
- const app = new SkillFlowApp();
351
- const result = await app.addSource(repoPath);
352
- expect(result.ok).toBe(true);
353
- if (!result.ok) {
354
- return;
355
- }
356
- expect(result.data.leafCount).toBe(1);
357
- expect(result.warnings).toHaveLength(1);
358
- });
359
- test("accepts skills that use YAML frontmatter metadata", async () => {
360
- const repoPath = await createRepo(sandbox.sandboxRoot, {
361
- "browse/SKILL.md": `---
362
- name: browse
363
- version: 1.1.0
364
- description: |
365
- Fast headless browser for QA testing and site dogfooding.
366
- Opens pages and validates flows.
367
- ---
368
- <!-- generated -->
369
-
370
- ## Preamble
371
- `,
372
- });
373
- const app = new SkillFlowApp();
374
- const result = await app.addSource(repoPath);
375
- expect(result.ok).toBe(true);
376
- if (!result.ok) {
377
- return;
378
- }
379
- expect(result.data.leafCount).toBe(1);
380
- const listResult = await app.listWorkflows();
381
- expect(listResult.ok).toBe(true);
382
- if (!listResult.ok) {
383
- return;
384
- }
385
- expect(listResult.data.summaries[0]?.leafs[0]?.title).toBe("browse");
386
- expect(listResult.data.summaries[0]?.leafs[0]?.description).toContain("Fast headless browser");
387
- expect(listResult.data.summaries[0]?.leafs[0]?.name).toBe("browse");
388
- });
389
- test("uses repository display name for a root-level skill link name", async () => {
390
- const repoPath = await createRepo(sandbox.sandboxRoot, {
391
- "SKILL.md": skillDoc("gstack", "Root skill."),
392
- });
393
- const inventory = new InventoryService();
394
- const scanned = await inventory.scanSource("garrytan-gstack", repoPath, "gstack");
395
- expect(scanned.leafs[0]?.linkName).toBe("gstack");
396
- });
397
- test("keeps metadata warnings on valid skills", async () => {
398
- const repoPath = await createRepo(sandbox.sandboxRoot, {
399
- "folder-name/SKILL.md": skillDoc("bad--name", "x".repeat(1025)),
400
- });
401
- const app = new SkillFlowApp();
402
- const result = await app.addSource(repoPath);
403
- expect(result.ok).toBe(true);
404
- if (!result.ok) {
405
- return;
406
- }
407
- const list = await app.listWorkflows();
408
- expect(list.ok).toBe(true);
409
- if (!list.ok) {
410
- return;
411
- }
412
- expect(list.data.summaries[0]?.leafs[0]?.metadataWarnings.length).toBeGreaterThan(0);
413
- });
414
- test("reads old lock entries without metadata fields", async () => {
415
- const repoPath = await createRepo(sandbox.sandboxRoot, {
416
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
417
- });
418
- const app = new SkillFlowApp();
419
- const added = await app.addSource(repoPath);
420
- expect(added.ok).toBe(true);
421
- if (!added.ok) {
422
- return;
423
- }
424
- const lockPath = path.join(sandbox.stateRoot, "lock.json");
425
- const lock = JSON.parse(await fs.readFile(lockPath, "utf8"));
426
- lock.leafInventory = lock.leafInventory.map((leaf) => {
427
- const next = { ...leaf };
428
- delete next.metadataWarnings;
429
- delete next.linkName;
430
- return next;
431
- });
432
- await fs.writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
433
- const list = await app.listWorkflows();
434
- expect(list.ok).toBe(true);
435
- if (!list.ok) {
436
- return;
437
- }
438
- expect(list.data.summaries[0]?.leafs[0]?.metadataWarnings).toEqual([]);
439
- expect(list.data.summaries[0]?.leafs[0]?.linkName).toBe("browse");
440
- });
441
- test("local source updates re-copy from origin and report changed diffs", async () => {
442
- const repoPath = await createRepo(sandbox.sandboxRoot, {
443
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
444
- });
445
- const sourceService = createSourceService();
446
- const added = await sourceService.addSource(repoPath);
447
- expect(added.ok).toBe(true);
448
- if (!added.ok) {
449
- return;
450
- }
451
- const sourceId = added.data.manifest.id;
452
- const checkoutPath = path.join(sandbox.stateRoot, "source", "local", sourceId);
453
- await writeRepoFiles(repoPath, {
454
- "browse/SKILL.md": skillDoc("browse", "Browser flow, updated upstream."),
455
- });
456
- await writeRepoFiles(checkoutPath, {
457
- "browse/SKILL.md": skillDoc("browse", "Stale checkout content."),
458
- });
459
- const updated = await sourceService.updateSources([sourceId]);
460
- expect(updated.ok).toBe(true);
461
- if (!updated.ok) {
462
- return;
463
- }
464
- const item = updated.data.updated[0];
465
- expect(item?.changed).toBe(true);
466
- expect(item?.diffs.map((diff) => diff.kind)).toEqual(["changed"]);
467
- expect(await fs.readFile(path.join(checkoutPath, "browse", "SKILL.md"), "utf8")).toContain("Browser flow, updated upstream.");
468
- });
469
- test("local sources with the same folder name fall back to parent-prefixed naming", async () => {
470
- const leftParent = path.join(sandbox.sandboxRoot, "left");
471
- const rightParent = path.join(sandbox.sandboxRoot, "right");
472
- const leftRepo = path.join(leftParent, "skills");
473
- const rightRepo = path.join(rightParent, "skills");
474
- await writeRepoFiles(leftRepo, {
475
- "browse/SKILL.md": skillDoc("browse", "Left browse."),
476
- });
477
- await writeRepoFiles(rightRepo, {
478
- "review/SKILL.md": skillDoc("review", "Right review."),
479
- });
480
- const sourceService = createSourceService();
481
- const first = await sourceService.addSource(leftRepo);
482
- const second = await sourceService.addSource(rightRepo);
483
- expect(first.ok).toBe(true);
484
- expect(second.ok).toBe(true);
485
- if (!first.ok || !second.ok) {
486
- return;
487
- }
488
- expect(first.data.manifest.displayName).toBe("skills");
489
- expect(first.data.manifest.id).toBe("skills");
490
- expect(second.data.manifest.displayName).toBe("right_skills");
491
- expect(second.data.manifest.id).toBe("right-skills");
492
- });
493
- test("local source update fails without mutating checkout when origin path is missing", async () => {
494
- const repoPath = path.join(sandbox.sandboxRoot, "local-skills");
495
- await writeRepoFiles(repoPath, {
496
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
497
- });
498
- const sourceService = createSourceService();
499
- const added = await sourceService.addSource(repoPath);
500
- expect(added.ok).toBe(true);
501
- if (!added.ok) {
502
- return;
503
- }
504
- const checkoutPath = path.join(sandbox.stateRoot, "source", "local", added.data.manifest.id);
505
- const before = await fs.readFile(path.join(checkoutPath, "browse", "SKILL.md"), "utf8");
506
- await fs.rm(repoPath, { recursive: true, force: true });
507
- const updated = await sourceService.updateSources([added.data.manifest.id]);
508
- expect(updated.ok).toBe(false);
509
- if (updated.ok) {
510
- return;
511
- }
512
- expect(updated.errors[0]?.code).toBe("LOCAL_UPDATE_FAILED");
513
- expect(updated.errors[0]?.message).toContain("Local source origin is missing");
514
- expect(await fs.readFile(path.join(checkoutPath, "browse", "SKILL.md"), "utf8")).toBe(before);
515
- });
516
- test("repairSource refreshes a local checkout from origin without mutating target disk", async () => {
517
- const repoPath = await createRepo(sandbox.sandboxRoot, {
518
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
519
- });
520
- const app = new SkillFlowApp();
521
- const added = await app.addSource(repoPath);
522
- expect(added.ok).toBe(true);
523
- if (!added.ok) {
524
- return;
525
- }
526
- const sourceId = added.data.manifest.id;
527
- const leafId = `${sourceId}:browse`;
528
- const checkoutPath = app.store.getSourceCheckoutPath("local", sourceId);
529
- await app.applyDraft(sourceId, {
530
- enabledTargets: ["openclaw"],
531
- selectedLeafIds: [leafId],
532
- });
533
- const lockBefore = await app.store.readLock();
534
- const targetPath = lockBefore.deployments.find((deployment) => deployment.sourceId === sourceId && deployment.target === "openclaw")?.targetPath;
535
- expect(targetPath).toBeTruthy();
536
- if (!targetPath) {
537
- return;
538
- }
539
- const targetBefore = await fs.readFile(path.join(targetPath, "SKILL.md"), "utf8");
540
- await writeRepoFiles(repoPath, {
541
- "browse/SKILL.md": skillDoc("browse", "Browser flow, refreshed upstream."),
542
- });
543
- const repaired = await app.repairSource([sourceId]);
544
- expect(repaired.ok).toBe(true);
545
- expect(await fs.readFile(path.join(checkoutPath, "browse", "SKILL.md"), "utf8")).toContain("Browser flow, refreshed upstream.");
546
- expect(await fs.readFile(path.join(targetPath, "SKILL.md"), "utf8")).toBe(targetBefore);
547
- });
548
- test("source updates classify changed, removed, and added leafs in order", async () => {
549
- const repoPath = await createRepo(sandbox.sandboxRoot, {
550
- "one/SKILL.md": skillDoc("one", "One."),
551
- "two/SKILL.md": skillDoc("two", "Two."),
552
- });
553
- const sourceService = createSourceService();
554
- const added = await sourceService.addSource(repoPath);
555
- expect(added.ok).toBe(true);
556
- if (!added.ok) {
557
- return;
558
- }
559
- await writeRepoFiles(repoPath, {
560
- "one/SKILL.md": skillDoc("one", "One, updated."),
561
- "three/SKILL.md": skillDoc("three", "Three."),
562
- });
563
- await fs.rm(path.join(repoPath, "two"), { recursive: true, force: true });
564
- const updated = await sourceService.updateSources([added.data.manifest.id]);
565
- expect(updated.ok).toBe(true);
566
- if (!updated.ok) {
567
- return;
568
- }
569
- const item = updated.data.updated[0];
570
- expect(item?.diffs.map((diff) => diff.kind)).toEqual([
571
- "changed",
572
- "removed",
573
- "added",
574
- ]);
575
- expect(item?.changed).toBe(true);
576
- expect(item?.addedLeafIds).toEqual([`${added.data.manifest.id}:three`]);
577
- expect(item?.removedLeafIds).toEqual([`${added.data.manifest.id}:two`]);
578
- expect(item?.invalidatedLeafIds).toEqual([]);
579
- });
580
- test("source updates classify exact-path renames as moved", async () => {
581
- const repoPath = await createRepo(sandbox.sandboxRoot, {
582
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
583
- });
584
- const sourceService = createSourceService();
585
- const added = await sourceService.addSource(repoPath);
586
- expect(added.ok).toBe(true);
587
- if (!added.ok) {
588
- return;
589
- }
590
- await fs.rename(path.join(repoPath, "browse"), path.join(repoPath, "browse-renamed"));
591
- const updated = await sourceService.updateSources([added.data.manifest.id]);
592
- expect(updated.ok).toBe(true);
593
- if (!updated.ok) {
594
- return;
595
- }
596
- const item = updated.data.updated[0];
597
- expect(item?.diffs.map((diff) => diff.kind)).toEqual(["moved"]);
598
- expect(item?.addedLeafIds).toEqual([]);
599
- expect(item?.removedLeafIds).toEqual([]);
600
- });
601
- test("source updates treat requestedPath moves out of scope as remove plus add", async () => {
602
- const repoPath = await createRepo(sandbox.sandboxRoot, {
603
- "skills/browse/SKILL.md": skillDoc("browse", "Browser flow."),
604
- });
605
- const sourceService = createSourceService();
606
- const added = await sourceService.addSource(repoPath, { path: "skills/browse" });
607
- expect(added.ok).toBe(true);
608
- if (!added.ok) {
609
- return;
610
- }
611
- await fs.rename(path.join(repoPath, "skills", "browse"), path.join(repoPath, "skills", "outside"));
612
- const updated = await sourceService.updateSources([added.data.manifest.id]);
613
- expect(updated.ok).toBe(true);
614
- if (!updated.ok) {
615
- return;
616
- }
617
- const item = updated.data.updated[0];
618
- expect(item?.diffs.map((diff) => diff.kind)).toEqual(["removed", "added"]);
619
- });
620
- test("source updates classify invalidated leafs separately from removals", async () => {
621
- const repoPath = await createRepo(sandbox.sandboxRoot, {
622
- "browse/SKILL.md": skillDoc("browse", "Browser flow."),
623
- });
624
- const sourceService = createSourceService();
625
- const added = await sourceService.addSource(repoPath);
626
- expect(added.ok).toBe(true);
627
- if (!added.ok) {
628
- return;
629
- }
630
- await writeRepoFiles(repoPath, {
631
- "browse/SKILL.md": "Broken now",
632
- });
633
- const updated = await sourceService.updateSources([added.data.manifest.id]);
634
- expect(updated.ok).toBe(true);
635
- if (!updated.ok) {
636
- return;
637
- }
638
- const item = updated.data.updated[0];
639
- expect(item?.diffs.map((diff) => diff.kind)).toEqual(["invalidated"]);
640
- expect(item?.invalidatedLeafIds).toEqual([`${added.data.manifest.id}:browse`]);
641
- expect(item?.removedLeafIds).toEqual([]);
642
- });
643
- });
644
- //# sourceMappingURL=source-lifecycle.test.js.map