ace-pack 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/LICENSE +21 -0
- package/README.md +242 -0
- package/ace-pack.cmd +2 -0
- package/agent-memory-pack.cmd +2 -0
- package/install-ace-pack.cmd +2 -0
- package/install-ace-pack.mjs +261 -0
- package/install-agent-memory-pack.cmd +2 -0
- package/install-agent-memory-pack.mjs +17 -0
- package/logo.svg +25 -0
- package/package.json +49 -0
- package/scripts/ace-hub.mjs +137 -0
- package/scripts/ace-onboard.mjs +503 -0
- package/scripts/ace-project-presets.mjs +158 -0
- package/scripts/ace-universal-doc-templates.mjs +40 -0
- package/scripts/agent-memory-lib.mjs +141 -0
- package/scripts/agent-memory-templates.mjs +350 -0
- package/scripts/ai-memory-config.mjs +171 -0
- package/scripts/ai-memory-utils.mjs +372 -0
- package/scripts/ai-report-brief.mjs +131 -0
- package/scripts/ai-report-current-task-code.mjs +128 -0
- package/scripts/ai-report.mjs +185 -0
- package/scripts/ai-task-classify.mjs +236 -0
- package/scripts/ai-task-finish.mjs +206 -0
- package/scripts/ai-update.mjs +236 -0
- package/scripts/bootstrap-agent-memory.mjs +23 -0
- package/scripts/check-agent-memory.mjs +22 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
4
|
+
import readline from 'node:readline'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
export const GENERATED_CONTEXT_PATH = '.ai/generated-context.md'
|
|
8
|
+
|
|
9
|
+
export const HUB_MENU = `[ACE] Agentic Context Engine - Knowledge Hub
|
|
10
|
+
Select the context payload you want to generate:
|
|
11
|
+
|
|
12
|
+
[1] AI Coder Context (For Cursor/VSCode agent: Task, changed files, handoff, reflection)
|
|
13
|
+
[2] AI Architect Context (For Browser AI: Strict rules, tech docs, decisions, roadmap. HIGH DENSITY, LOW TOKEN)
|
|
14
|
+
[3] Business Report (For Humans: Roadmap and recent work log)
|
|
15
|
+
[4] Developer Docs (For Onboarding: Tech docs and devops/setup)
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
const CONTEXT_PAYLOADS = {
|
|
19
|
+
1: [
|
|
20
|
+
requiredFile('.ai/current-task.md'),
|
|
21
|
+
requiredFile('.ai/session-handoff.md'),
|
|
22
|
+
requiredFile('.ai/changed-files.md'),
|
|
23
|
+
requiredFile('.ai/reflection-log.md'),
|
|
24
|
+
],
|
|
25
|
+
2: [
|
|
26
|
+
requiredFile('AGENTS.md'),
|
|
27
|
+
requiredFile('.ai/tech-docs.md'),
|
|
28
|
+
requiredFile('.ai/decisions.md'),
|
|
29
|
+
requiredFile('.ai/product-roadmap.md'),
|
|
30
|
+
],
|
|
31
|
+
3: [requiredFile('.ai/product-roadmap.md'), requiredFile('.ai/work-log.md')],
|
|
32
|
+
4: [requiredFile('.ai/tech-docs.md'), optionalFile('DEVOPS.md')],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function generateContextPayload(rootDir, selection) {
|
|
36
|
+
const files = getPayloadFiles(selection)
|
|
37
|
+
const sections = []
|
|
38
|
+
const includedFiles = []
|
|
39
|
+
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const content = await readContextFile(rootDir, file)
|
|
42
|
+
|
|
43
|
+
if (content === null) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
includedFiles.push(file.path)
|
|
48
|
+
sections.push(formatContextSection(file.path, content))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const outputPath = path.join(rootDir, GENERATED_CONTEXT_PATH)
|
|
52
|
+
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
53
|
+
await writeFile(outputPath, `${sections.join('\n\n')}\n`, 'utf8')
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
includedFiles,
|
|
57
|
+
outputPath,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPayloadFiles(selection) {
|
|
62
|
+
const payload = CONTEXT_PAYLOADS[selection.trim()]
|
|
63
|
+
|
|
64
|
+
if (!payload) {
|
|
65
|
+
throw new Error(`Invalid ACE hub option: ${selection}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return payload
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readContextFile(rootDir, file) {
|
|
72
|
+
const filePath = path.join(rootDir, file.path)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return await readFile(filePath, 'utf8')
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
78
|
+
if (file.required) {
|
|
79
|
+
throw new Error(`Missing required context file: ${file.path}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatContextSection(filePath, content) {
|
|
90
|
+
return `# --- FILE: ${filePath} ---\n\n${content.trimEnd()}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function requiredFile(filePath) {
|
|
94
|
+
return {
|
|
95
|
+
path: filePath,
|
|
96
|
+
required: true,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function optionalFile(filePath) {
|
|
101
|
+
return {
|
|
102
|
+
path: filePath,
|
|
103
|
+
required: false,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function promptSelection() {
|
|
108
|
+
const rl = readline.createInterface({ input, output })
|
|
109
|
+
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
rl.question('Enter option: ', (answer) => {
|
|
112
|
+
rl.close()
|
|
113
|
+
resolve(answer)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
process.stdout.write(`${HUB_MENU}\n`)
|
|
120
|
+
|
|
121
|
+
const selection = process.argv[2] ?? (await promptSelection())
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await generateContextPayload(process.cwd(), selection)
|
|
125
|
+
process.stdout.write(
|
|
126
|
+
"Context generated and saved to .ai/generated-context.md. Copy this file's content to your AI.\n",
|
|
127
|
+
)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
130
|
+
process.stderr.write(`${message}\n`)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
136
|
+
await main()
|
|
137
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
UNIVERSAL_HIGH_RISK_KEYWORDS,
|
|
7
|
+
UNIVERSAL_HIGH_RISK_PATHS,
|
|
8
|
+
buildMemoryConfig,
|
|
9
|
+
buildPresetMemoryConfig,
|
|
10
|
+
dedupeKeywordRules,
|
|
11
|
+
dedupePathRules,
|
|
12
|
+
getProjectPreset,
|
|
13
|
+
} from './ace-project-presets.mjs'
|
|
14
|
+
import {
|
|
15
|
+
getArgValue,
|
|
16
|
+
nowTimestamp,
|
|
17
|
+
parseCliArgs,
|
|
18
|
+
readTextIfExists,
|
|
19
|
+
writeAceBanner,
|
|
20
|
+
} from './ai-memory-utils.mjs'
|
|
21
|
+
|
|
22
|
+
export const PROJECT_PROFILE_PATH = '.ai/project-profile.md'
|
|
23
|
+
export const RECOMMENDED_CONFIG_PATH = '.ai/memory-config.recommended.json'
|
|
24
|
+
|
|
25
|
+
const MAX_SCANNED_FILES = 5000
|
|
26
|
+
const MAX_SCAN_DEPTH = 8
|
|
27
|
+
const SKIPPED_DIRS = new Set([
|
|
28
|
+
'.git',
|
|
29
|
+
'.next',
|
|
30
|
+
'.turbo',
|
|
31
|
+
'build',
|
|
32
|
+
'coverage',
|
|
33
|
+
'dist',
|
|
34
|
+
'node_modules',
|
|
35
|
+
'out',
|
|
36
|
+
'target',
|
|
37
|
+
'vendor',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const DETECTION_RULES = [
|
|
41
|
+
{
|
|
42
|
+
ecosystem: 'Next.js / TypeScript',
|
|
43
|
+
paths: [
|
|
44
|
+
{ label: 'auth package', pattern: 'packages/auth/**', tier: 'large' },
|
|
45
|
+
{ label: 'Next.js app router', pattern: 'app/**', tier: 'standard' },
|
|
46
|
+
{ label: 'Next.js app router', pattern: 'apps/*/src/app/**', tier: 'standard' },
|
|
47
|
+
{ label: 'Next.js middleware', pattern: 'middleware.ts', tier: 'large' },
|
|
48
|
+
{ label: 'Next.js middleware', pattern: '**/middleware.ts', tier: 'large' },
|
|
49
|
+
],
|
|
50
|
+
signals: ['next.config.ts', 'next.config.js'],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
ecosystem: 'tRPC / API routers',
|
|
54
|
+
paths: [
|
|
55
|
+
{ label: 'tRPC router', pattern: 'packages/api/src/routers/**', tier: 'large' },
|
|
56
|
+
{ label: 'tRPC router', pattern: 'src/server/api/**', tier: 'large' },
|
|
57
|
+
{ label: 'API routes', pattern: '**/api/**', tier: 'standard' },
|
|
58
|
+
],
|
|
59
|
+
signals: ['@trpc/server'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
ecosystem: 'Drizzle / database',
|
|
63
|
+
paths: [
|
|
64
|
+
{ label: 'database schema', pattern: 'packages/db/src/schema/**', tier: 'large' },
|
|
65
|
+
{ label: 'database migration', pattern: 'packages/db/drizzle/**', tier: 'large' },
|
|
66
|
+
{ label: 'database migration', pattern: 'packages/db/migrations/**', tier: 'large' },
|
|
67
|
+
{ label: 'database schema', pattern: 'src/db/**', tier: 'large' },
|
|
68
|
+
],
|
|
69
|
+
signals: ['drizzle-orm', 'drizzle-kit'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ecosystem: 'Python / FastAPI',
|
|
73
|
+
paths: [
|
|
74
|
+
{ label: 'Python security module', pattern: 'app/core/security.py', tier: 'large' },
|
|
75
|
+
{ label: 'Python auth module', pattern: 'app/**/auth*.py', tier: 'large' },
|
|
76
|
+
{ label: 'FastAPI routers', pattern: 'app/api/**', tier: 'standard' },
|
|
77
|
+
{ label: 'Python database migration', pattern: 'alembic/**', tier: 'large' },
|
|
78
|
+
],
|
|
79
|
+
signals: ['requirements.txt', 'pyproject.toml', 'fastapi'],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
ecosystem: 'Go service',
|
|
83
|
+
paths: [
|
|
84
|
+
{ label: 'Go auth package', pattern: 'internal/auth/**', tier: 'large' },
|
|
85
|
+
{ label: 'Go middleware', pattern: 'internal/middleware/**', tier: 'large' },
|
|
86
|
+
{ label: 'Go API handlers', pattern: 'internal/handlers/**', tier: 'standard' },
|
|
87
|
+
{ label: 'Go database migration', pattern: 'migrations/**', tier: 'large' },
|
|
88
|
+
],
|
|
89
|
+
signals: ['go.mod'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
ecosystem: '.NET service',
|
|
93
|
+
paths: [
|
|
94
|
+
{ label: '.NET auth module', pattern: '**/Auth/**', tier: 'large' },
|
|
95
|
+
{ label: '.NET middleware', pattern: '**/Middleware/**', tier: 'large' },
|
|
96
|
+
{ label: '.NET migration', pattern: '**/Migrations/**', tier: 'large' },
|
|
97
|
+
],
|
|
98
|
+
signals: ['.csproj', '.sln'],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
export async function onboardRepository(rootDir, options = {}) {
|
|
103
|
+
if (options.check) {
|
|
104
|
+
return checkOnboarding(rootDir)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const timestamp = nowTimestamp()
|
|
108
|
+
const profile = await profileRepository(rootDir)
|
|
109
|
+
const recommendedConfig = options.preset
|
|
110
|
+
? buildPresetMemoryConfig(options.preset, {
|
|
111
|
+
generatedAt: timestamp,
|
|
112
|
+
source: 'ace:onboard',
|
|
113
|
+
status: options.apply ? 'profiled' : 'recommended',
|
|
114
|
+
})
|
|
115
|
+
: buildRecommendedMemoryConfig(profile, timestamp)
|
|
116
|
+
|
|
117
|
+
const profileContent = formatProjectProfile(profile, recommendedConfig, {
|
|
118
|
+
preset: options.preset,
|
|
119
|
+
timestamp,
|
|
120
|
+
})
|
|
121
|
+
const writtenFiles = await writeOnboardingFiles(rootDir, profileContent, recommendedConfig)
|
|
122
|
+
|
|
123
|
+
if (options.apply) {
|
|
124
|
+
const appliedConfig = options.preset
|
|
125
|
+
? recommendedConfig
|
|
126
|
+
: await mergeRecommendedConfig(rootDir, recommendedConfig, timestamp)
|
|
127
|
+
|
|
128
|
+
await writeJsonFile(path.join(rootDir, '.ai', 'memory-config.json'), appliedConfig)
|
|
129
|
+
writtenFiles.push('.ai/memory-config.json')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
applied: Boolean(options.apply),
|
|
134
|
+
detectedEcosystems: profile.ecosystems,
|
|
135
|
+
recommendedConfig,
|
|
136
|
+
recommendedRuleCount:
|
|
137
|
+
recommendedConfig.highRiskPaths.length + recommendedConfig.highRiskKeywords.length,
|
|
138
|
+
status: 'ok',
|
|
139
|
+
writtenFiles,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function profileRepository(rootDir) {
|
|
144
|
+
const files = await scanRepoFiles(rootDir)
|
|
145
|
+
const fileSet = new Set(files)
|
|
146
|
+
const packageMetadata = await readPackageMetadata(rootDir)
|
|
147
|
+
const contentSignals = await readContentSignals(rootDir, files)
|
|
148
|
+
const signals = new Set([
|
|
149
|
+
...files.map((file) => path.basename(file)),
|
|
150
|
+
...packageMetadata.packageSignals,
|
|
151
|
+
...contentSignals,
|
|
152
|
+
])
|
|
153
|
+
const ecosystems = []
|
|
154
|
+
const recommendedPaths = [...UNIVERSAL_HIGH_RISK_PATHS]
|
|
155
|
+
const recommendedKeywords = [...UNIVERSAL_HIGH_RISK_KEYWORDS]
|
|
156
|
+
|
|
157
|
+
for (const rule of DETECTION_RULES) {
|
|
158
|
+
if (!rule.signals.some((signal) => signalMatches(signal, signals))) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ecosystems.push(rule.ecosystem)
|
|
163
|
+
|
|
164
|
+
for (const pathRule of rule.paths) {
|
|
165
|
+
if (files.some((file) => pathRuleMatchesFile(pathRule.pattern, file, fileSet))) {
|
|
166
|
+
recommendedPaths.push(pathRule)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
ecosystems: ecosystems.length > 0 ? ecosystems : ['Generic repository'],
|
|
173
|
+
filesScanned: files.length,
|
|
174
|
+
packageManager: detectPackageManager(fileSet),
|
|
175
|
+
recommendedKeywords: dedupeKeywordRules(recommendedKeywords),
|
|
176
|
+
recommendedPaths: dedupePathRules(recommendedPaths),
|
|
177
|
+
signals: [...signals].sort(),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildRecommendedMemoryConfig(profile, timestamp = nowTimestamp()) {
|
|
182
|
+
return buildMemoryConfig({
|
|
183
|
+
highRiskKeywords: profile.recommendedKeywords,
|
|
184
|
+
highRiskPaths: profile.recommendedPaths,
|
|
185
|
+
profile: {
|
|
186
|
+
detectedEcosystems: profile.ecosystems,
|
|
187
|
+
generatedAt: timestamp,
|
|
188
|
+
source: 'ace:onboard',
|
|
189
|
+
status: 'recommended',
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function checkOnboarding(rootDir) {
|
|
195
|
+
const configContent = await readTextIfExists(path.join(rootDir, '.ai', 'memory-config.json'))
|
|
196
|
+
const profileContent = await readTextIfExists(path.join(rootDir, PROJECT_PROFILE_PATH))
|
|
197
|
+
const issues = []
|
|
198
|
+
|
|
199
|
+
if (configContent === null) {
|
|
200
|
+
issues.push('Missing .ai/memory-config.json.')
|
|
201
|
+
} else {
|
|
202
|
+
const config = JSON.parse(configContent)
|
|
203
|
+
|
|
204
|
+
if (config?._profile?.status !== 'profiled') {
|
|
205
|
+
issues.push('ACE project profile is not applied. Run pnpm ace:onboard -- --apply.')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (profileContent === null) {
|
|
210
|
+
issues.push(`Missing ${PROJECT_PROFILE_PATH}. Run pnpm ace:onboard.`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (issues.length > 0) {
|
|
214
|
+
return { issues, status: 'failed' }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { issues: [], status: 'ok' }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function mergeRecommendedConfig(rootDir, recommendedConfig, timestamp) {
|
|
221
|
+
const existingContent = await readTextIfExists(path.join(rootDir, '.ai', 'memory-config.json'))
|
|
222
|
+
const existingConfig = existingContent ? JSON.parse(existingContent) : {}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
...existingConfig,
|
|
226
|
+
_name: 'ACE (Agentic Context Engine) Configuration',
|
|
227
|
+
_profile: {
|
|
228
|
+
detectedEcosystems: recommendedConfig._profile.detectedEcosystems,
|
|
229
|
+
profiledAt: timestamp,
|
|
230
|
+
source: 'ace:onboard',
|
|
231
|
+
status: 'profiled',
|
|
232
|
+
},
|
|
233
|
+
highRiskKeywords: dedupeKeywordRules([
|
|
234
|
+
...(existingConfig.highRiskKeywords ?? []),
|
|
235
|
+
...recommendedConfig.highRiskKeywords,
|
|
236
|
+
]),
|
|
237
|
+
highRiskPaths: dedupePathRules([
|
|
238
|
+
...(existingConfig.highRiskPaths ?? []),
|
|
239
|
+
...recommendedConfig.highRiskPaths,
|
|
240
|
+
]),
|
|
241
|
+
thresholds: existingConfig.thresholds ?? recommendedConfig.thresholds,
|
|
242
|
+
version: existingConfig.version ?? recommendedConfig.version,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function writeOnboardingFiles(rootDir, profileContent, recommendedConfig) {
|
|
247
|
+
const writtenFiles = []
|
|
248
|
+
const profilePath = path.join(rootDir, PROJECT_PROFILE_PATH)
|
|
249
|
+
const recommendedPath = path.join(rootDir, RECOMMENDED_CONFIG_PATH)
|
|
250
|
+
|
|
251
|
+
await writeTextFile(profilePath, profileContent)
|
|
252
|
+
writtenFiles.push(PROJECT_PROFILE_PATH)
|
|
253
|
+
await writeJsonFile(recommendedPath, recommendedConfig)
|
|
254
|
+
writtenFiles.push(RECOMMENDED_CONFIG_PATH)
|
|
255
|
+
|
|
256
|
+
return writtenFiles
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function formatProjectProfile(profile, recommendedConfig, { preset, timestamp }) {
|
|
260
|
+
const ecosystemLines = profile.ecosystems.map((ecosystem) => `- ${ecosystem}`).join('\n')
|
|
261
|
+
const pathLines = recommendedConfig.highRiskPaths
|
|
262
|
+
.slice(0, 20)
|
|
263
|
+
.map((rule) => `- \`${rule.pattern}\` - ${rule.label} (${rule.tier})`)
|
|
264
|
+
.join('\n')
|
|
265
|
+
const keywordLines = recommendedConfig.highRiskKeywords
|
|
266
|
+
.slice(0, 20)
|
|
267
|
+
.map((rule) => `- \`${rule.keyword}\` - ${rule.label} (${rule.tier})`)
|
|
268
|
+
.join('\n')
|
|
269
|
+
const modeLine = preset
|
|
270
|
+
? `Applied recommendation source: preset \`${preset}\`.`
|
|
271
|
+
: 'Applied recommendation source: repository scan.'
|
|
272
|
+
|
|
273
|
+
return `# ACE Project Profile
|
|
274
|
+
|
|
275
|
+
Generated: ${timestamp}
|
|
276
|
+
|
|
277
|
+
${modeLine}
|
|
278
|
+
|
|
279
|
+
## Detected Ecosystems
|
|
280
|
+
${ecosystemLines}
|
|
281
|
+
|
|
282
|
+
## Repository Shape
|
|
283
|
+
- Files scanned: ${profile.filesScanned}
|
|
284
|
+
- Package manager: ${profile.packageManager}
|
|
285
|
+
|
|
286
|
+
## Recommended High-Risk Paths
|
|
287
|
+
${pathLines}
|
|
288
|
+
|
|
289
|
+
## Recommended High-Risk Keywords
|
|
290
|
+
${keywordLines}
|
|
291
|
+
|
|
292
|
+
## Next Step
|
|
293
|
+
- Review \`.ai/memory-config.recommended.json\`.
|
|
294
|
+
- Run \`pnpm ace:onboard -- --apply\` to apply the recommended profile.
|
|
295
|
+
- For known Next/tRPC/Drizzle SaaS repos, run \`pnpm ace:onboard -- --preset next-trpc-drizzle-saas --apply\`.
|
|
296
|
+
`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function scanRepoFiles(rootDir) {
|
|
300
|
+
const files = []
|
|
301
|
+
|
|
302
|
+
async function visit(directory, depth) {
|
|
303
|
+
if (files.length >= MAX_SCANNED_FILES || depth > MAX_SCAN_DEPTH) {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let entries
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
entries = await readdir(directory, { withFileTypes: true })
|
|
311
|
+
} catch {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
if (files.length >= MAX_SCANNED_FILES) {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const absolutePath = path.join(directory, entry.name)
|
|
321
|
+
const relativePath = normalizeRepoPath(path.relative(rootDir, absolutePath))
|
|
322
|
+
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
if (!SKIPPED_DIRS.has(entry.name)) {
|
|
325
|
+
await visit(absolutePath, depth + 1)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (entry.isFile()) {
|
|
332
|
+
files.push(relativePath)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await visit(rootDir, 0)
|
|
338
|
+
|
|
339
|
+
return files.sort()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function readPackageMetadata(rootDir) {
|
|
343
|
+
const packageSignals = []
|
|
344
|
+
const packageContent = await readTextIfExists(path.join(rootDir, 'package.json'))
|
|
345
|
+
|
|
346
|
+
if (!packageContent) {
|
|
347
|
+
return { packageSignals }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const packageJson = JSON.parse(stripByteOrderMark(packageContent))
|
|
352
|
+
const dependencies = {
|
|
353
|
+
...(packageJson.dependencies ?? {}),
|
|
354
|
+
...(packageJson.devDependencies ?? {}),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
packageSignals.push(...Object.keys(dependencies))
|
|
358
|
+
} catch {
|
|
359
|
+
return { packageSignals }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { packageSignals }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function readContentSignals(rootDir, files) {
|
|
366
|
+
const signals = []
|
|
367
|
+
|
|
368
|
+
for (const file of ['requirements.txt', 'pyproject.toml', 'go.mod']) {
|
|
369
|
+
if (!files.includes(file)) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const content = await readTextIfExists(path.join(rootDir, file))
|
|
374
|
+
|
|
375
|
+
if (content?.toLowerCase().includes('fastapi')) {
|
|
376
|
+
signals.push('fastapi')
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return signals
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function detectPackageManager(fileSet) {
|
|
384
|
+
if (fileSet.has('pnpm-lock.yaml')) {
|
|
385
|
+
return 'pnpm'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (fileSet.has('yarn.lock')) {
|
|
389
|
+
return 'yarn'
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (fileSet.has('package-lock.json')) {
|
|
393
|
+
return 'npm'
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (fileSet.has('uv.lock')) {
|
|
397
|
+
return 'uv'
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (fileSet.has('poetry.lock')) {
|
|
401
|
+
return 'poetry'
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (fileSet.has('go.mod')) {
|
|
405
|
+
return 'go'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return 'not detected'
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function signalMatches(signal, signals) {
|
|
412
|
+
if (signal.startsWith('.')) {
|
|
413
|
+
return [...signals].some((value) => value.endsWith(signal))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return signals.has(signal)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function pathRuleMatchesFile(pattern, file, fileSet) {
|
|
420
|
+
if (pattern.includes('*')) {
|
|
421
|
+
return globToRegExp(pattern).test(file)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return file === pattern || fileSet.has(pattern)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function globToRegExp(pattern) {
|
|
428
|
+
const escapedPattern = normalizeRepoPath(pattern)
|
|
429
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
430
|
+
.replace(/\*\*/g, '__DOUBLE_STAR__')
|
|
431
|
+
.replace(/\*/g, '[^/]*')
|
|
432
|
+
.replace(/__DOUBLE_STAR__/g, '.*')
|
|
433
|
+
|
|
434
|
+
return new RegExp(`^${escapedPattern}$`, 'u')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeRepoPath(filePath) {
|
|
438
|
+
return filePath.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function stripByteOrderMark(content) {
|
|
442
|
+
return content.replace(/^\uFEFF/u, '')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function writeTextFile(filePath, content) {
|
|
446
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
447
|
+
await writeFile(filePath, normalizeTrailingNewline(content), 'utf8')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function writeJsonFile(filePath, value) {
|
|
451
|
+
await writeTextFile(filePath, JSON.stringify(value, null, 2))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeTrailingNewline(content) {
|
|
455
|
+
return content.endsWith('\n') ? content : `${content}\n`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function main() {
|
|
459
|
+
writeAceBanner()
|
|
460
|
+
|
|
461
|
+
const args = parseCliArgs(process.argv.slice(2))
|
|
462
|
+
const rootDir = path.resolve(process.cwd(), getArgValue(args, 'root') ?? '.')
|
|
463
|
+
const preset = getArgValue(args, 'preset')
|
|
464
|
+
|
|
465
|
+
if (preset && !getProjectPreset(preset)) {
|
|
466
|
+
throw new Error(`Unknown ACE project preset: ${preset}`)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const result = await onboardRepository(rootDir, {
|
|
470
|
+
apply: getArgValue(args, 'apply') === 'true',
|
|
471
|
+
check: getArgValue(args, 'check') === 'true',
|
|
472
|
+
preset,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
if (getArgValue(args, 'json') === 'true') {
|
|
476
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (result.status === 'failed') {
|
|
481
|
+
process.stderr.write('ACE onboarding check failed:\n')
|
|
482
|
+
|
|
483
|
+
for (const issue of result.issues) {
|
|
484
|
+
process.stderr.write(`- ${issue}\n`)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
process.exit(1)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
process.stdout.write(
|
|
491
|
+
result.applied
|
|
492
|
+
? `ACE project profile applied. Updated: ${result.writtenFiles.join(', ')}\n`
|
|
493
|
+
: `ACE project profile generated. Review ${RECOMMENDED_CONFIG_PATH}, then run pnpm ace:onboard -- --apply.\n`,
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
498
|
+
await main().catch((error) => {
|
|
499
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
500
|
+
process.stderr.write(`${message}\n`)
|
|
501
|
+
process.exit(1)
|
|
502
|
+
})
|
|
503
|
+
}
|