code-foundry 0.26.0 → 0.27.1

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.
Files changed (44) hide show
  1. package/.githooks/pre-commit +3 -4
  2. package/.github/CONTRIBUTING.md +10 -13
  3. package/.github/actions/setup/action.yml +68 -3
  4. package/.github/code-foundry.yml +6 -1
  5. package/.github/workflows/ci.yml +29 -24
  6. package/.github/workflows/ci_self-ci.yml +1 -1
  7. package/.github/workflows/codeql.yml +23 -29
  8. package/.github/workflows/codeql_self-ci.yml +1 -1
  9. package/.github/workflows/draft-pr.yml +2 -2
  10. package/.github/workflows/release-pr.yml +4 -4
  11. package/.github/workflows/release.yml +17 -29
  12. package/.github/workflows/security.yml +24 -19
  13. package/.github/workflows/security_self-ci.yml +1 -1
  14. package/.github/workflows/test.yml +20 -20
  15. package/.github/workflows/test_self-ci.yml +1 -1
  16. package/AGENTS.md +13 -10
  17. package/CHANGELOG.md +29 -0
  18. package/README.md +16 -6
  19. package/docs/CONFIGURATION.md +10 -1
  20. package/docs/INITIALIZATION.md +6 -5
  21. package/docs/RELEASES.md +5 -0
  22. package/docs/WORKFLOWS.md +12 -5
  23. package/package.json +7 -3
  24. package/src/cli.mjs +29 -25
  25. package/src/commands/doctor.mjs +70 -0
  26. package/src/commands/sync.mjs +224 -0
  27. package/src/lib/config.mjs +38 -0
  28. package/src/lib/profile.mjs +114 -0
  29. package/src/runtime.mjs +321 -0
  30. package/.github/scripts/bootstrap.sh +0 -21
  31. package/.github/scripts/changed-files.sh +0 -96
  32. package/.github/scripts/ci.sh +0 -641
  33. package/.github/scripts/codeql-languages.sh +0 -91
  34. package/.github/scripts/doctor.sh +0 -98
  35. package/.github/scripts/format-fast-path.sh +0 -80
  36. package/.github/scripts/init-repo.sh +0 -268
  37. package/.github/scripts/pre-commit.sh +0 -67
  38. package/.github/scripts/profile.sh +0 -186
  39. package/.github/scripts/security.sh +0 -214
  40. package/.github/scripts/sitecustomize.py +0 -17
  41. package/.github/scripts/sync-codeowners.sh +0 -76
  42. package/.github/scripts/sync-protection.sh +0 -138
  43. package/.github/scripts/sync-template.sh +0 -748
  44. package/.github/scripts/turbo-cache-probe.sh +0 -102
@@ -0,0 +1,70 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+ import { join, resolve } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { includesValue, readConfig } from '../lib/config.mjs'
7
+ import { resolveProfile } from '../lib/profile.mjs'
8
+
9
+ /** @param {string} root */
10
+ export function doctor(root) {
11
+ const target = resolve(root)
12
+ const config = readConfig(join(target, '.github/code-foundry.yml'))
13
+ const profile = resolveProfile(target)
14
+ let errors = 0
15
+ /** @param {string} message */
16
+ const error = (message) => { console.error(`ERROR: ${message}`); errors += 1 }
17
+ /** @param {string} message */
18
+ const warn = (message) => console.warn(`WARN: ${message}`)
19
+
20
+ const toolchain = config.toolchain ?? 'auto'
21
+ if (!['auto', 'native', 'mise'].includes(toolchain)) error(`unsupported toolchain: ${toolchain}`)
22
+ if (toolchain === 'mise' && !existsSync(join(target, '.mise.toml'))) error('.mise.toml is required when toolchain: mise')
23
+ if (toolchain === 'auto') {
24
+ console.log(existsSync(join(target, '.mise.toml'))
25
+ ? 'Toolchain: mise (detected from existing .mise.toml).'
26
+ : 'Toolchain: native (mise not configured).')
27
+ }
28
+ const hooks = git(target, ['config', '--get', 'core.hooksPath'])
29
+ if (hooks !== '.githooks') warn('Git hooks are not enabled; run `npx code-foundry init`')
30
+
31
+ const packageFile = join(target, 'package.json')
32
+ if (existsSync(packageFile)) {
33
+ let packageJson
34
+ try { packageJson = JSON.parse(readFileSync(packageFile, 'utf8')) }
35
+ catch { error('package.json is not valid JSON'); packageJson = {} }
36
+ const lockfiles = ['bun.lock', 'bun.lockb', 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'].filter((file) => existsSync(join(target, file)))
37
+ const groups = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
38
+ if (!lockfiles.length && groups.some((key) => Object.keys(packageJson[key] ?? {}).length)) error('package.json exists but no supported lockfile was found')
39
+ if (lockfiles.length > 1) error('multiple JavaScript lockfiles found; keep one package manager')
40
+ if (packageJson.packageManager && lockfiles.length) {
41
+ const declared = String(packageJson.packageManager).split('@')[0]
42
+ const actual = lockfiles[0]?.startsWith('bun') ? 'bun' : lockfiles[0]?.split('-')[0].replace('.yaml', '')
43
+ if (actual && declared !== actual) error(`packageManager (${declared}) does not match ${actual} lockfile`)
44
+ }
45
+ }
46
+
47
+ if (profile.languages.split(',').includes('rust') && !commandExists('cargo')) error('Cargo is required for this repository')
48
+ if (profile.languages.split(',').includes('rust')) {
49
+ const metadata = spawnSync('cargo', ['metadata', '--no-deps', '--format-version', '1'], { cwd: target, stdio: 'ignore' })
50
+ if (metadata.status !== 0) error('cargo metadata failed')
51
+ }
52
+ if (profile.languages.split(',').includes('python') && !commandExists('python') && !existsSync(join(target, '.venv/bin/python'))) error('Python is required for this repository')
53
+
54
+ for (const workflow of ['ci', 'codeql', 'security', 'test', 'draft-pr', 'release-pr', 'release']) {
55
+ if (includesValue(config.features ?? 'all', workflow) && !existsSync(join(target, `.github/workflows/${workflow}.yml`))) error(`missing enabled workflow: ${workflow}.yml`)
56
+ }
57
+ console.log('Remote CI, Test, Security, CodeQL, and release runtimes are loaded by reusable workflow wrappers.')
58
+ if (errors) throw new Error(`Repository doctor found ${errors} error(s).`)
59
+ console.log('Repository doctor passed.')
60
+ }
61
+
62
+ /** @param {string} root @param {string[]} args */
63
+ function git(root, args) {
64
+ return spawnSync('git', args, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? ''
65
+ }
66
+
67
+ /** @param {string} command */
68
+ function commandExists(command) {
69
+ return spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0
70
+ }
@@ -0,0 +1,224 @@
1
+ // @ts-check
2
+
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { dirname, join, resolve } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { detectLanguages, detectPackageManager, detectProfile } from '../lib/profile.mjs'
7
+ import { configured, includesValue, readConfig } from '../lib/config.mjs'
8
+
9
+ const standardFiles = [
10
+ '.editorconfig', '.gitattributes', '.gitignore', 'release-please-config.json',
11
+ '.githooks/pre-commit', 'AGENTS.md', 'LICENSE', 'NOTICE', 'ruff.toml', '.prettierrc',
12
+ '.github/CODEOWNERS', '.github/CODE_OF_CONDUCT.md', '.github/CONTRIBUTING.md',
13
+ '.github/PULL_REQUEST_TEMPLATE.md', '.github/SECURITY.md', '.github/dependabot.yml',
14
+ '.github/ISSUE_TEMPLATE/bug_report.yml', '.github/ISSUE_TEMPLATE/config.yml',
15
+ '.github/ISSUE_TEMPLATE/feature_request.yml',
16
+ '.github/workflows/ci.yml', '.github/workflows/codeql.yml', '.github/workflows/draft-pr.yml',
17
+ '.github/workflows/release-pr.yml', '.github/workflows/release.yml',
18
+ '.github/workflows/security.yml', '.github/workflows/test.yml',
19
+ ]
20
+
21
+ const protectedFiles = new Set([
22
+ 'AGENTS.md', '.github/CODE_OF_CONDUCT.md', '.github/CONTRIBUTING.md',
23
+ '.github/PULL_REQUEST_TEMPLATE.md', '.github/SECURITY.md', 'NOTICE',
24
+ ])
25
+
26
+ const legacyFiles = [
27
+ '.github/code-foundry.yml.example', '.github/template.yml', '.github/template.yml.example',
28
+ '.github/scripts/bootstrap.sh', '.github/scripts/changed-files.sh', '.github/scripts/ci.sh',
29
+ '.github/scripts/codeql-languages.sh', '.github/scripts/doctor.sh', '.github/scripts/format-fast-path.sh',
30
+ '.github/scripts/init-repo.sh', '.github/scripts/pre-commit.sh', '.github/scripts/profile.sh',
31
+ '.github/scripts/security.sh', '.github/scripts/sitecustomize.py', '.github/scripts/sync-codeowners.sh',
32
+ '.github/scripts/sync-protection.sh', '.github/scripts/sync-template.sh', '.github/scripts/turbo-cache-probe.sh',
33
+ '.github/licenses/MIT.txt',
34
+ '.github/licenses/GPL-3.0-or-later.txt', '.github/licenses/AGPL-3.0-or-later.txt',
35
+ ]
36
+
37
+ /** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean }} SyncOptions */
38
+
39
+ /** @param {SyncOptions} options */
40
+ export function syncRepository(options) {
41
+ const target = resolve(options.target)
42
+ const source = resolve(options.source)
43
+ const dryRun = options.dryRun ?? false
44
+ const force = options.force ?? false
45
+ const configPath = join(target, '.github/code-foundry.yml')
46
+ const existingConfig = readConfig(configPath)
47
+ if (!Object.keys(existingConfig).length && !options.init) throw new Error('Missing .github/code-foundry.yml; run init first.')
48
+ const defaults = createDefaultConfig(target, source)
49
+ let config = { ...defaults, ...existingConfig }
50
+ if (!Object.keys(existingConfig).length) {
51
+ writeOrReport(configPath, renderConfig(config), dryRun)
52
+ } else {
53
+ const missing = Object.keys(defaults).filter((key) => !(key in existingConfig))
54
+ if (missing.length) {
55
+ const current = readFileSync(configPath, 'utf8').trimEnd()
56
+ const additions = missing.map((key) => `${key}: ${defaults[key]}`).join('\n')
57
+ writeOrReport(configPath, `${current}\n${additions}\n`, dryRun)
58
+ }
59
+ }
60
+
61
+ const languages = configured(config.languages, detectLanguages(target).join(','))
62
+ const features = configured(config.features, 'all')
63
+ const runtimeRepository = configured(config.runtime_repository, '0xPlayerOne/code-foundry')
64
+ const runtimeRef = configured(config.runtime_ref, `v${readPackageVersion(source)}`)
65
+ const toolchain = configured(config.toolchain, 'auto')
66
+ if (!['auto', 'native', 'mise'].includes(toolchain)) {
67
+ throw new Error(`Unsupported toolchain: ${toolchain}; use auto, native, or mise.`)
68
+ }
69
+ const license = configured(config.license, existsSync(join(target, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later')
70
+ const changed = []
71
+
72
+ for (const file of standardFiles) {
73
+ if (!shouldInclude(file, languages, features)) continue
74
+ const sourceFile = sourcePath(source, file)
75
+ if (!existsSync(sourceFile)) throw new Error(`Template file missing: ${file}`)
76
+ const destination = join(target, file)
77
+ if (!force && protectedFiles.has(file) && existsSync(destination)) {
78
+ const existing = readFileSync(destination, 'utf8')
79
+ if (!isLegacyManagedDoc(file, existing)) continue
80
+ }
81
+ if ((file === 'LICENSE' || file === 'NOTICE') && license === 'preserve' && existsSync(destination)) continue
82
+ if ((file === 'LICENSE' || file === 'NOTICE') && license === 'none') continue
83
+ if (file === '.github/CODEOWNERS' && existsSync(destination)) continue
84
+ let content = readFileSync(sourceFile)
85
+ if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
86
+ content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef))
87
+ }
88
+ if (file === '.gitignore' && existsSync(destination)) {
89
+ content = Buffer.from(mergeGitignore(content.toString('utf8'), readFileSync(destination, 'utf8')))
90
+ }
91
+ if (!existsSync(destination) || !buffersEqual(content, readFileSync(destination))) {
92
+ changed.push(file)
93
+ writeOrReport(destination, content, dryRun)
94
+ }
95
+ }
96
+
97
+ if (license !== 'preserve' && license !== 'none') {
98
+ const licenseFile = license === 'mit' ? 'MIT.txt' : license === 'agpl-3.0-or-later' ? 'AGPL-3.0-or-later.txt' : 'GPL-3.0-or-later.txt'
99
+ const sourceLicense = join(source, '.github/licenses', licenseFile)
100
+ if (!existsSync(sourceLicense)) throw new Error(`License template missing: ${sourceLicense}`)
101
+ writeOrReport(join(target, 'LICENSE'), readFileSync(sourceLicense), dryRun)
102
+ if (!existsSync(join(target, 'NOTICE'))) writeOrReport(join(target, 'NOTICE'), readFileSync(join(source, 'NOTICE')), dryRun)
103
+ }
104
+
105
+ for (const file of legacyFiles) {
106
+ const destination = join(target, file)
107
+ if (existsSync(destination)) {
108
+ changed.push(file)
109
+ if (dryRun) console.log(`Would remove ${file}`)
110
+ else rmSync(destination, { force: true })
111
+ }
112
+ }
113
+ for (const file of ['ruff.toml', '.prettierrc', '.prettierignore']) {
114
+ const relevant = file === 'ruff.toml' ? includesValue(languages, 'python') : includesValue(languages, 'typescript')
115
+ if (!relevant && existsSync(join(target, file))) {
116
+ changed.push(file)
117
+ if (dryRun) console.log(`Would remove irrelevant language configuration ${file}`)
118
+ else rmSync(join(target, file), { force: true })
119
+ }
120
+ }
121
+ if (!dryRun && existsSync(join(target, '.githooks/pre-commit'))) {
122
+ chmodSync(join(target, '.githooks/pre-commit'), 0o755)
123
+ git(target, ['config', 'core.hooksPath', '.githooks'])
124
+ }
125
+ console.log(`${changed.length} baseline file(s) differ.`)
126
+ return { changed, config }
127
+ }
128
+
129
+ /** @param {string} file @param {string} languages @param {string} features */
130
+ function shouldInclude(file, languages, features) {
131
+ if (file === 'ruff.toml') return includesValue(languages, 'python')
132
+ if (file === '.prettierrc') return includesValue(languages, 'typescript')
133
+ if (file === '.github/dependabot.yml') return includesValue(features, 'dependabot')
134
+ const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
135
+ return !workflow || includesValue(features, workflow)
136
+ }
137
+
138
+ /** @param {string} source @param {string} file */
139
+ function sourcePath(source, file) {
140
+ if (file.startsWith('.github/workflows/')) {
141
+ const name = file.slice('.github/workflows/'.length, -4)
142
+ return join(source, '.github/workflows', `${name}_self-ci.yml`)
143
+ }
144
+ return join(source, file)
145
+ }
146
+
147
+ /** @param {string} content @param {Record<string,string>} config @param {string} repository @param {string} ref */
148
+ function renderWorkflow(content, config, repository, ref) {
149
+ const localPrefix = 'uses: ./.github/workflows/'
150
+ const remotePrefix = `uses: ${repository}/.github/workflows/`
151
+ let rendered = content.replaceAll(localPrefix, remotePrefix)
152
+ rendered = rendered.replace(new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'), `$&@${ref}`)
153
+ /** @type {Record<string, string|undefined>} */
154
+ const runners = {
155
+ ci: config.ci_runner ?? config.runner,
156
+ test: config.test_runner ?? config.runner,
157
+ security: config.security_runner ?? config.runner,
158
+ codeql: config.codeql_runner ?? config.runner,
159
+ 'draft-pr': config.pr_runner ?? config.runner,
160
+ 'release-pr': config.pr_runner ?? config.runner,
161
+ release: config.release_runner ?? config.runner,
162
+ }
163
+ const workflow = content.match(/\.github\/workflows\/([^/]+)\.yml/)?.[1]
164
+ const runner = workflow ? runners[workflow] : undefined
165
+ if (runner) rendered = rendered.replace(/^(\s+runner:)\s+.*$/m, `$1 ${runner}`)
166
+ return rendered
167
+ }
168
+
169
+ /** @param {string} value */
170
+ function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') }
171
+
172
+ /** @param {string} baseline @param {string} existing */
173
+ function mergeGitignore(baseline, existing) {
174
+ const marker = '# Repository-specific rules'
175
+ const custom = existing.includes(marker) ? existing.slice(existing.indexOf(marker) + marker.length).trim() : ''
176
+ return custom ? `${baseline.trimEnd()}\n\n${marker}\n${custom}\n` : baseline
177
+ }
178
+
179
+ /** @param {string} file @param {string} content */
180
+ function isLegacyManagedDoc(file, content) {
181
+ if (file === 'AGENTS.md') {
182
+ return content.includes('.github/scripts/bootstrap.sh') && content.includes('bash .github/scripts/ci.sh')
183
+ }
184
+ if (file === '.github/CONTRIBUTING.md') {
185
+ return content.includes('.github/scripts/bootstrap.sh') && content.includes('.github/template.yml')
186
+ }
187
+ return false
188
+ }
189
+
190
+ /** @param {string} target @param {string[]} args */
191
+ function git(target, args) { spawnSync('git', args, { cwd: target, stdio: 'ignore' }) }
192
+
193
+ /** @param {string} file @param {Buffer|string} content @param {boolean} dryRun */
194
+ function writeOrReport(file, content, dryRun) {
195
+ if (dryRun) { console.log(`Would sync ${file}`); return }
196
+ mkdirSync(dirname(file), { recursive: true })
197
+ writeFileSync(file, content)
198
+ }
199
+
200
+ /** @param {Buffer} a @param {Buffer} b */
201
+ function buffersEqual(a, b) { return a.equals(b) }
202
+
203
+ /** @param {string} root @param {string} source @returns {Record<string,string>} */
204
+ function createDefaultConfig(root, source) {
205
+ const languages = detectLanguages(root).join(',')
206
+ const packageManager = detectPackageManager(root)
207
+ return {
208
+ version: '1', profile: detectProfile(root), languages, features: 'all', codeql: 'auto', dependency_review: 'auto', package_manager: packageManager,
209
+ runtime_repository: '0xPlayerOne/code-foundry', runtime_ref: `v${readPackageVersion(source)}`,
210
+ runner: 'ubuntu-latest', unit_runner: 'ubuntu-slim', ci_runner: 'ubuntu-latest', test_runner: 'ubuntu-latest',
211
+ toolchain: 'auto',
212
+ security_runner: 'ubuntu-slim', codeql_runner: 'ubuntu-latest', pr_runner: 'ubuntu-slim', release_runner: 'ubuntu-slim',
213
+ prune_standard: 'false', cache_packages: 'auto', cache_build: 'auto', coverage_minimum: '80', turbo_remote: 'auto',
214
+ release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node', npm_publish: 'false',
215
+ license: existsSync(join(root, 'LICENSE')) ? 'preserve' : 'gpl-3.0-or-later', git_workflow: 'staging-release', merge_strategy: 'rebase',
216
+ }
217
+ }
218
+
219
+ /** @param {Record<string,string>} config */
220
+ function renderConfig(config) { return `${Object.entries(config).map(([key, value]) => `${key}: ${value}`).join('\n')}\n` }
221
+ /** @param {string} root */
222
+ function readPackageVersion(root) {
223
+ try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version ?? '0.0.0' } catch { return '0.0.0' }
224
+ }
@@ -0,0 +1,38 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+
5
+ /**
6
+ * Read the intentionally small YAML subset used by code-foundry.yml.
7
+ * Values are scalar strings; this keeps the CLI dependency-free.
8
+ * @param {string} file
9
+ * @returns {Record<string, string>}
10
+ */
11
+ export function readConfig(file) {
12
+ if (!existsSync(file)) return {}
13
+
14
+ /** @type {Record<string, string>} */
15
+ const config = {}
16
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
17
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
18
+ if (!match) continue
19
+ const value = match[2].replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '')
20
+ config[match[1]] = value
21
+ }
22
+ return config
23
+ }
24
+
25
+ /** @param {string} value @param {string} fallback */
26
+ export function configured(value, fallback) {
27
+ return value === undefined || value === '' ? fallback : value
28
+ }
29
+
30
+ /** @param {string} value @returns {string[]} */
31
+ export function listValue(value) {
32
+ return value.split(',').map((item) => item.trim()).filter(Boolean)
33
+ }
34
+
35
+ /** @param {string} value @param {string} item */
36
+ export function includesValue(value, item) {
37
+ return value === 'all' || listValue(value).includes(item)
38
+ }
@@ -0,0 +1,114 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { configured, includesValue, readConfig } from './config.mjs'
6
+
7
+ const ignoredDirectories = new Set([
8
+ '.git',
9
+ '.code-foundry',
10
+ '.kilo',
11
+ '.mise',
12
+ '.next',
13
+ '.nuxt',
14
+ '.venv',
15
+ 'node_modules',
16
+ 'target',
17
+ 'vendor',
18
+ ])
19
+
20
+ /** @param {string} root @returns {string[]} */
21
+ function sourceFiles(root) {
22
+ /** @type {string[]} */
23
+ const files = []
24
+ /** @param {string} directory */
25
+ function visit(directory) {
26
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
27
+ if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue
28
+ const path = join(directory, entry.name)
29
+ if (entry.isDirectory()) visit(path)
30
+ else files.push(path)
31
+ }
32
+ }
33
+ visit(root)
34
+ return files
35
+ }
36
+
37
+ /** @param {string} root @param {RegExp} pattern */
38
+ function hasSource(root, pattern) {
39
+ return sourceFiles(root).some((file) => pattern.test(file))
40
+ }
41
+
42
+ /** @param {string} root */
43
+ export function detectLanguages(root) {
44
+ const files = sourceFiles(root)
45
+ /** @type {string[]} */
46
+ const languages = []
47
+ if (
48
+ existsSync(join(root, 'package.json')) ||
49
+ files.some((file) => /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/.test(file)) ||
50
+ files.some((file) => /(?:bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$/.test(file))
51
+ ) languages.push('typescript')
52
+ if (existsSync(join(root, 'Cargo.toml')) || hasSource(root, /\.rs$/)) languages.push('rust')
53
+ if (
54
+ existsSync(join(root, 'pyproject.toml')) ||
55
+ existsSync(join(root, 'requirements.txt')) ||
56
+ existsSync(join(root, 'requirements-dev.txt')) ||
57
+ hasSource(root, /\.py$/)
58
+ ) languages.push('python')
59
+ if (files.some((file) => file.endsWith('.sol'))) languages.push('solidity')
60
+ return languages.length ? languages : ['none']
61
+ }
62
+
63
+ /** @param {string} root */
64
+ export function detectPackageManager(root) {
65
+ if (existsSync(join(root, 'bun.lock')) || existsSync(join(root, 'bun.lockb'))) return 'bun'
66
+ if (existsSync(join(root, 'pnpm-lock.yaml'))) return 'pnpm'
67
+ if (existsSync(join(root, 'yarn.lock'))) return 'yarn'
68
+ if (existsSync(join(root, 'package-lock.json'))) return 'npm'
69
+ if (existsSync(join(root, 'package.json'))) return 'bun'
70
+ return 'none'
71
+ }
72
+
73
+ /** @param {string} root */
74
+ export function detectProfile(root) {
75
+ if (existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'pnpm-workspace.yaml'))) return 'monorepo'
76
+ if (existsSync(join(root, 'package.json'))) {
77
+ try {
78
+ const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
79
+ if (Array.isArray(packageJson.workspaces) || typeof packageJson.workspaces === 'object') return 'monorepo'
80
+ } catch {
81
+ // Doctor reports malformed package.json separately.
82
+ }
83
+ }
84
+ if (existsSync(join(root, 'package.json')) || existsSync(join(root, 'pyproject.toml')) || existsSync(join(root, 'Cargo.toml'))) return 'application'
85
+ return 'minimal'
86
+ }
87
+
88
+ /** @param {string} root */
89
+ export function resolveProfile(root) {
90
+ const config = readConfig(join(root, '.github/code-foundry.yml'))
91
+ const languages = configured(config.languages, 'auto')
92
+ const profile = configured(config.profile, 'auto')
93
+ const packageManager = configured(config.package_manager, 'auto')
94
+ return {
95
+ profile: profile === 'auto' ? detectProfile(root) : profile,
96
+ languages: languages === 'auto' ? detectLanguages(root).join(',') : languages,
97
+ package_manager: packageManager === 'auto' ? detectPackageManager(root) : packageManager,
98
+ features: configured(config.features, 'all'),
99
+ release_type: configured(config.release_type, detectReleaseType(root)),
100
+ npm_publish: configured(config.npm_publish, 'false'),
101
+ runner: configured(config.runner, 'ubuntu-latest'),
102
+ }
103
+ }
104
+
105
+ /** @param {string} root */
106
+ function detectReleaseType(root) {
107
+ if (existsSync(join(root, 'package.json'))) return 'node'
108
+ if (existsSync(join(root, 'pyproject.toml'))) return 'python'
109
+ if (existsSync(join(root, 'Cargo.toml'))) return 'rust'
110
+ if (existsSync(join(root, 'version.txt'))) return 'simple'
111
+ return 'none'
112
+ }
113
+
114
+ export { includesValue }