@clossys/launcher 0.1.1 → 0.1.5

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 (55) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +49 -9
  3. package/dist/check-cli.d.ts.map +1 -1
  4. package/dist/check-cli.js +9 -1
  5. package/dist/check-cli.js.map +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +14 -6
  9. package/dist/cli.js.map +1 -1
  10. package/dist/core.d.ts +28 -5
  11. package/dist/core.d.ts.map +1 -1
  12. package/dist/core.js +389 -32
  13. package/dist/core.js.map +1 -1
  14. package/dist/host.d.ts.map +1 -1
  15. package/dist/host.js +16 -1
  16. package/dist/host.js.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/skills.d.ts +20 -0
  22. package/dist/skills.d.ts.map +1 -0
  23. package/dist/skills.js +106 -0
  24. package/dist/skills.js.map +1 -0
  25. package/dist/types.d.ts +50 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +3 -2
  28. package/skeleton/README.md +11 -21
  29. package/skill-catalogue/advisor/SKILL.md +50 -0
  30. package/skill-catalogue/architect/SKILL.md +42 -0
  31. package/skill-catalogue/bouncer/SKILL.md +43 -0
  32. package/skill-catalogue/builder/SKILL.md +42 -0
  33. package/skill-catalogue/butler/SKILL.md +43 -0
  34. package/skill-catalogue/controller/SKILL.md +42 -0
  35. package/skill-catalogue/designer/SKILL.md +50 -0
  36. package/skill-catalogue/giver/SKILL.md +43 -0
  37. package/skill-catalogue/influencer/SKILL.md +43 -0
  38. package/skill-catalogue/inspector/SKILL.md +42 -0
  39. package/skill-catalogue/integrator/SKILL.md +42 -0
  40. package/skill-catalogue/keeper/SKILL.md +43 -0
  41. package/skill-catalogue/launcher/SKILL.md +53 -0
  42. package/skill-catalogue/locksmith/SKILL.md +42 -0
  43. package/skill-catalogue/messenger/SKILL.md +43 -0
  44. package/skill-catalogue/observer/SKILL.md +42 -0
  45. package/skill-catalogue/publisher/SKILL.md +50 -0
  46. package/skill-catalogue/starter/SKILL.md +48 -0
  47. package/skill-catalogue/strategist/SKILL.md +51 -0
  48. package/skill-catalogue/writer/SKILL.md +51 -0
  49. package/src/check-cli.ts +9 -1
  50. package/src/cli.ts +13 -6
  51. package/src/core.ts +442 -28
  52. package/src/host.ts +14 -1
  53. package/src/index.ts +10 -0
  54. package/src/skills.ts +120 -0
  55. package/src/types.ts +50 -0
package/src/host.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
3
4
  import type { CommandResult, WorkspaceHost } from "./types.js";
4
5
 
5
6
  function run(command: string, args: readonly string[], options?: { cwd?: string }): CommandResult {
@@ -43,6 +44,13 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
43
44
  now: () => new Date().toISOString(),
44
45
  exists: (path) => existsSync(path),
45
46
  isDirectory: (path) => existsSync(path) && statSync(path).isDirectory(),
47
+ isSymlink: (path) => {
48
+ try {
49
+ return lstatSync(path).isSymbolicLink();
50
+ } catch {
51
+ return false;
52
+ }
53
+ },
46
54
  readText: (path) => {
47
55
  try {
48
56
  return readFileSync(path, "utf8");
@@ -56,6 +64,11 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
56
64
  mkdirp: (path) => {
57
65
  mkdirSync(path, { recursive: true });
58
66
  },
67
+ symlink: (relativeTarget, linkPath) => {
68
+ mkdirSync(dirname(linkPath), { recursive: true });
69
+ if (existsSync(linkPath)) rmSync(linkPath, { recursive: true, force: true });
70
+ symlinkSync(relativeTarget, linkPath, "dir");
71
+ },
59
72
  readDir: (path) => readdirSync(path),
60
73
  run,
61
74
  prompt,
package/src/index.ts CHANGED
@@ -1,23 +1,33 @@
1
1
  /** Consumer-owned workspace hub scaffolder: create, resume, or appoint a GitHub repository. */
2
2
  export {
3
3
  applyWorkspacePlan,
4
+ checkInventoryEntries,
4
5
  formatHubHealth,
6
+ hasAdvisorPin,
5
7
  inspectInventory,
6
8
  isHubDocument,
9
+ launcherPackageRootFromModule,
7
10
  observeWorkspace,
8
11
  parseGitHubRemote,
9
12
  planWorkspace,
13
+ readInventoryRepositories,
10
14
  reportHubHealth,
11
15
  DEFAULT_REPOSITORY_NAME,
12
16
  WORKSPACE_INVENTORY_REL,
13
17
  WORKSPACE_MARKER_REL,
14
18
  } from "./core.js";
15
19
  export type {
20
+ ApplyWorkspaceOptions,
16
21
  CommandResult,
17
22
  CwdObservation,
23
+ DependencyBucket,
18
24
  HubDocument,
19
25
  HubHealthReport,
20
26
  InventoryObservation,
27
+ InventoryValidationEntry,
28
+ InventoryValidationReport,
29
+ PinFinding,
30
+ PinGrade,
21
31
  WorkspaceApplyResult,
22
32
  WorkspaceDecision,
23
33
  WorkspaceHost,
package/src/skills.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { dirname, join, resolve, sep } from "node:path";
2
+ import type { WorkspaceHost } from "./types.js";
3
+
4
+ export interface SkillCompositionResult {
5
+ readonly composed: readonly string[];
6
+ readonly skipped: readonly { readonly packageDir: string; readonly note: string }[];
7
+ }
8
+
9
+ export interface ComposeSkillsOptions {
10
+ readonly skillCatalogueRoot?: string;
11
+ readonly launcherPackageRoot: string;
12
+ }
13
+
14
+ const DISCOVERY_PREFIXES = [".cursor/skills", ".claude/skills"] as const;
15
+ const AGENTS_SKILLS_REL = join(".agents", "skills");
16
+
17
+ function containedPath(root: string, relativePath: string): string {
18
+ const resolved = resolve(root, relativePath);
19
+ const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
20
+ if (resolved !== root && !resolved.startsWith(prefix)) {
21
+ throw new Error(`refusing to write outside the workspace directory: ${relativePath}`);
22
+ }
23
+ return resolved;
24
+ }
25
+
26
+ function skillSourceCandidates(packageDir: string, options: ComposeSkillsOptions): string[] {
27
+ const paths: string[] = [];
28
+ if (options.skillCatalogueRoot) {
29
+ paths.push(join(options.skillCatalogueRoot, packageDir, "SKILL.md"));
30
+ }
31
+ paths.push(join(options.launcherPackageRoot, "skill-catalogue", packageDir, "SKILL.md"));
32
+ paths.push(join(options.launcherPackageRoot, "..", packageDir, "skill", "SKILL.md"));
33
+ return paths;
34
+ }
35
+
36
+ function readSkillBody(host: WorkspaceHost, packageDir: string, options: ComposeSkillsOptions): string | null {
37
+ for (const path of skillSourceCandidates(packageDir, options)) {
38
+ const body = host.readText(path);
39
+ if (body !== null) return body;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /** Package directory names that have a resolvable skill source in the catalogue or monorepo. */
45
+ export function listSkillPackageCandidates(host: WorkspaceHost, options: ComposeSkillsOptions): string[] {
46
+ const names = new Set<string>();
47
+ const considerRoot = (root: string | undefined): void => {
48
+ if (root === undefined || !host.isDirectory(root)) return;
49
+ for (const name of host.readDir(root)) {
50
+ if (name.startsWith(".")) continue;
51
+ const skillPath = join(root, name, "SKILL.md");
52
+ if (host.readText(skillPath) !== null) names.add(name);
53
+ }
54
+ };
55
+ considerRoot(options.skillCatalogueRoot);
56
+ considerRoot(join(options.launcherPackageRoot, "skill-catalogue"));
57
+ const packagesSibling = join(options.launcherPackageRoot, "..");
58
+ if (host.isDirectory(packagesSibling)) {
59
+ for (const name of host.readDir(packagesSibling)) {
60
+ if (name.startsWith(".")) continue;
61
+ const skillPath = join(packagesSibling, name, "skill", "SKILL.md");
62
+ if (host.readText(skillPath) !== null) names.add(name);
63
+ }
64
+ }
65
+ return [...names].sort((a, b) => a.localeCompare(b));
66
+ }
67
+
68
+ function writeAgentsSkill(host: WorkspaceHost, directory: string, packageDir: string, body: string): void {
69
+ const relativeDir = join(AGENTS_SKILLS_REL, `clossys-${packageDir}`);
70
+ const skillRel = join(relativeDir, "SKILL.md");
71
+ const target = containedPath(directory, skillRel);
72
+ host.mkdirp(dirname(target));
73
+ host.writeText(target, body.endsWith("\n") ? body : `${body}\n`);
74
+ }
75
+
76
+ function writeDiscoveryLink(host: WorkspaceHost, directory: string, packageDir: string, skillBody: string): void {
77
+ const agentsDirRel = join(AGENTS_SKILLS_REL, `clossys-${packageDir}`);
78
+ const linkTarget = join("..", "..", agentsDirRel);
79
+ for (const prefix of DISCOVERY_PREFIXES) {
80
+ const prefixPath = containedPath(directory, prefix);
81
+ // A directory-symlink onto `.agents/skills` already exposes every composed
82
+ // skill. Writing a nested `.claude/skills/clossys-<pkg>` link follows that
83
+ // symlink and replaces the just-written SKILL.md with a circular link.
84
+ if (host.isSymlink(prefixPath)) continue;
85
+ const linkRel = join(prefix, `clossys-${packageDir}`);
86
+ const linkPath = containedPath(directory, linkRel);
87
+ host.mkdirp(dirname(linkPath));
88
+ try {
89
+ host.symlink(linkTarget, linkPath);
90
+ } catch {
91
+ const fallbackDir = containedPath(directory, join(linkRel, "SKILL.md"));
92
+ host.mkdirp(dirname(fallbackDir));
93
+ host.writeText(fallbackDir, skillBody.endsWith("\n") ? skillBody : `${skillBody}\n`);
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Writes `.agents/skills/clossys-<pkg>/SKILL.md` and host discovery links for every
100
+ * catalogue package. Missing sources are skipped with notes; the hub apply continues.
101
+ */
102
+ export function composeSkills(
103
+ host: WorkspaceHost,
104
+ directory: string,
105
+ options: ComposeSkillsOptions,
106
+ ): SkillCompositionResult {
107
+ const composed: string[] = [];
108
+ const skipped: { packageDir: string; note: string }[] = [];
109
+ for (const packageDir of listSkillPackageCandidates(host, options)) {
110
+ const body = readSkillBody(host, packageDir, options);
111
+ if (body === null) {
112
+ skipped.push({ packageDir, note: "skill source missing at apply time" });
113
+ continue;
114
+ }
115
+ writeAgentsSkill(host, directory, packageDir, body);
116
+ writeDiscoveryLink(host, directory, packageDir, body);
117
+ composed.push(packageDir);
118
+ }
119
+ return { composed, skipped };
120
+ }
package/src/types.ts CHANGED
@@ -16,9 +16,13 @@ export interface WorkspaceHost {
16
16
  now(): string;
17
17
  exists(path: string): boolean;
18
18
  isDirectory(path: string): boolean;
19
+ /** True when path exists and is a symlink (lstat; does not follow). Missing path is false. */
20
+ isSymlink(path: string): boolean;
19
21
  readText(path: string): string | null;
20
22
  writeText(path: string, contents: string): void;
21
23
  mkdirp(path: string): void;
24
+ /** Creates a relative symlink at linkPath pointing at relativeTarget (directory link). */
25
+ symlink(relativeTarget: string, linkPath: string): void;
22
26
  readDir(path: string): string[];
23
27
  run(command: string, args: readonly string[], options?: { cwd?: string }): CommandResult;
24
28
  prompt(message: string, choices: readonly string[]): string | null;
@@ -37,6 +41,34 @@ export interface InventoryObservation {
37
41
  readonly count: number;
38
42
  }
39
43
 
44
+ /** A package.json dependency bucket scanned for the advisor pin. */
45
+ export type DependencyBucket = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
46
+
47
+ /** Staleness verdict for one pinned advisor version against the live registry version. */
48
+ export type PinGrade = "stale" | "current" | "indeterminate";
49
+
50
+ /** One graded advisor pin in one dependency bucket. */
51
+ export interface PinFinding {
52
+ readonly bucket: DependencyBucket;
53
+ readonly pinned: string;
54
+ readonly grade: PinGrade;
55
+ readonly note?: string;
56
+ }
57
+
58
+ /** Verdict for one hub inventory repository id after a read-only existence check. */
59
+ export interface InventoryValidationEntry {
60
+ readonly id: string;
61
+ readonly known: boolean | null;
62
+ readonly note?: string;
63
+ }
64
+
65
+ /** Read-only validation outcome for hub inventory ids. Never mutates the inventory. */
66
+ export interface InventoryValidationReport {
67
+ readonly entries: readonly InventoryValidationEntry[];
68
+ readonly skipped: boolean;
69
+ readonly note?: string;
70
+ }
71
+
40
72
  /** Read-only pin and inventory report after adopt or resume. Never uninstalls. */
41
73
  export interface HubHealthReport {
42
74
  readonly marker: "present" | "missing";
@@ -44,10 +76,26 @@ export interface HubHealthReport {
44
76
  readonly advisorPin: {
45
77
  readonly dependencies?: string;
46
78
  readonly devDependencies?: string;
79
+ readonly optionalDependencies?: string;
80
+ readonly peerDependencies?: string;
47
81
  readonly live?: string;
48
82
  };
49
83
  readonly dualPin: boolean;
50
84
  readonly extraClossys: readonly string[];
85
+ readonly pinFindings: readonly PinFinding[];
86
+ readonly degraded: boolean;
87
+ readonly skillComposition?: {
88
+ readonly composed: readonly string[];
89
+ readonly skipped: readonly { readonly packageDir: string; readonly note: string }[];
90
+ readonly rosterTargets?: readonly string[];
91
+ readonly rosterSkipped?: readonly { readonly inventoryId: string; readonly note: string }[];
92
+ };
93
+ }
94
+
95
+ /** Optional paths for skill composition during apply. */
96
+ export interface ApplyWorkspaceOptions {
97
+ readonly skillCatalogueRoot?: string;
98
+ readonly launcherPackageRoot?: string;
51
99
  }
52
100
 
53
101
  export interface CwdObservation {
@@ -95,6 +143,8 @@ export interface WorkspacePlanAdopt {
95
143
  readonly advisorVersion: string;
96
144
  /** Absolute path of a populated inventory document to copy. Absent when cwd already has one. */
97
145
  readonly inventorySource?: string;
146
+ /** Merged repository ids (on-disk first, then new ids from --inventory) written when both sources are populated. */
147
+ readonly mergedInventoryIds?: readonly string[];
98
148
  }
99
149
 
100
150
  export type WorkspacePlan = WorkspacePlanCreate | WorkspacePlanResume | WorkspacePlanAdopt;