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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/pluribus.js +108 -0
- package/docs/composable-contexts.md +124 -0
- package/docs/openclaw-integration.md +145 -0
- package/docs/release-checklist.md +91 -0
- package/docs/remote-composable-context-imports.md +233 -0
- package/examples/claude-cowork/.cursorrules +82 -0
- package/examples/claude-cowork/pluribus.md +137 -0
- package/examples/composable-contexts/pluribus.md +29 -0
- package/examples/composable-contexts/shared/security-constraints.md +6 -0
- package/examples/composable-contexts/shared/team-context.md +10 -0
- package/examples/openclaw/AGENTS.md +134 -0
- package/examples/openclaw/CLAUDE.md +132 -0
- package/examples/openclaw/pluribus.md +99 -0
- package/package.json +52 -0
- package/spec/context-format.md +356 -0
- package/spec/skills-format.md +325 -0
- package/src/commands/init.js +153 -0
- package/src/commands/sync.js +213 -0
- package/src/commands/validate.js +146 -0
- package/src/commands/watch.js +111 -0
- package/src/index.js +11 -0
- package/src/skills/built-in.js +345 -0
- package/src/utils/args.js +35 -0
- package/src/utils/imports.js +690 -0
- package/src/utils/parser.js +74 -0
- package/src/utils/renderer.js +123 -0
- package/src/utils/version.js +1 -0
|
@@ -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'
|