@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.
@@ -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
+ }