code-foundry 0.25.0 → 0.27.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.
Files changed (47) hide show
  1. package/.githooks/pre-commit +3 -4
  2. package/.github/CONTRIBUTING.md +10 -13
  3. package/.github/actions/setup/action.yml +50 -3
  4. package/.github/code-foundry.yml +6 -1
  5. package/.github/licenses/AGPL-3.0-or-later.txt +661 -0
  6. package/.github/licenses/GPL-3.0-or-later.txt +674 -0
  7. package/.github/licenses/MIT.txt +4 -16
  8. package/.github/workflows/ci.yml +20 -23
  9. package/.github/workflows/ci_self-ci.yml +1 -1
  10. package/.github/workflows/codeql.yml +23 -29
  11. package/.github/workflows/codeql_self-ci.yml +1 -1
  12. package/.github/workflows/draft-pr.yml +2 -2
  13. package/.github/workflows/release-pr.yml +4 -4
  14. package/.github/workflows/release.yml +17 -29
  15. package/.github/workflows/security.yml +24 -19
  16. package/.github/workflows/security_self-ci.yml +1 -1
  17. package/.github/workflows/test.yml +20 -20
  18. package/.github/workflows/test_self-ci.yml +1 -1
  19. package/AGENTS.md +13 -10
  20. package/CHANGELOG.md +28 -0
  21. package/README.md +19 -8
  22. package/docs/CONFIGURATION.md +11 -2
  23. package/docs/INITIALIZATION.md +9 -5
  24. package/docs/RELEASES.md +5 -0
  25. package/docs/WORKFLOWS.md +12 -5
  26. package/package.json +7 -3
  27. package/src/cli.mjs +29 -25
  28. package/src/commands/doctor.mjs +70 -0
  29. package/src/commands/sync.mjs +224 -0
  30. package/src/lib/config.mjs +38 -0
  31. package/src/lib/profile.mjs +114 -0
  32. package/src/runtime.mjs +316 -0
  33. package/.github/scripts/bootstrap.sh +0 -21
  34. package/.github/scripts/changed-files.sh +0 -96
  35. package/.github/scripts/ci.sh +0 -641
  36. package/.github/scripts/codeql-languages.sh +0 -91
  37. package/.github/scripts/doctor.sh +0 -98
  38. package/.github/scripts/format-fast-path.sh +0 -80
  39. package/.github/scripts/init-repo.sh +0 -264
  40. package/.github/scripts/pre-commit.sh +0 -67
  41. package/.github/scripts/profile.sh +0 -186
  42. package/.github/scripts/security.sh +0 -214
  43. package/.github/scripts/sitecustomize.py +0 -17
  44. package/.github/scripts/sync-codeowners.sh +0 -76
  45. package/.github/scripts/sync-protection.sh +0 -138
  46. package/.github/scripts/sync-template.sh +0 -744
  47. package/.github/scripts/turbo-cache-probe.sh +0 -102
@@ -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 }