pluribus-context 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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * pluribus init — create a pluribus.md file in the current directory.
3
+ * Supports --name, --description, --tools flags or interactive prompts.
4
+ */
5
+
6
+ import * as fs from 'fs'
7
+ import * as path from 'path'
8
+ import * as readline from 'readline'
9
+ import { SUPPORTED_TOOLS } from '../skills/built-in.js'
10
+
11
+ const DEFAULT_TOOLS = ['claude', 'cursor', 'openclaw']
12
+
13
+ /**
14
+ * Ask a question via readline and return the answer.
15
+ * @param {readline.Interface} rl
16
+ * @param {string} question
17
+ * @param {string} defaultValue
18
+ * @returns {Promise<string>}
19
+ */
20
+ function ask(rl, question, defaultValue) {
21
+ return new Promise((resolve) => {
22
+ const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `
23
+ rl.question(prompt, (answer) => {
24
+ resolve(answer.trim() || defaultValue || '')
25
+ })
26
+ })
27
+ }
28
+
29
+ /**
30
+ * @param {Record<string, string | boolean>} args
31
+ */
32
+ export async function runInit(args) {
33
+ const targetDir = process.cwd()
34
+ const outputPath = path.join(targetDir, 'pluribus.md')
35
+
36
+ // Check if file already exists
37
+ if (fs.existsSync(outputPath)) {
38
+ console.log(`āš ļø pluribus.md already exists at ${outputPath}`)
39
+ console.log(' Delete it first or run \`pluribus sync\` to regenerate outputs.')
40
+ process.exit(1)
41
+ }
42
+
43
+ let name = args.name || ''
44
+ let description = args.description || ''
45
+ let toolsRaw = args.tools || ''
46
+
47
+ const isInteractive = !args.name && !args.description && process.stdin.isTTY
48
+
49
+ if (isInteractive) {
50
+ console.log('\nšŸ“ Pluribus Init — let\'s set up your context file.\n')
51
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
52
+
53
+ name = await ask(rl, ' Your name or team name', path.basename(targetDir))
54
+ description = await ask(rl, ' Project description (one line)', 'A new project')
55
+ const toolsDefault = DEFAULT_TOOLS.join(',')
56
+ const toolsInput = await ask(rl, ` Tools to enable (${SUPPORTED_TOOLS.join(',')})`, toolsDefault)
57
+ toolsRaw = toolsInput
58
+
59
+ rl.close()
60
+ console.log('')
61
+ } else {
62
+ // Non-interactive: fill defaults
63
+ if (!name) name = path.basename(targetDir)
64
+ if (!description) description = 'A new project'
65
+ if (!toolsRaw) toolsRaw = DEFAULT_TOOLS.join(',')
66
+ }
67
+
68
+ const tools = String(toolsRaw)
69
+ .split(',')
70
+ .map((t) => t.trim().toLowerCase())
71
+ .filter((t) => t.length > 0)
72
+
73
+ const unknownTools = tools.filter((t) => !SUPPORTED_TOOLS.includes(t))
74
+ if (unknownTools.length > 0) {
75
+ console.warn(`āš ļø Unknown tools (will be ignored by sync): ${unknownTools.join(', ')}`)
76
+ console.warn(` Supported: ${SUPPORTED_TOOLS.join(', ')}`)
77
+ }
78
+
79
+ const content = generatePluribusTemplate(name, description, tools)
80
+
81
+ fs.writeFileSync(outputPath, content, 'utf8')
82
+
83
+ console.log(`āœ… pluribus.md created at ${outputPath}`)
84
+ console.log(`šŸ“ Edit the file to fill in your project context.`)
85
+ console.log(`šŸ”„ Run \`pluribus sync\` to generate tool-specific files.`)
86
+ console.log(`\n Tools enabled: ${tools.join(', ')}`)
87
+ }
88
+
89
+ /**
90
+ * Generate the pluribus.md template content.
91
+ * @param {string} name
92
+ * @param {string} description
93
+ * @param {string[]} tools
94
+ * @returns {string}
95
+ */
96
+ function generatePluribusTemplate(name, description, tools) {
97
+ const toolsComment = tools.length > 0
98
+ ? `<!-- pluribus:tools: ${tools.join(',')} -->`
99
+ : ''
100
+
101
+ return `${toolsComment}
102
+
103
+ # Identity
104
+
105
+ I am ${name}, building **${description}**.
106
+
107
+ <!-- Describe who you are, what the project is, and (if applicable) the AI persona to adopt. Keep this concise — 3–10 lines. -->
108
+
109
+ # Stack
110
+
111
+ <!-- List your full technical picture: language + version, frameworks, key libraries, test tools, linter/formatter, infrastructure. -->
112
+ <!-- Example:
113
+ - **Language:** TypeScript 5.4 (strict mode)
114
+ - **Runtime:** Node.js 22 LTS
115
+ - **Framework:** None — pure Node CLI
116
+ - **Testing:** Jest 29 + ts-jest
117
+ - **Package manager:** pnpm
118
+ -->
119
+
120
+ - **Language:** (e.g. TypeScript 5.4)
121
+ - **Runtime:** (e.g. Node.js 22 LTS)
122
+ - **Framework:** (e.g. None)
123
+
124
+ # Conventions
125
+
126
+ <!-- How code is written in this project. Be opinionated and explicit. Cover: async patterns, error handling, naming conventions, file structure, forbidden patterns. -->
127
+ <!-- Example:
128
+ - Always use \`async/await\` — never \`.then()/.catch()\` chains
129
+ - No class-based code — use plain functions and closures
130
+ - File naming: \`kebab-case.ts\`
131
+ -->
132
+
133
+ - (Add your coding conventions here)
134
+
135
+ # Goals
136
+
137
+ <!-- What this project is optimizing for. List 3–7 goals in priority order. Be specific to this project. -->
138
+
139
+ 1. (First goal)
140
+ 2. (Second goal)
141
+ 3. (Third goal)
142
+
143
+ # Constraints
144
+
145
+ <!-- Hard rules. What the AI must never do, regardless of context. -->
146
+ <!-- Example:
147
+ - Never introduce new dependencies without explicit confirmation
148
+ - Never use \`eval\`, \`Function()\`, or dynamic \`require()\`
149
+ -->
150
+
151
+ - (Add your hard constraints here)
152
+ `.trimStart()
153
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * pluribus sync — read pluribus.md and generate tool-specific output files.
3
+ * Supports --dry-run, --tools, --source flags.
4
+ */
5
+
6
+ import * as fs from 'fs'
7
+ import * as path from 'path'
8
+ import { parsePluribusFile, validateSections, REQUIRED_SECTIONS } from '../utils/parser.js'
9
+ import { resolveImportsAsync } from '../utils/imports.js'
10
+ import { renderTemplate, parseSkillFile } from '../utils/renderer.js'
11
+ import { BUILT_IN_SKILLS, SUPPORTED_TOOLS } from '../skills/built-in.js'
12
+
13
+ /**
14
+ * @param {Record<string, string | boolean>} args
15
+ */
16
+ export async function runSync(args) {
17
+ const isDryRun = Boolean(args['dry-run'])
18
+ const sourceArg = typeof args.source === 'string' ? args.source : null
19
+ const toolsArg = typeof args.tools === 'string' ? args.tools : null
20
+ const updateImports = Boolean(args['update-imports'])
21
+
22
+ const cwd = process.cwd()
23
+
24
+ // Resolve source file
25
+ const sourcePath = sourceArg
26
+ ? path.resolve(cwd, sourceArg)
27
+ : path.join(cwd, 'pluribus.md')
28
+
29
+ if (!fs.existsSync(sourcePath)) {
30
+ console.error(`āŒ pluribus.md not found at: ${sourcePath}`)
31
+ console.error(` Run \`pluribus init\` to create one.`)
32
+ process.exit(1)
33
+ }
34
+
35
+ // Read and parse pluribus.md
36
+ let rawContent
37
+ try {
38
+ rawContent = fs.readFileSync(sourcePath, 'utf8')
39
+ } catch (err) {
40
+ console.error(`āŒ Could not read ${sourcePath}: ${err.message}`)
41
+ process.exit(1)
42
+ }
43
+
44
+ let resolvedContent
45
+ try {
46
+ const projectDir = path.dirname(sourcePath)
47
+ const resolved = await resolveImportsAsync(sourcePath, {
48
+ rootDir: projectDir,
49
+ allowRemote: updateImports,
50
+ lockfilePath: path.join(projectDir, 'pluribus.lock.json'),
51
+ cacheDir: path.join(projectDir, '.pluribus', 'cache', 'remote'),
52
+ updateLockfile: updateImports,
53
+ })
54
+ resolvedContent = resolved.content
55
+ } catch (err) {
56
+ console.error(`āŒ Could not resolve imports for ${sourcePath}: ${err.message}`)
57
+ process.exit(1)
58
+ }
59
+
60
+ const sections = parsePluribusFile(resolvedContent)
61
+
62
+ // Validate required sections
63
+ const { valid, errors } = validateSections(sections)
64
+ if (!valid) {
65
+ console.error('āŒ pluribus.md has validation errors:')
66
+ for (const e of errors) {
67
+ console.error(` • ${e}`)
68
+ }
69
+ console.error(`\n Complete all required sections (${REQUIRED_SECTIONS.join(', ')}) and re-run.`)
70
+ process.exit(1)
71
+ }
72
+
73
+ // Determine which tools to sync
74
+ let toolsToSync
75
+
76
+ if (toolsArg) {
77
+ // Explicit override via --tools flag
78
+ toolsToSync = toolsArg
79
+ .split(',')
80
+ .map((t) => t.trim().toLowerCase())
81
+ .filter((t) => t.length > 0)
82
+ } else {
83
+ // Read from pluribus.md comment: <!-- pluribus:tools: claude,cursor,openclaw -->
84
+ const toolsCommentMatch = rawContent.match(/<!--\s*pluribus:tools:\s*([^-]+)\s*-->/)
85
+ if (toolsCommentMatch) {
86
+ toolsToSync = toolsCommentMatch[1]
87
+ .split(',')
88
+ .map((t) => t.trim().toLowerCase())
89
+ .filter((t) => t.length > 0)
90
+ } else {
91
+ // Default: all supported tools
92
+ toolsToSync = [...SUPPORTED_TOOLS]
93
+ }
94
+ }
95
+
96
+ const unknownTools = toolsToSync.filter((t) => !SUPPORTED_TOOLS.includes(t))
97
+ if (unknownTools.length > 0) {
98
+ console.warn(`āš ļø Unknown tools will be skipped: ${unknownTools.join(', ')}`)
99
+ console.warn(` Supported: ${SUPPORTED_TOOLS.join(', ')}`)
100
+ toolsToSync = toolsToSync.filter((t) => SUPPORTED_TOOLS.includes(t))
101
+ }
102
+
103
+ if (toolsToSync.length === 0) {
104
+ console.error('āŒ No valid tools to sync.')
105
+ process.exit(1)
106
+ }
107
+
108
+ console.log(`šŸ”„ Syncing pluribus.md → ${toolsToSync.join(', ')}${isDryRun ? ' (dry run)' : ''}`)
109
+ console.log('')
110
+
111
+ let successCount = 0
112
+ let failCount = 0
113
+
114
+ for (const toolId of toolsToSync) {
115
+ // Check for local skill override first: pluribus/skills/<tool>.md
116
+ const localSkillPath = path.join(cwd, 'pluribus', 'skills', `${toolId}.md`)
117
+ let skill
118
+
119
+ if (fs.existsSync(localSkillPath)) {
120
+ console.log(` šŸ“‚ Using local skill override: ${localSkillPath}`)
121
+ try {
122
+ const skillContent = fs.readFileSync(localSkillPath, 'utf8')
123
+ const parsed = parseSkillFile(skillContent)
124
+ skill = {
125
+ id: toolId,
126
+ outputFiles: parsed.output,
127
+ template: parsed.template,
128
+ required: parsed.sections.required,
129
+ optional: parsed.sections.optional,
130
+ }
131
+ } catch (err) {
132
+ console.error(` āŒ [${toolId}] Failed to parse local skill: ${err.message}`)
133
+ failCount++
134
+ continue
135
+ }
136
+ } else {
137
+ skill = BUILT_IN_SKILLS[toolId]
138
+ }
139
+
140
+ if (!skill) {
141
+ console.error(` āŒ [${toolId}] Skill not found.`)
142
+ failCount++
143
+ continue
144
+ }
145
+
146
+ // Check required sections for this skill
147
+ const missingRequired = skill.required.filter((s) => {
148
+ const sectionName = Object.keys(sections).find(
149
+ (k) => k.toLowerCase() === s.toLowerCase()
150
+ )
151
+ return !sectionName || !sections[sectionName]?.trim()
152
+ })
153
+
154
+ if (missingRequired.length > 0) {
155
+ console.warn(` āš ļø [${toolId}] Skipping — missing required sections: ${missingRequired.join(', ')}`)
156
+ failCount++
157
+ continue
158
+ }
159
+
160
+ // Render the template
161
+ let rendered
162
+ try {
163
+ rendered = renderTemplate(skill.template, sections, path.relative(cwd, sourcePath) || 'pluribus.md')
164
+ } catch (err) {
165
+ console.error(` āŒ [${toolId}] Template rendering failed: ${err.message}`)
166
+ failCount++
167
+ continue
168
+ }
169
+
170
+ // Write output files
171
+ for (const outputFile of skill.outputFiles) {
172
+ const outputPath = path.join(cwd, outputFile)
173
+
174
+ if (isDryRun) {
175
+ console.log(` šŸ“ [dry-run] Would write: ${outputFile}`)
176
+ console.log(' ' + '─'.repeat(60))
177
+ const preview = rendered.split('\n').slice(0, 20).join('\n ')
178
+ console.log(' ' + preview)
179
+ if (rendered.split('\n').length > 20) {
180
+ console.log(` ... (${rendered.split('\n').length - 20} more lines)`)
181
+ }
182
+ console.log(' ' + '─'.repeat(60))
183
+ console.log('')
184
+ } else {
185
+ try {
186
+ // Ensure parent directory exists
187
+ const outputDir = path.dirname(outputPath)
188
+ if (!fs.existsSync(outputDir)) {
189
+ fs.mkdirSync(outputDir, { recursive: true })
190
+ }
191
+ fs.writeFileSync(outputPath, rendered, 'utf8')
192
+ console.log(` āœ… [${toolId}] → ${outputFile}`)
193
+ successCount++
194
+ } catch (err) {
195
+ console.error(` āŒ [${toolId}] Failed to write ${outputFile}: ${err.message}`)
196
+ failCount++
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ console.log('')
203
+
204
+ if (isDryRun) {
205
+ console.log(`šŸ“ Dry run complete — no files were written.`)
206
+ } else {
207
+ if (failCount === 0) {
208
+ console.log(`āœ… Sync complete — ${successCount} file(s) written.`)
209
+ } else {
210
+ console.log(`āœ… Sync complete — ${successCount} file(s) written, ${failCount} skipped/failed.`)
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * pluribus validate — check pluribus.md before sync.
3
+ *
4
+ * Validation intentionally mirrors sync's source/import behavior, but it does
5
+ * not render or write tool output. Remote imports are only refreshed when
6
+ * --update-imports is passed; otherwise locked remote imports must resolve from
7
+ * the local lock/cache path.
8
+ */
9
+
10
+ import * as fs from 'fs'
11
+ import * as path from 'path'
12
+ import { parsePluribusFile, validateSections, REQUIRED_SECTIONS } from '../utils/parser.js'
13
+ import { resolveImportsAsync } from '../utils/imports.js'
14
+ import { SUPPORTED_TOOLS } from '../skills/built-in.js'
15
+
16
+ const TOOLS_COMMENT_RE = /<!--\s*pluribus:tools:\s*([^-]+)\s*-->/
17
+
18
+ /**
19
+ * @param {Record<string, string | boolean>} args
20
+ */
21
+ export async function runValidate(args) {
22
+ const sourceArg = typeof args.source === 'string' ? args.source : null
23
+ const updateImports = Boolean(args['update-imports'])
24
+ const cwd = process.cwd()
25
+ const sourcePath = sourceArg
26
+ ? path.resolve(cwd, sourceArg)
27
+ : path.join(cwd, 'pluribus.md')
28
+
29
+ const errors = []
30
+ const warnings = []
31
+
32
+ if (!fs.existsSync(sourcePath)) {
33
+ console.error(`āŒ pluribus.md not found at: ${sourcePath}`)
34
+ console.error(' Run `pluribus init` to create one.')
35
+ process.exit(1)
36
+ }
37
+
38
+ let rawContent
39
+ try {
40
+ rawContent = fs.readFileSync(sourcePath, 'utf8')
41
+ } catch (err) {
42
+ console.error(`āŒ Could not read ${sourcePath}: ${err.message}`)
43
+ process.exit(1)
44
+ }
45
+
46
+ console.log(`āœ“ Found ${path.relative(cwd, sourcePath) || 'pluribus.md'}`)
47
+
48
+ const duplicateSections = findDuplicateSections(rawContent)
49
+ for (const duplicate of duplicateSections) {
50
+ errors.push(`Duplicate section: # ${duplicate.name} (lines ${duplicate.firstLine} and ${duplicate.line})`)
51
+ }
52
+
53
+ const rawSections = parsePluribusFile(rawContent)
54
+ if (Object.keys(rawSections).length === 0) {
55
+ errors.push('No top-level sections found. Add required # Identity, # Stack, # Conventions, # Goals, and # Constraints sections.')
56
+ } else {
57
+ console.log(`āœ“ Detected ${Object.keys(rawSections).length} top-level section(s) before imports`)
58
+ }
59
+
60
+ const tools = parseToolsComment(rawContent)
61
+ if (tools.length > 0) {
62
+ const unknownTools = tools.filter((tool) => !SUPPORTED_TOOLS.includes(tool))
63
+ if (unknownTools.length > 0) {
64
+ errors.push(`Unknown tool(s) in pluribus:tools comment: ${unknownTools.join(', ')}. Supported: ${SUPPORTED_TOOLS.join(', ')}`)
65
+ } else {
66
+ console.log(`āœ“ Tools comment is valid: ${tools.join(', ')}`)
67
+ }
68
+ }
69
+
70
+ let resolvedContent
71
+ let importCount = 0
72
+ try {
73
+ const projectDir = path.dirname(sourcePath)
74
+ const resolved = await resolveImportsAsync(sourcePath, {
75
+ rootDir: projectDir,
76
+ allowRemote: updateImports,
77
+ lockfilePath: path.join(projectDir, 'pluribus.lock.json'),
78
+ cacheDir: path.join(projectDir, '.pluribus', 'cache', 'remote'),
79
+ updateLockfile: updateImports,
80
+ })
81
+ resolvedContent = resolved.content
82
+ importCount = resolved.imports.length
83
+ console.log(`āœ“ Imports resolved (${importCount})${updateImports ? ' with refresh enabled' : ''}`)
84
+ } catch (err) {
85
+ errors.push(`Could not resolve imports: ${err.message}`)
86
+ }
87
+
88
+ if (resolvedContent) {
89
+ const sections = parsePluribusFile(resolvedContent)
90
+ const validation = validateSections(sections)
91
+ for (const error of validation.errors) {
92
+ errors.push(error)
93
+ }
94
+
95
+ const missingRecommended = REQUIRED_SECTIONS.filter((section) => !sections[section]?.trim())
96
+ if (missingRecommended.length === 0) {
97
+ console.log(`āœ“ Required sections are present: ${REQUIRED_SECTIONS.join(', ')}`)
98
+ }
99
+ }
100
+
101
+ for (const warning of warnings) {
102
+ console.warn(`āš ļø ${warning}`)
103
+ }
104
+
105
+ if (errors.length > 0) {
106
+ console.error('')
107
+ for (const error of errors) {
108
+ console.error(`āœ— ${error}`)
109
+ }
110
+ console.error(`\nFound ${errors.length} error(s). Fix before syncing.`)
111
+ process.exit(1)
112
+ }
113
+
114
+ console.log('')
115
+ console.log('āœ… pluribus.md is valid. Ready to sync.')
116
+ }
117
+
118
+ function findDuplicateSections(content) {
119
+ const cleaned = content.replace(/^\uFEFF/, '')
120
+ const seen = new Map()
121
+ const duplicates = []
122
+ const lines = cleaned.split(/\r?\n/)
123
+
124
+ lines.forEach((line, idx) => {
125
+ if (!line.startsWith('# ') || line.startsWith('## ')) return
126
+ const name = line.slice(2).trim()
127
+ const key = name.toLowerCase()
128
+ const lineNumber = idx + 1
129
+ if (seen.has(key)) {
130
+ duplicates.push({ name, firstLine: seen.get(key), line: lineNumber })
131
+ } else {
132
+ seen.set(key, lineNumber)
133
+ }
134
+ })
135
+
136
+ return duplicates
137
+ }
138
+
139
+ function parseToolsComment(content) {
140
+ const match = content.match(TOOLS_COMMENT_RE)
141
+ if (!match) return []
142
+ return match[1]
143
+ .split(',')
144
+ .map((tool) => tool.trim().toLowerCase())
145
+ .filter(Boolean)
146
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * pluribus watch — monitor pluribus.md and re-run sync after edits.
3
+ *
4
+ * This intentionally uses Node's built-in fs.watch instead of an external
5
+ * dependency. The watcher is narrow by design: it watches the selected source
6
+ * file and debounces rapid editor save events before delegating to sync.
7
+ */
8
+
9
+ import * as fs from 'fs'
10
+ import * as path from 'path'
11
+ import { runSync } from './sync.js'
12
+
13
+ const DEFAULT_DEBOUNCE_MS = 400
14
+
15
+ /**
16
+ * @param {Record<string, string | boolean>} args
17
+ */
18
+ export async function runWatch(args) {
19
+ const sourceArg = typeof args.source === 'string' ? args.source : null
20
+ const debounceArg = typeof args.debounce === 'string' ? Number(args.debounce) : null
21
+ const debounceMs = Number.isFinite(debounceArg) && debounceArg >= 300
22
+ ? debounceArg
23
+ : DEFAULT_DEBOUNCE_MS
24
+ const once = Boolean(args.once)
25
+ const cwd = process.cwd()
26
+ const sourcePath = sourceArg
27
+ ? path.resolve(cwd, sourceArg)
28
+ : path.join(cwd, 'pluribus.md')
29
+ const sourceDir = path.dirname(sourcePath)
30
+ const sourceFile = path.basename(sourcePath)
31
+ const displayPath = path.relative(cwd, sourcePath) || sourceFile
32
+
33
+ if (!fs.existsSync(sourcePath)) {
34
+ console.error(`āŒ pluribus.md not found at: ${sourcePath}`)
35
+ console.error(' Run `pluribus init` to create one.')
36
+ process.exit(1)
37
+ }
38
+
39
+ let debounceTimer = null
40
+ let running = false
41
+ let pending = false
42
+ let stopped = false
43
+
44
+ const syncArgs = { ...args }
45
+ delete syncArgs.once
46
+ delete syncArgs.debounce
47
+
48
+ const watcher = fs.watch(sourceDir, { persistent: true }, (_eventType, filename) => {
49
+ if (stopped) return
50
+ if (filename && filename.toString() !== sourceFile) return
51
+ scheduleSync()
52
+ })
53
+
54
+ console.log(`šŸ‘€ Watching ${displayPath} for changes...`)
55
+ console.log(` Debounce: ${debounceMs}ms${once ? ' | once mode enabled' : ''}`)
56
+ console.log(' Press Ctrl+C to stop.')
57
+ console.log('')
58
+
59
+ watcher.on('error', (err) => {
60
+ console.error(`āŒ Watcher failed: ${err.message}`)
61
+ process.exit(1)
62
+ })
63
+
64
+ process.once('SIGINT', () => {
65
+ stopped = true
66
+ if (debounceTimer) clearTimeout(debounceTimer)
67
+ watcher.close()
68
+ console.log('\nšŸ‘‹ Stopped watching.')
69
+ process.exit(0)
70
+ })
71
+
72
+ function scheduleSync() {
73
+ if (debounceTimer) clearTimeout(debounceTimer)
74
+ debounceTimer = setTimeout(() => {
75
+ debounceTimer = null
76
+ void runDebouncedSync()
77
+ }, debounceMs)
78
+ }
79
+
80
+ async function runDebouncedSync() {
81
+ if (running) {
82
+ pending = true
83
+ return
84
+ }
85
+
86
+ running = true
87
+ const timestamp = new Date().toLocaleTimeString('en-GB', { hour12: false })
88
+ console.log(`[${timestamp}] Change detected, syncing...`)
89
+
90
+ try {
91
+ await runSync(syncArgs)
92
+ const doneAt = new Date().toLocaleTimeString('en-GB', { hour12: false })
93
+ console.log(`[${doneAt}] Done.`)
94
+ } catch (err) {
95
+ console.error(`āŒ Sync failed: ${err.message || err}`)
96
+ } finally {
97
+ running = false
98
+ }
99
+
100
+ if (once) {
101
+ stopped = true
102
+ watcher.close()
103
+ process.exit(0)
104
+ }
105
+
106
+ if (pending) {
107
+ pending = false
108
+ scheduleSync()
109
+ }
110
+ }
111
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Pluribus — public API surface (for future programmatic use)
3
+ */
4
+
5
+ export { runInit } from './commands/init.js'
6
+ export { runSync } from './commands/sync.js'
7
+ export { runValidate } from './commands/validate.js'
8
+ export { runWatch } from './commands/watch.js'
9
+ export { parsePluribusFile, validateSections } from './utils/parser.js'
10
+ export { renderTemplate } from './utils/renderer.js'
11
+ export { BUILT_IN_SKILLS, SUPPORTED_TOOLS } from './skills/built-in.js'