orkestrate 0.1.14 → 0.2.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.
- package/AGENTS.md +56 -0
- package/CONTRIBUTING.md +35 -0
- package/README.md +38 -58
- package/SECURITY.md +24 -0
- package/bin/orkestrate.ts +2 -0
- package/docs/concepts.md +119 -0
- package/docs/demo-extension-builder.md +82 -0
- package/docs/extensions/adapters.md +57 -0
- package/docs/extensions/architecture.md +49 -0
- package/docs/extensions/introduction.md +26 -0
- package/docs/getting-started.md +85 -0
- package/docs/hosted-registry.md +90 -0
- package/docs/pack-authoring.md +75 -0
- package/docs/roadmap.md +59 -0
- package/docs/troubleshooting.md +28 -0
- package/extensions/opencode-adapter/index.ts +106 -0
- package/extensions/opencode-adapter/orkestrate.extension.json +17 -0
- package/package.json +40 -33
- package/packs/coding/harnesses/opencode/agents/coding.md +8 -0
- package/packs/coding/harnesses/opencode/opencode.json +24 -0
- package/packs/coding/harnesses/opencode/skills/orkestrate/SKILL.md +57 -0
- package/packs/coding/pack.yaml +5 -0
- package/packs/extension-builder/harnesses/opencode/agents/extension-builder.md +8 -0
- package/packs/extension-builder/harnesses/opencode/opencode.json +31 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate/SKILL.md +54 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate-pack-author/SKILL.md +59 -0
- package/packs/extension-builder/pack.yaml +5 -0
- package/src/cli/cmd/extension-submit.ts +267 -0
- package/src/cli/cmd/pack-create.ts +43 -0
- package/src/cli/cmd/pack.ts +53 -0
- package/src/cli/cmd/profile-create.ts +199 -0
- package/src/cli/cmd/profile-submit.ts +236 -0
- package/src/cli/cmd/profile-validate.ts +5 -0
- package/src/cli/cmd/registry.ts +66 -0
- package/src/cli/cmd/run.ts +37 -0
- package/src/cli/index.ts +163 -0
- package/src/cli/tui.ts +355 -0
- package/src/cli/ui/welcome.ts +73 -0
- package/src/cli.ts +1 -0
- package/src/sdk/cross-platform.ts +25 -0
- package/src/sdk/extensions/loader.ts +89 -0
- package/src/sdk/extensions/manifest.ts +193 -0
- package/src/sdk/extensions/types.ts +12 -0
- package/src/sdk/harness/sync-slice.ts +57 -0
- package/src/sdk/launch/broker.ts +87 -0
- package/src/sdk/launch/runner.ts +57 -0
- package/src/sdk/launch/terminal.ts +75 -0
- package/src/sdk/launch/types.ts +7 -0
- package/src/sdk/launch/windows.ts +109 -0
- package/src/sdk/packs/catalog.ts +172 -0
- package/src/sdk/packs/create.ts +99 -0
- package/src/sdk/packs/fs.ts +52 -0
- package/src/sdk/packs/github.ts +249 -0
- package/src/sdk/packs/paths.ts +19 -0
- package/src/sdk/packs/registry.ts +40 -0
- package/src/sdk/packs/schema.ts +51 -0
- package/src/sdk/packs/store.ts +172 -0
- package/src/sdk/profiles/catalog.ts +199 -0
- package/src/sdk/profiles/github.ts +177 -0
- package/src/sdk/profiles/install.ts +161 -0
- package/src/sdk/profiles/load.ts +209 -0
- package/src/sdk/profiles/materialize.ts +85 -0
- package/src/sdk/profiles/pack.ts +128 -0
- package/src/sdk/profiles/schema.ts +201 -0
- package/src/sdk/registry.ts +19 -0
- package/src/sdk/runs/registry.ts +142 -0
- package/src/sdk/runs/types.ts +15 -0
- package/src/sdk/types.ts +39 -0
- package/src/version.ts +3 -0
- package/dist/cli.js +0 -1668
- package/dist/cli.js.map +0 -1
- package/dist/mcp-entry.js +0 -181
- package/dist/mcp-entry.js.map +0 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseProfile, type Profile } from "./schema";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
PROFILE_MANIFEST,
|
|
7
|
+
findProfileManifestInDir,
|
|
8
|
+
getPackRootFromProfilePath,
|
|
9
|
+
listCatalogEntries,
|
|
10
|
+
pathExists,
|
|
11
|
+
} from "./pack";
|
|
12
|
+
|
|
13
|
+
const profileModuleDir = import.meta.dir ?? process.cwd();
|
|
14
|
+
const seedProfilesDir = join(profileModuleDir, "..", "..", "..", "profiles");
|
|
15
|
+
export const workspaceProfilesDir = join(process.cwd(), ".orkestrate", "profiles");
|
|
16
|
+
export const globalProfilesDir = join(homedir(), ".orkestrate", "profiles");
|
|
17
|
+
|
|
18
|
+
export type ProfileLocation = {
|
|
19
|
+
name: string;
|
|
20
|
+
manifestPath: string;
|
|
21
|
+
packRoot: string;
|
|
22
|
+
scope: "workspace" | "global";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function discoverInStore(baseDir: string, scope: "workspace" | "global"): Promise<ProfileLocation[]> {
|
|
26
|
+
const found: ProfileLocation[] = [];
|
|
27
|
+
try {
|
|
28
|
+
const entries = await readdir(baseDir);
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const full = join(baseDir, entry);
|
|
31
|
+
const info = await stat(full);
|
|
32
|
+
|
|
33
|
+
if (info.isDirectory()) {
|
|
34
|
+
const manifest = await findProfileManifestInDir(full);
|
|
35
|
+
if (manifest) {
|
|
36
|
+
found.push({
|
|
37
|
+
name: entry,
|
|
38
|
+
manifestPath: manifest,
|
|
39
|
+
packRoot: full,
|
|
40
|
+
scope,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (entry.endsWith(".json")) {
|
|
47
|
+
const name = entry.slice(0, -".json".length);
|
|
48
|
+
found.push({
|
|
49
|
+
name,
|
|
50
|
+
manifestPath: full,
|
|
51
|
+
packRoot: getPackRootFromProfilePath(full),
|
|
52
|
+
scope,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return preferPackDirsOverLegacyJson(found);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** When both `name/` and `name.json` exist, keep the directory pack. */
|
|
63
|
+
function preferPackDirsOverLegacyJson(locations: ProfileLocation[]): ProfileLocation[] {
|
|
64
|
+
const byName = new Map<string, ProfileLocation>();
|
|
65
|
+
for (const loc of locations) {
|
|
66
|
+
const existing = byName.get(loc.name);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
byName.set(loc.name, loc);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const existingIsDir = existing.manifestPath.endsWith(`${PROFILE_MANIFEST}`);
|
|
72
|
+
const nextIsDir = loc.manifestPath.endsWith(`${PROFILE_MANIFEST}`);
|
|
73
|
+
if (!existingIsDir && nextIsDir) {
|
|
74
|
+
byName.set(loc.name, loc);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...byName.values()];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function assertProfileName(name: string): string {
|
|
81
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(name)) {
|
|
82
|
+
throw new Error('Profile name must use lowercase letters, numbers, "-", or "_"');
|
|
83
|
+
}
|
|
84
|
+
return name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function ensureProfileStore(): Promise<void> {
|
|
88
|
+
await mkdir(globalProfilesDir, { recursive: true });
|
|
89
|
+
await mkdir(workspaceProfilesDir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
const catalogEntries = await listCatalogEntries(seedProfilesDir);
|
|
92
|
+
for (const entry of catalogEntries) {
|
|
93
|
+
const installedDir = join(globalProfilesDir, entry.slug);
|
|
94
|
+
if (await pathExists(installedDir)) continue;
|
|
95
|
+
|
|
96
|
+
if (entry.kind === "pack") {
|
|
97
|
+
const { copyDirectory } = await import("./pack");
|
|
98
|
+
await copyDirectory(entry.path, installedDir);
|
|
99
|
+
} else {
|
|
100
|
+
await mkdir(installedDir, { recursive: true });
|
|
101
|
+
const raw = await Bun.file(entry.path).json();
|
|
102
|
+
await writeFile(join(installedDir, PROFILE_MANIFEST), JSON.stringify(raw, null, 2) + "\n");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function resolveProfileLocations(name: string): Promise<ProfileLocation[]> {
|
|
108
|
+
await ensureProfileStore();
|
|
109
|
+
const safe = assertProfileName(name);
|
|
110
|
+
const workspace = await discoverInStore(workspaceProfilesDir, "workspace");
|
|
111
|
+
const global = await discoverInStore(globalProfilesDir, "global");
|
|
112
|
+
return [...workspace, ...global].filter((loc) => {
|
|
113
|
+
const manifestName = loc.name;
|
|
114
|
+
try {
|
|
115
|
+
return manifestName === safe;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function resolveProfileFilePath(name: string): Promise<string> {
|
|
123
|
+
const locations = await resolveProfileLocations(name);
|
|
124
|
+
if (locations.length === 0) {
|
|
125
|
+
await mkdir(workspaceProfilesDir, { recursive: true });
|
|
126
|
+
return join(workspaceProfilesDir, assertProfileName(name), PROFILE_MANIFEST);
|
|
127
|
+
}
|
|
128
|
+
const workspace = locations.find((l) => l.scope === "workspace");
|
|
129
|
+
return (workspace ?? locations[0]).manifestPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function loadProfile(name: string): Promise<Profile> {
|
|
133
|
+
const manifestPath = await resolveProfileFilePath(name);
|
|
134
|
+
if (!(await pathExists(manifestPath))) {
|
|
135
|
+
throw new Error(`Profile "${name}" was not found locally or globally.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const profile = parseProfile(await Bun.file(manifestPath).json(), (warning) => {
|
|
139
|
+
console.warn(`profile "${name}": ${warning.field} - ${warning.message}`);
|
|
140
|
+
});
|
|
141
|
+
profile.sourcePath = manifestPath;
|
|
142
|
+
profile.packRoot = getPackRootFromProfilePath(manifestPath);
|
|
143
|
+
return profile;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function listProfiles(options?: { warn?: boolean }): Promise<Profile[]> {
|
|
147
|
+
await ensureProfileStore();
|
|
148
|
+
|
|
149
|
+
const workspace = await discoverInStore(workspaceProfilesDir, "workspace");
|
|
150
|
+
const global = await discoverInStore(globalProfilesDir, "global");
|
|
151
|
+
|
|
152
|
+
const byName = new Map<string, ProfileLocation>();
|
|
153
|
+
for (const loc of preferPackDirsOverLegacyJson([...global, ...workspace])) {
|
|
154
|
+
const existing = byName.get(loc.name);
|
|
155
|
+
if (!existing) {
|
|
156
|
+
byName.set(loc.name, loc);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Workspace wins over global when both are packs
|
|
160
|
+
if (loc.scope === "workspace") {
|
|
161
|
+
byName.set(loc.name, loc);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const validProfiles: Profile[] = [];
|
|
166
|
+
for (const loc of byName.values()) {
|
|
167
|
+
try {
|
|
168
|
+
const profile = parseProfile(await Bun.file(loc.manifestPath).json());
|
|
169
|
+
profile.sourcePath = loc.manifestPath;
|
|
170
|
+
profile.packRoot = loc.packRoot;
|
|
171
|
+
validProfiles.push(profile);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (options?.warn) {
|
|
174
|
+
console.warn(`Skipped corrupted profile "${loc.name}":`, error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return validProfiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function saveProfile(
|
|
183
|
+
profile: Profile,
|
|
184
|
+
options?: { overwrite?: boolean; isGlobal?: boolean }
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
await ensureProfileStore();
|
|
187
|
+
assertProfileName(profile.name);
|
|
188
|
+
|
|
189
|
+
const base = options?.isGlobal ? globalProfilesDir : workspaceProfilesDir;
|
|
190
|
+
const dir = join(base, profile.name);
|
|
191
|
+
const manifestPath = join(dir, PROFILE_MANIFEST);
|
|
192
|
+
|
|
193
|
+
await mkdir(base, { recursive: true });
|
|
194
|
+
|
|
195
|
+
if (!options?.overwrite && (await pathExists(manifestPath))) {
|
|
196
|
+
throw new Error(`Profile "${profile.name}" already exists`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await mkdir(dir, { recursive: true });
|
|
200
|
+
await writeFile(manifestPath, JSON.stringify(profile, null, 2) + "\n", "utf-8");
|
|
201
|
+
profile.sourcePath = manifestPath;
|
|
202
|
+
profile.packRoot = dir;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @deprecated use seed dir layout; kept for re-exports */
|
|
206
|
+
export async function jsonProfileNames(dir: string): Promise<string[]> {
|
|
207
|
+
const locs = await discoverInStore(dir, "global");
|
|
208
|
+
return locs.map((l) => l.name);
|
|
209
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import type { Profile } from "./schema";
|
|
4
|
+
import { pathExists } from "./pack";
|
|
5
|
+
|
|
6
|
+
const profileModuleDir = import.meta.dir ?? process.cwd();
|
|
7
|
+
const bundledSkillsDir = join(profileModuleDir, "..", "..", "..", "skills");
|
|
8
|
+
|
|
9
|
+
async function copySkillDir(skillDir: string, targetSkillsRoot: string, skillName: string): Promise<void> {
|
|
10
|
+
const dest = join(targetSkillsRoot, skillName);
|
|
11
|
+
await mkdir(dest, { recursive: true });
|
|
12
|
+
await cp(skillDir, dest, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function resolveSkillSource(skillId: string, packRoot: string, cwd: string): Promise<string | null> {
|
|
16
|
+
const packSkill = join(packRoot, "skills", skillId);
|
|
17
|
+
if (await pathExists(join(packSkill, "SKILL.md"))) {
|
|
18
|
+
return packSkill;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const bundled = join(bundledSkillsDir, skillId);
|
|
22
|
+
if (await pathExists(join(bundled, "SKILL.md"))) {
|
|
23
|
+
return bundled;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const candidates = [
|
|
27
|
+
join(cwd, ".opencode", "skills", skillId),
|
|
28
|
+
join(cwd, ".agents", "skills", skillId),
|
|
29
|
+
join(cwd, ".claude", "skills", skillId),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
if (await pathExists(join(candidate, "SKILL.md"))) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function materializePackToOpenCodeHome(
|
|
42
|
+
profile: Profile,
|
|
43
|
+
opencodeConfigDir: string,
|
|
44
|
+
cwd: string
|
|
45
|
+
): Promise<{ skillNames: string[]; pluginPaths: string[] }> {
|
|
46
|
+
const packRoot = profile.packRoot ?? join(profile.sourcePath ?? cwd, "..");
|
|
47
|
+
const skillsOut = join(opencodeConfigDir, "skills");
|
|
48
|
+
const pluginsOut = join(opencodeConfigDir, "plugins");
|
|
49
|
+
await mkdir(skillsOut, { recursive: true });
|
|
50
|
+
await mkdir(pluginsOut, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const resources = profile.config?.resources || profile.resources;
|
|
53
|
+
const requestedSkills = new Set<string>([...(resources?.skills ?? []), "orkestrate"]);
|
|
54
|
+
const installedSkills: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (const skillId of requestedSkills) {
|
|
57
|
+
const source = await resolveSkillSource(skillId, packRoot, cwd);
|
|
58
|
+
if (!source) continue;
|
|
59
|
+
await copySkillDir(source, skillsOut, basename(source) === skillId ? skillId : basename(source));
|
|
60
|
+
installedSkills.push(skillId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pluginsDir = join(packRoot, "plugins");
|
|
64
|
+
const pluginPaths: string[] = [];
|
|
65
|
+
if (await pathExists(pluginsDir)) {
|
|
66
|
+
const entries = await readdir(pluginsDir);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!/\.(ts|js|mjs|cjs)$/.test(entry)) continue;
|
|
69
|
+
const src = join(pluginsDir, entry);
|
|
70
|
+
const dest = join(pluginsOut, entry);
|
|
71
|
+
await cp(src, dest, { force: true });
|
|
72
|
+
pluginPaths.push(dest);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { skillNames: installedSkills, pluginPaths };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildSkillAllowPermission(skillNames: string[]): Record<string, "allow"> {
|
|
80
|
+
const permission: Record<string, "allow"> = {};
|
|
81
|
+
for (const name of skillNames) {
|
|
82
|
+
permission[name] = "allow";
|
|
83
|
+
}
|
|
84
|
+
return permission;
|
|
85
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const PROFILE_MANIFEST = "profile.json";
|
|
5
|
+
|
|
6
|
+
export type PackMeta = {
|
|
7
|
+
slug?: string;
|
|
8
|
+
ref?: string;
|
|
9
|
+
packPath?: string;
|
|
10
|
+
sourceUrl?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function getPackRootFromProfilePath(profileJsonPath: string): string {
|
|
14
|
+
return join(profileJsonPath, "..");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
18
|
+
try {
|
|
19
|
+
await stat(path);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
27
|
+
await mkdir(dest, { recursive: true });
|
|
28
|
+
await cp(src, dest, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function findProfileManifestInDir(dir: string): Promise<string | null> {
|
|
32
|
+
const direct = join(dir, PROFILE_MANIFEST);
|
|
33
|
+
if (await pathExists(direct)) return direct;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** List installable catalog slugs: `name/` packs and legacy `name.json` files. */
|
|
38
|
+
export async function listCatalogEntries(catalogDir: string): Promise<
|
|
39
|
+
{ slug: string; kind: "pack" | "legacy-json"; path: string }[]
|
|
40
|
+
> {
|
|
41
|
+
const entries: { slug: string; kind: "pack" | "legacy-json"; path: string }[] = [];
|
|
42
|
+
try {
|
|
43
|
+
const names = await readdir(catalogDir);
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const full = join(catalogDir, name);
|
|
46
|
+
const info = await stat(full);
|
|
47
|
+
if (info.isDirectory()) {
|
|
48
|
+
if (await findProfileManifestInDir(full)) {
|
|
49
|
+
entries.push({ slug: name, kind: "pack", path: full });
|
|
50
|
+
}
|
|
51
|
+
} else if (name.endsWith(".json")) {
|
|
52
|
+
entries.push({ slug: name.slice(0, -".json".length), kind: "legacy-json", path: full });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return entries.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parsePackMeta(manifest: Record<string, unknown>): PackMeta {
|
|
62
|
+
const block = manifest.orkestrate;
|
|
63
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const o = block as Record<string, unknown>;
|
|
67
|
+
return {
|
|
68
|
+
slug: typeof o.slug === "string" ? o.slug : undefined,
|
|
69
|
+
ref: typeof o.ref === "string" ? o.ref : undefined,
|
|
70
|
+
packPath: typeof o.packPath === "string" ? o.packPath : undefined,
|
|
71
|
+
sourceUrl: typeof o.sourceUrl === "string" ? o.sourceUrl : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function validatePackBundle(
|
|
76
|
+
profile: { packRoot?: string; sourcePath?: string; config?: Record<string, any>; resources?: { skills?: string[] } },
|
|
77
|
+
options?: { bundledSkillsDir?: string; cwd?: string }
|
|
78
|
+
): Promise<{ errors: string[]; warnings: string[] }> {
|
|
79
|
+
const errors: string[] = [];
|
|
80
|
+
const warnings: string[] = [];
|
|
81
|
+
const packRoot = profile.packRoot ?? join(profile.sourcePath ?? ".", "..");
|
|
82
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
83
|
+
const resources = profile.config?.resources ?? profile.resources;
|
|
84
|
+
const skills = resources?.skills ?? [];
|
|
85
|
+
|
|
86
|
+
for (const skillId of skills) {
|
|
87
|
+
const packSkill = join(packRoot, "skills", skillId, "SKILL.md");
|
|
88
|
+
if (await pathExists(packSkill)) continue;
|
|
89
|
+
|
|
90
|
+
if (options?.bundledSkillsDir) {
|
|
91
|
+
const bundled = join(options.bundledSkillsDir, skillId, "SKILL.md");
|
|
92
|
+
if (await pathExists(bundled)) continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const workspaceCandidates = [
|
|
96
|
+
join(cwd, ".opencode", "skills", skillId, "SKILL.md"),
|
|
97
|
+
join(cwd, ".agents", "skills", skillId, "SKILL.md"),
|
|
98
|
+
];
|
|
99
|
+
const found = await Promise.all(workspaceCandidates.map(pathExists));
|
|
100
|
+
if (found.some(Boolean)) continue;
|
|
101
|
+
|
|
102
|
+
errors.push(`Skill "${skillId}" not found in pack, bundled skills, or workspace`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pluginsDir = join(packRoot, "plugins");
|
|
106
|
+
if (await pathExists(pluginsDir)) {
|
|
107
|
+
const entries = await readdir(pluginsDir);
|
|
108
|
+
const pluginFiles = entries.filter((e) => /\.(ts|js|mjs|cjs)$/.test(e));
|
|
109
|
+
if (entries.length > 0 && pluginFiles.length === 0) {
|
|
110
|
+
warnings.push("plugins/ exists but contains no .ts/.js plugin files");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { errors, warnings };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function applyScaffold(
|
|
118
|
+
packRoot: string,
|
|
119
|
+
workspaceCwd: string,
|
|
120
|
+
profileName: string
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const scaffoldDir = join(packRoot, "scaffold");
|
|
123
|
+
if (!(await pathExists(scaffoldDir))) return;
|
|
124
|
+
|
|
125
|
+
const target = join(workspaceCwd, ".orkestrate", "packs", profileName);
|
|
126
|
+
await mkdir(target, { recursive: true });
|
|
127
|
+
await copyDirectory(scaffoldDir, target);
|
|
128
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export type McpServerConfig = {
|
|
2
|
+
id: string;
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
env?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type Profile = {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
harness: string;
|
|
12
|
+
author?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
version?: string;
|
|
15
|
+
sourcePath?: string;
|
|
16
|
+
/** Directory containing profile.json and bundled skills/plugins/scaffold */
|
|
17
|
+
packRoot?: string;
|
|
18
|
+
info: string;
|
|
19
|
+
config: Record<string, any>;
|
|
20
|
+
|
|
21
|
+
// Optional fields for backward compatibility / type convenience in CLI / adapters
|
|
22
|
+
workspace?: {
|
|
23
|
+
root: string;
|
|
24
|
+
policy: "current-directory" | "custom";
|
|
25
|
+
};
|
|
26
|
+
model?: {
|
|
27
|
+
provider: string;
|
|
28
|
+
id: string;
|
|
29
|
+
thinking?: string;
|
|
30
|
+
cycle?: string[];
|
|
31
|
+
};
|
|
32
|
+
prompt?: string;
|
|
33
|
+
resources?: {
|
|
34
|
+
skills?: string[];
|
|
35
|
+
prompts?: string[];
|
|
36
|
+
extensions?: string[];
|
|
37
|
+
mcpServers?: McpServerConfig[];
|
|
38
|
+
};
|
|
39
|
+
tools?: {
|
|
40
|
+
allow?: string[];
|
|
41
|
+
deny?: string[];
|
|
42
|
+
};
|
|
43
|
+
session?: {
|
|
44
|
+
dir: string;
|
|
45
|
+
};
|
|
46
|
+
env?: Record<string, string>;
|
|
47
|
+
harnessConfig?: Record<string, any>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ProfileParseWarning = {
|
|
51
|
+
field: string;
|
|
52
|
+
message: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertString(value: unknown, field: string): string {
|
|
60
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
61
|
+
throw new Error(`Profile field "${field}" must be a non-empty string`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertStringArray(value: unknown, field: string): string[] {
|
|
67
|
+
if (value === undefined) return [];
|
|
68
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
69
|
+
throw new Error(`Profile field "${field}" must be an array of strings`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertWorkspacePolicy(value: unknown, field: string): "current-directory" | "custom" {
|
|
75
|
+
if (value === "current-directory" || value === "custom") {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Profile field "${field}" must be "current-directory" or "custom"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertHarnessName(value: unknown, field: string): string {
|
|
82
|
+
const raw = isRecord(value) && typeof value.adapter === "string" ? value.adapter : value;
|
|
83
|
+
const str = assertString(raw, field);
|
|
84
|
+
if (!/^[a-z0-9-]+$/.test(str)) {
|
|
85
|
+
throw new Error(`Profile field "${field}" must contain only lowercase letters, numbers, and dashes`);
|
|
86
|
+
}
|
|
87
|
+
return str;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertProfileNameValue(value: unknown, field: string): string {
|
|
91
|
+
const str = assertString(value, field);
|
|
92
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(str)) {
|
|
93
|
+
throw new Error('Profile name must use lowercase letters, numbers, "-", or "_"');
|
|
94
|
+
}
|
|
95
|
+
return str;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseProfile(value: unknown, warn?: (warning: ProfileParseWarning) => void): Profile {
|
|
99
|
+
if (!isRecord(value)) {
|
|
100
|
+
throw new Error("Profile must be an object");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const name = assertProfileNameValue(value.name, "name");
|
|
104
|
+
const description = assertString(value.description, "description");
|
|
105
|
+
const harness = assertHarnessName(value.harness, "harness");
|
|
106
|
+
const info = assertString(value.info, "info");
|
|
107
|
+
|
|
108
|
+
const author = typeof value.author === "string" ? value.author : undefined;
|
|
109
|
+
const version = typeof value.version === "string" ? value.version : undefined;
|
|
110
|
+
const tags = Array.isArray(value.tags) && value.tags.every((t) => typeof t === "string")
|
|
111
|
+
? (value.tags as string[])
|
|
112
|
+
: undefined;
|
|
113
|
+
|
|
114
|
+
let config: Record<string, any> = {};
|
|
115
|
+
if (isRecord(value.config)) {
|
|
116
|
+
config = value.config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Populate config from legacy top-level fields if config is empty
|
|
120
|
+
if (Object.keys(config).length === 0) {
|
|
121
|
+
const legacyKeys = ["workspace", "model", "prompt", "resources", "tools", "session", "env", "harnessConfig"];
|
|
122
|
+
for (const key of legacyKeys) {
|
|
123
|
+
if (key in value) {
|
|
124
|
+
config[key] = value[key];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const profile: Profile = {
|
|
130
|
+
name,
|
|
131
|
+
description,
|
|
132
|
+
harness,
|
|
133
|
+
info,
|
|
134
|
+
author,
|
|
135
|
+
tags,
|
|
136
|
+
version,
|
|
137
|
+
config,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Optional typing fields mapping for convenience/backward compatibility
|
|
141
|
+
const configWorkspace = config.workspace || value.workspace;
|
|
142
|
+
if (isRecord(configWorkspace)) {
|
|
143
|
+
profile.workspace = {
|
|
144
|
+
root: typeof configWorkspace.root === "string" ? configWorkspace.root : ".",
|
|
145
|
+
policy: assertWorkspacePolicy(configWorkspace.policy || "current-directory", "workspace.policy"),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const configModel = config.model || value.model;
|
|
150
|
+
if (isRecord(configModel)) {
|
|
151
|
+
profile.model = {
|
|
152
|
+
provider: typeof configModel.provider === "string" ? configModel.provider : "default",
|
|
153
|
+
id: typeof configModel.id === "string" ? configModel.id : "default",
|
|
154
|
+
thinking: typeof configModel.thinking === "string" ? configModel.thinking : undefined,
|
|
155
|
+
cycle: Array.isArray(configModel.cycle) ? assertStringArray(configModel.cycle, "model.cycle") : undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const configPrompt = config.prompt || value.prompt;
|
|
160
|
+
if (typeof configPrompt === "string") {
|
|
161
|
+
profile.prompt = configPrompt;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const configResources = config.resources || value.resources;
|
|
165
|
+
if (isRecord(configResources)) {
|
|
166
|
+
profile.resources = {
|
|
167
|
+
skills: assertStringArray(configResources.skills, "resources.skills"),
|
|
168
|
+
prompts: assertStringArray(configResources.prompts, "resources.prompts"),
|
|
169
|
+
extensions: assertStringArray(configResources.extensions, "resources.extensions"),
|
|
170
|
+
mcpServers: (configResources.mcpServers as McpServerConfig[]) || [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const configTools = config.tools || value.tools;
|
|
175
|
+
if (isRecord(configTools)) {
|
|
176
|
+
profile.tools = {
|
|
177
|
+
allow: assertStringArray(configTools.allow, "tools.allow"),
|
|
178
|
+
deny: assertStringArray(configTools.deny, "tools.deny"),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const configSession = config.session || value.session;
|
|
183
|
+
if (isRecord(configSession)) {
|
|
184
|
+
profile.session = {
|
|
185
|
+
dir: typeof configSession.dir === "string" ? configSession.dir : "",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const configEnv = config.env || value.env;
|
|
190
|
+
if (isRecord(configEnv)) {
|
|
191
|
+
profile.env = configEnv as Record<string, string>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const configHarnessConfig = config.harnessConfig || value.harnessConfig;
|
|
195
|
+
if (isRecord(configHarnessConfig)) {
|
|
196
|
+
profile.harnessConfig = configHarnessConfig;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return profile;
|
|
200
|
+
}
|
|
201
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HarnessDriver } from "./types";
|
|
2
|
+
|
|
3
|
+
class ExtensionRegistry {
|
|
4
|
+
private adapters = new Map<string, HarnessDriver>();
|
|
5
|
+
|
|
6
|
+
registerAdapter(harness: string, adapter: HarnessDriver): void {
|
|
7
|
+
this.adapters.set(harness, adapter);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getAdapter(harness: string): HarnessDriver | undefined {
|
|
11
|
+
return this.adapters.get(harness);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const registry = new ExtensionRegistry();
|
|
17
|
+
|
|
18
|
+
// Convenience alias for direct use outside extensions
|
|
19
|
+
export const getAdapter = registry.getAdapter.bind(registry);
|