@ex-machina/facets 0.1.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/package.json +27 -0
- package/src/__tests__/e2e.test.ts +129 -0
- package/src/cli/init.ts +58 -0
- package/src/cli.ts +206 -0
- package/src/discovery/__tests__/list.test.ts +114 -0
- package/src/discovery/cache.ts +218 -0
- package/src/discovery/clear.ts +15 -0
- package/src/discovery/list.ts +129 -0
- package/src/index.ts +27 -0
- package/src/installation/__tests__/install.test.ts +210 -0
- package/src/installation/install.ts +232 -0
- package/src/installation/status.ts +42 -0
- package/src/installation/uninstall.ts +116 -0
- package/src/registry/__tests__/loader.test.ts +67 -0
- package/src/registry/__tests__/schemas.test.ts +206 -0
- package/src/registry/files.ts +60 -0
- package/src/registry/loader.ts +42 -0
- package/src/registry/schemas.ts +119 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import yaml from "js-yaml"
|
|
2
|
+
import { FacetsYamlSchema, FacetsLockSchema, type FacetsYaml, type FacetsLock } from "./schemas.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Platform-specific paths for facets config files.
|
|
6
|
+
* In v1, this is always .opencode/.
|
|
7
|
+
*/
|
|
8
|
+
export function facetsYamlPath(projectRoot: string): string {
|
|
9
|
+
return `${projectRoot}/.opencode/facets.yaml`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function facetsLockPath(projectRoot: string): string {
|
|
13
|
+
return `${projectRoot}/.opencode/facets.lock`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function localFacetsDir(projectRoot: string): string {
|
|
17
|
+
return `${projectRoot}/.opencode/facets`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- facets.yaml ---
|
|
21
|
+
|
|
22
|
+
export async function readFacetsYaml(projectRoot: string): Promise<FacetsYaml> {
|
|
23
|
+
const filePath = facetsYamlPath(projectRoot)
|
|
24
|
+
try {
|
|
25
|
+
const raw = await Bun.file(filePath).text()
|
|
26
|
+
const parsed = yaml.load(raw)
|
|
27
|
+
const result = FacetsYamlSchema.safeParse(parsed)
|
|
28
|
+
if (result.success) return result.data
|
|
29
|
+
return { remote: {}, local: [] }
|
|
30
|
+
} catch {
|
|
31
|
+
return { remote: {}, local: [] }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function writeFacetsYaml(projectRoot: string, data: FacetsYaml): Promise<void> {
|
|
36
|
+
const filePath = facetsYamlPath(projectRoot)
|
|
37
|
+
const content = yaml.dump(data, { lineWidth: -1, noRefs: true })
|
|
38
|
+
await Bun.write(filePath, content)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- facets.lock ---
|
|
42
|
+
|
|
43
|
+
export async function readFacetsLock(projectRoot: string): Promise<FacetsLock> {
|
|
44
|
+
const filePath = facetsLockPath(projectRoot)
|
|
45
|
+
try {
|
|
46
|
+
const raw = await Bun.file(filePath).text()
|
|
47
|
+
const parsed = yaml.load(raw)
|
|
48
|
+
const result = FacetsLockSchema.safeParse(parsed)
|
|
49
|
+
if (result.success) return result.data
|
|
50
|
+
return { remote: {} }
|
|
51
|
+
} catch {
|
|
52
|
+
return { remote: {} }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function writeFacetsLock(projectRoot: string, data: FacetsLock): Promise<void> {
|
|
57
|
+
const filePath = facetsLockPath(projectRoot)
|
|
58
|
+
const content = yaml.dump(data, { lineWidth: -1, noRefs: true })
|
|
59
|
+
await Bun.write(filePath, content)
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import yaml from "js-yaml"
|
|
2
|
+
import { FacetManifestSchema, type FacetManifest } from "./schemas.ts"
|
|
3
|
+
|
|
4
|
+
export interface LoadManifestSuccess {
|
|
5
|
+
success: true
|
|
6
|
+
manifest: FacetManifest
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LoadManifestError {
|
|
10
|
+
success: false
|
|
11
|
+
error: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LoadManifestResult = LoadManifestSuccess | LoadManifestError
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load and validate a facet.yaml manifest from the given path.
|
|
18
|
+
* Returns the parsed manifest or a structured error.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadManifest(manifestPath: string): Promise<LoadManifestResult> {
|
|
21
|
+
let raw: string
|
|
22
|
+
try {
|
|
23
|
+
raw = await Bun.file(manifestPath).text()
|
|
24
|
+
} catch {
|
|
25
|
+
return { success: false, error: `Cannot read manifest: ${manifestPath}` }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let parsed: unknown
|
|
29
|
+
try {
|
|
30
|
+
parsed = yaml.load(raw)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { success: false, error: `Invalid YAML in manifest: ${err}` }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = FacetManifestSchema.safeParse(parsed)
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
38
|
+
return { success: false, error: `Invalid manifest: ${issues}` }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { success: true, manifest: result.data }
|
|
42
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod/v4"
|
|
2
|
+
|
|
3
|
+
// --- Prompt field: string (file path) or object with file/url ---
|
|
4
|
+
|
|
5
|
+
const PromptSchema = z.union([
|
|
6
|
+
z.string(),
|
|
7
|
+
z.object({ file: z.string() }),
|
|
8
|
+
z.object({ url: z.string().url() }),
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
// --- Agent descriptor ---
|
|
12
|
+
|
|
13
|
+
const AgentPlatformOpencode = z
|
|
14
|
+
.object({
|
|
15
|
+
tools: z.union([
|
|
16
|
+
z.record(z.string(), z.boolean()),
|
|
17
|
+
z.array(z.string()),
|
|
18
|
+
]).optional(),
|
|
19
|
+
})
|
|
20
|
+
.passthrough()
|
|
21
|
+
|
|
22
|
+
const AgentPlatformGeneric = z.record(z.string(), z.unknown())
|
|
23
|
+
|
|
24
|
+
const AgentDescriptorSchema = z.object({
|
|
25
|
+
description: z.string().optional(),
|
|
26
|
+
prompt: PromptSchema,
|
|
27
|
+
platforms: z
|
|
28
|
+
.object({
|
|
29
|
+
opencode: AgentPlatformOpencode.optional(),
|
|
30
|
+
})
|
|
31
|
+
.catchall(AgentPlatformGeneric)
|
|
32
|
+
.optional(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// --- Command descriptor ---
|
|
36
|
+
|
|
37
|
+
const CommandDescriptorSchema = z.object({
|
|
38
|
+
description: z.string().optional(),
|
|
39
|
+
prompt: PromptSchema,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// --- Platform section ---
|
|
43
|
+
|
|
44
|
+
const PlatformOpencodeSchema = z.object({
|
|
45
|
+
tools: z.array(z.string()).optional(),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const PlatformsSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
opencode: PlatformOpencodeSchema.optional(),
|
|
51
|
+
})
|
|
52
|
+
.catchall(z.record(z.string(), z.unknown()))
|
|
53
|
+
|
|
54
|
+
// --- Requires: string or array of strings ---
|
|
55
|
+
|
|
56
|
+
const RequiresSchema = z.union([z.string(), z.array(z.string())])
|
|
57
|
+
|
|
58
|
+
// --- FacetManifest (facet.yaml) ---
|
|
59
|
+
|
|
60
|
+
export const FacetManifestSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
name: z.string().min(1),
|
|
63
|
+
version: z.string().min(1),
|
|
64
|
+
description: z.string().optional(),
|
|
65
|
+
author: z.string().optional(),
|
|
66
|
+
requires: RequiresSchema.optional(),
|
|
67
|
+
skills: z.array(z.string()).optional(),
|
|
68
|
+
agents: z.record(z.string(), AgentDescriptorSchema).optional(),
|
|
69
|
+
commands: z.record(z.string(), CommandDescriptorSchema).optional(),
|
|
70
|
+
platforms: PlatformsSchema.optional(),
|
|
71
|
+
})
|
|
72
|
+
.passthrough()
|
|
73
|
+
|
|
74
|
+
export type FacetManifest = z.infer<typeof FacetManifestSchema>
|
|
75
|
+
|
|
76
|
+
// --- FacetsYaml (facets.yaml — project dependency file) ---
|
|
77
|
+
|
|
78
|
+
const RemoteEntrySchema = z.object({
|
|
79
|
+
url: z.string().url(),
|
|
80
|
+
version: z.string().optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export const FacetsYamlSchema = z.object({
|
|
84
|
+
remote: z.record(z.string(), RemoteEntrySchema).optional(),
|
|
85
|
+
local: z.array(z.string()).optional(),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export type FacetsYaml = z.infer<typeof FacetsYamlSchema>
|
|
89
|
+
|
|
90
|
+
// --- FacetsLock (facets.lock — lockfile) ---
|
|
91
|
+
|
|
92
|
+
const LockEntrySchema = z.object({
|
|
93
|
+
url: z.string().url(),
|
|
94
|
+
version: z.string(),
|
|
95
|
+
integrity: z.string(),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const FacetsLockSchema = z.object({
|
|
99
|
+
remote: z.record(z.string(), LockEntrySchema).optional(),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
export type FacetsLock = z.infer<typeof FacetsLockSchema>
|
|
103
|
+
|
|
104
|
+
// --- Helpers ---
|
|
105
|
+
|
|
106
|
+
/** Normalize requires to always be an array */
|
|
107
|
+
export function normalizeRequires(requires: string | string[] | undefined): string[] {
|
|
108
|
+
if (!requires) return []
|
|
109
|
+
if (typeof requires === "string") return [requires]
|
|
110
|
+
return requires
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve a prompt field to a file path relative to the facet directory */
|
|
114
|
+
export function resolvePromptPath(prompt: z.infer<typeof PromptSchema>): string | null {
|
|
115
|
+
if (typeof prompt === "string") return prompt
|
|
116
|
+
if ("file" in prompt) return prompt.file
|
|
117
|
+
// URL prompts are not resolved to local paths
|
|
118
|
+
return null
|
|
119
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|