geeknative 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/bin/geeknative.js +12 -0
- package/package.json +42 -4
- package/src/cli.js +61 -0
- package/src/codegen/app.js +105 -0
- package/src/codegen/icons.js +87 -0
- package/src/codegen/index.js +44 -0
- package/src/codegen/strings.js +308 -0
- package/src/codegen/tokens.js +449 -0
- package/src/codegen/util.js +122 -0
- package/src/commands/check.js +148 -0
- package/src/commands/create.js +82 -0
- package/src/commands/doctor.js +108 -0
- package/src/commands/gen.js +44 -0
- package/src/commands/parity.js +134 -0
- package/src/commands/run.js +91 -0
- package/src/lib/exec.js +77 -0
- package/src/lib/fsutil.js +92 -0
- package/src/lib/log.js +37 -0
- package/src/lib/managed.js +80 -0
- package/src/lib/project.js +52 -0
- package/src/lib/toolchains.js +126 -0
- package/template/.claude/agents/android-engineer.md +15 -0
- package/template/.claude/agents/ios-engineer.md +15 -0
- package/template/.claude/agents/web-engineer.md +15 -0
- package/template/.claude/settings.json +65 -0
- package/template/.geeknative/framework.mk +41 -0
- package/template/.geeknative/hooks/after-edit.mjs +28 -0
- package/template/.geeknative/hooks/parity-guard.mjs +49 -0
- package/template/.geeknative/hooks/subagent-context.mjs +24 -0
- package/template/.geeknative/playbooks/bug.md +7 -0
- package/template/.geeknative/playbooks/design.md +10 -0
- package/template/.geeknative/playbooks/engineering.md +9 -0
- package/template/.geeknative/playbooks/feature.md +11 -0
- package/template/.geeknative/playbooks/platform-specific.md +8 -0
- package/template/.geeknative/playbooks/product.md +12 -0
- package/template/.geeknative/templates/feature.md +49 -0
- package/template/.gemini/settings.json +5 -0
- package/template/.github/dependabot.yml +18 -0
- package/template/AGENTS.md +97 -0
- package/template/CLAUDE.md +9 -0
- package/template/DESIGN.md +65 -0
- package/template/Makefile +6 -0
- package/template/PRODUCT.md +40 -0
- package/template/README.md +28 -0
- package/template/_gitignore +33 -0
- package/template/apps/android/AGENTS.md +66 -0
- package/template/apps/android/CLAUDE.md +1 -0
- package/template/apps/android/app/build.gradle.kts +68 -0
- package/template/apps/android/app/identity.properties +4 -0
- package/template/apps/android/app/src/main/AndroidManifest.xml +24 -0
- package/template/apps/android/app/src/main/kotlin/app/App.kt +15 -0
- package/template/apps/android/app/src/main/kotlin/app/MainActivity.kt +49 -0
- package/template/apps/android/app/src/main/kotlin/app/core/AppContainer.kt +27 -0
- package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppCard.kt +30 -0
- package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppTheme.kt +35 -0
- package/template/apps/android/app/src/main/kotlin/app/core/navigation/AppNavigation.kt +40 -0
- package/template/apps/android/app/src/main/kotlin/app/core/navigation/Route.kt +13 -0
- package/template/apps/android/app/src/main/kotlin/app/features/home/HomeScreen.kt +67 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/Appearance.kt +22 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsRepository.kt +40 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsScreen.kt +113 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsViewModel.kt +28 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/AppIdentity.kt +10 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/Icons.kt +15 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/Tokens.kt +144 -0
- package/template/apps/android/app/src/main/res/drawable/ic_launcher_foreground.xml +17 -0
- package/template/apps/android/app/src/main/res/mipmap/ic_launcher.xml +10 -0
- package/template/apps/android/app/src/main/res/mipmap/ic_launcher_round.xml +10 -0
- package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/apps/android/app/src/main/res/values/generated_colors.xml +15 -0
- package/template/apps/android/app/src/main/res/values/generated_strings.xml +16 -0
- package/template/apps/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- package/template/apps/android/app/src/main/res/values/themes.xml +7 -0
- package/template/apps/android/app/src/main/res/values-night/generated_colors.xml +15 -0
- package/template/apps/android/app/src/main/res/values-night/themes.xml +6 -0
- package/template/apps/android/app/src/test/kotlin/app/features/settings/SettingsTest.kt +90 -0
- package/template/apps/android/build.gradle.kts +5 -0
- package/template/apps/android/gradle/gradle-daemon-jvm.properties +12 -0
- package/template/apps/android/gradle/libs.versions.toml +37 -0
- package/template/apps/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/apps/android/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/template/apps/android/gradle.properties +7 -0
- package/template/apps/android/gradlew +248 -0
- package/template/apps/android/gradlew.bat +82 -0
- package/template/apps/android/settings.gradle.kts +29 -0
- package/template/apps/ios/AGENTS.md +70 -0
- package/template/apps/ios/App/AppMain.swift +13 -0
- package/template/apps/ios/App/Core/AppContainer.swift +22 -0
- package/template/apps/ios/App/Core/DesignSystem/AppCard.swift +15 -0
- package/template/apps/ios/App/Core/Navigation/Route.swift +4 -0
- package/template/apps/ios/App/Core/RootView.swift +32 -0
- package/template/apps/ios/App/Features/Home/HomeScreen.swift +35 -0
- package/template/apps/ios/App/Features/Settings/Appearance.swift +27 -0
- package/template/apps/ios/App/Features/Settings/SettingsRepository.swift +40 -0
- package/template/apps/ios/App/Features/Settings/SettingsScreen.swift +40 -0
- package/template/apps/ios/App/Features/Settings/SettingsViewModel.swift +23 -0
- package/template/apps/ios/App/Generated/AppIdentity.swift +9 -0
- package/template/apps/ios/App/Generated/Colors.xcassets/AccentColor.colorset/Contents.json +38 -0
- package/template/apps/ios/App/Generated/Colors.xcassets/Contents.json +6 -0
- package/template/apps/ios/App/Generated/Icons.swift +8 -0
- package/template/apps/ios/App/Generated/Localizable.xcstrings +127 -0
- package/template/apps/ios/App/Generated/Strings.swift +40 -0
- package/template/apps/ios/App/Generated/Tokens.swift +65 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/Contents.json +6 -0
- package/template/apps/ios/App.xcodeproj/project.pbxproj +298 -0
- package/template/apps/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/template/apps/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +89 -0
- package/template/apps/ios/AppTests/SettingsTests.swift +54 -0
- package/template/apps/ios/CLAUDE.md +1 -0
- package/template/apps/ios/Config/App.xcconfig +23 -0
- package/template/apps/ios/Config/AppTests.xcconfig +10 -0
- package/template/apps/ios/Config/Base.xcconfig +25 -0
- package/template/apps/ios/Config/Debug.xcconfig +9 -0
- package/template/apps/ios/Config/Identity.xcconfig +5 -0
- package/template/apps/ios/Config/Release.xcconfig +6 -0
- package/template/apps/web/AGENTS.md +63 -0
- package/template/apps/web/CLAUDE.md +1 -0
- package/template/apps/web/package.json +38 -0
- package/template/apps/web/public/favicon.svg +6 -0
- package/template/apps/web/src/core/AppContainer.ts +18 -0
- package/template/apps/web/src/core/AppearanceEffect.tsx +31 -0
- package/template/apps/web/src/core/designsystem/AppCard.tsx +6 -0
- package/template/apps/web/src/features/home/HomeScreen.tsx +15 -0
- package/template/apps/web/src/features/home/home.test.tsx +22 -0
- package/template/apps/web/src/features/settings/Appearance.ts +29 -0
- package/template/apps/web/src/features/settings/LocalStorageSettingsRepository.ts +21 -0
- package/template/apps/web/src/features/settings/SettingsRepository.ts +7 -0
- package/template/apps/web/src/features/settings/SettingsScreen.tsx +44 -0
- package/template/apps/web/src/features/settings/settings.test.ts +45 -0
- package/template/apps/web/src/features/settings/useSettingsViewModel.ts +42 -0
- package/template/apps/web/src/generated/app.ts +9 -0
- package/template/apps/web/src/generated/icons.ts +9 -0
- package/template/apps/web/src/generated/strings.ts +88 -0
- package/template/apps/web/src/generated/tokens.css +86 -0
- package/template/apps/web/src/routeTree.gen.ts +86 -0
- package/template/apps/web/src/router.tsx +23 -0
- package/template/apps/web/src/routes/__root.tsx +72 -0
- package/template/apps/web/src/routes/index.tsx +6 -0
- package/template/apps/web/src/routes/settings.tsx +9 -0
- package/template/apps/web/src/styles.css +9 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/tsr.config.json +3 -0
- package/template/apps/web/vite.config.ts +9 -0
- package/template/apps/web/vitest.config.ts +9 -0
- package/template/contracts/app.json +7 -0
- package/template/contracts/icons.json +5 -0
- package/template/contracts/strings/en.json +13 -0
- package/template/contracts/tokens.json +40 -0
- package/template/package.json +17 -0
- package/template/spec/decisions.md +10 -0
- package/template/spec/features/home.md +39 -0
- package/template/spec/features/settings.md +50 -0
- package/template/spec/navigation.md +16 -0
- package/template/spec/parity.md +29 -0
- package/README.md +0 -3
package/src/lib/exec.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
3
|
+
import { createWriteStream, mkdirSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs a command, writing all of its output to `logFile`.
|
|
8
|
+
* Agents get a short summary on screen and can open the log when they need detail.
|
|
9
|
+
* @param {string} cmd
|
|
10
|
+
* @param {string[]} args
|
|
11
|
+
* @param {{ cwd?: string, logFile: string, env?: Record<string, string> }} options
|
|
12
|
+
* @returns {Promise<{ code: number, lines: string[] }>}
|
|
13
|
+
*/
|
|
14
|
+
export function runLogged(cmd, args, { cwd, logFile, env }) {
|
|
15
|
+
mkdirSync(path.dirname(logFile), { recursive: true })
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const out = createWriteStream(logFile)
|
|
18
|
+
out.write(`$ ${[cmd, ...args].join(' ')}\n\n`)
|
|
19
|
+
const child = spawn(cmd, args, { cwd, env: { ...process.env, ...env } })
|
|
20
|
+
/** @type {string[]} */
|
|
21
|
+
const lines = []
|
|
22
|
+
let pending = ''
|
|
23
|
+
/** @param {Buffer} chunk */
|
|
24
|
+
const collect = (chunk) => {
|
|
25
|
+
// Plain text: easier for agents to read, and for us to match.
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
const text = chunk.toString().replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
28
|
+
out.write(text)
|
|
29
|
+
pending += text
|
|
30
|
+
const parts = pending.split('\n')
|
|
31
|
+
pending = parts.pop() ?? ''
|
|
32
|
+
lines.push(...parts)
|
|
33
|
+
}
|
|
34
|
+
child.stdout.on('data', collect)
|
|
35
|
+
child.stderr.on('data', collect)
|
|
36
|
+
child.on('error', (error) => {
|
|
37
|
+
out.end()
|
|
38
|
+
resolve({ code: 127, lines: [String(error)] })
|
|
39
|
+
})
|
|
40
|
+
child.on('close', (code) => {
|
|
41
|
+
if (pending) lines.push(pending)
|
|
42
|
+
out.end()
|
|
43
|
+
resolve({ code: code ?? 1, lines })
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs a command attached to this terminal (for dev servers and interactive tools).
|
|
50
|
+
* @param {string} cmd
|
|
51
|
+
* @param {string[]} args
|
|
52
|
+
* @param {{ cwd?: string, env?: Record<string, string> }} [options]
|
|
53
|
+
* @returns {Promise<number>}
|
|
54
|
+
*/
|
|
55
|
+
export function runInherit(cmd, args, { cwd, env } = {}) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const child = spawn(cmd, args, { cwd, stdio: 'inherit', env: { ...process.env, ...env } })
|
|
58
|
+
child.on('error', () => resolve(127))
|
|
59
|
+
child.on('close', (code) => resolve(code ?? 1))
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Runs a command and returns its trimmed stdout, or null when it fails or isn't installed.
|
|
65
|
+
* @param {string} cmd
|
|
66
|
+
* @param {string[]} args
|
|
67
|
+
* @param {{ cwd?: string, input?: string, timeout?: number }} [options]
|
|
68
|
+
*/
|
|
69
|
+
export function capture(cmd, args, options = {}) {
|
|
70
|
+
const result = spawnSync(cmd, args, { encoding: 'utf8', ...options })
|
|
71
|
+
return result.status === 0 ? result.stdout.trim() : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {string} cmd */
|
|
75
|
+
export function which(cmd) {
|
|
76
|
+
return capture('which', [cmd])
|
|
77
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { CliError } from './log.js'
|
|
5
|
+
|
|
6
|
+
/** Build output and local state that never belongs in a template copy. */
|
|
7
|
+
const ARTIFACTS = new Set([
|
|
8
|
+
'node_modules',
|
|
9
|
+
'.build',
|
|
10
|
+
'build',
|
|
11
|
+
'.gradle',
|
|
12
|
+
'.kotlin',
|
|
13
|
+
'.idea',
|
|
14
|
+
'DerivedData',
|
|
15
|
+
'xcuserdata',
|
|
16
|
+
'.output',
|
|
17
|
+
'.tanstack',
|
|
18
|
+
'.nitro',
|
|
19
|
+
'.vinxi',
|
|
20
|
+
'dist',
|
|
21
|
+
'.DS_Store',
|
|
22
|
+
'local.properties',
|
|
23
|
+
'package-lock.json',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads and parses a JSON file, with an error message an agent can act on.
|
|
28
|
+
* @param {string} file
|
|
29
|
+
* @param {string} label shown in errors, e.g. "contracts/app.json"
|
|
30
|
+
*/
|
|
31
|
+
export function readJson(file, label) {
|
|
32
|
+
let text
|
|
33
|
+
try {
|
|
34
|
+
text = readFileSync(file, 'utf8')
|
|
35
|
+
} catch {
|
|
36
|
+
throw new CliError(`${label} is missing.`)
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new CliError(`${label} is not valid JSON: ${/** @type {Error} */ (error).message}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Writes `content` only if it differs from what's on disk. Returns true when the file changed.
|
|
47
|
+
* @param {string} file
|
|
48
|
+
* @param {string | Buffer} content
|
|
49
|
+
*/
|
|
50
|
+
export function writeIfChanged(file, content) {
|
|
51
|
+
const next = typeof content === 'string' ? Buffer.from(content) : content
|
|
52
|
+
if (existsSync(file) && readFileSync(file).equals(next)) return false
|
|
53
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
54
|
+
writeFileSync(file, next)
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Lists files under `dir` recursively, skipping build artifacts and dependencies.
|
|
60
|
+
* @param {string} dir
|
|
61
|
+
* @returns {string[]} absolute paths
|
|
62
|
+
*/
|
|
63
|
+
export function walk(dir) {
|
|
64
|
+
if (!existsSync(dir)) return []
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const files = []
|
|
67
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
if (ARTIFACTS.has(entry.name)) continue
|
|
69
|
+
const full = path.join(dir, entry.name)
|
|
70
|
+
if (entry.isDirectory()) files.push(...walk(full))
|
|
71
|
+
else files.push(full)
|
|
72
|
+
}
|
|
73
|
+
return files
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copies the template into a new project. Files named `_gitignore` become `.gitignore`
|
|
78
|
+
* (npm drops real .gitignore files from published packages).
|
|
79
|
+
* @param {string} from
|
|
80
|
+
* @param {string} to
|
|
81
|
+
* @param {{ keepGitignoreNames?: boolean }} [options]
|
|
82
|
+
*/
|
|
83
|
+
export function copyTemplate(from, to, { keepGitignoreNames = false } = {}) {
|
|
84
|
+
for (const file of walk(from)) {
|
|
85
|
+
const relative = path.relative(from, file)
|
|
86
|
+
if (relative.startsWith(`.geeknative${path.sep}logs${path.sep}`)) continue
|
|
87
|
+
const renamed = keepGitignoreNames ? relative : relative.replace(/(^|[\\/])_gitignore$/, '$1.gitignore')
|
|
88
|
+
const target = path.join(to, renamed)
|
|
89
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
90
|
+
copyFileSync(file, target)
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/lib/log.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
4
|
+
|
|
5
|
+
/** @param {number} code */
|
|
6
|
+
const paint = (code) => /** @param {unknown} text */ (text) =>
|
|
7
|
+
useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text)
|
|
8
|
+
|
|
9
|
+
export const color = {
|
|
10
|
+
bold: paint(1),
|
|
11
|
+
dim: paint(2),
|
|
12
|
+
red: paint(31),
|
|
13
|
+
green: paint(32),
|
|
14
|
+
yellow: paint(33),
|
|
15
|
+
cyan: paint(36),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {string} mark @param {string} label @param {string} message */
|
|
19
|
+
const line = (mark, label, message) => console.log(`${mark} ${label.padEnd(10)} ${message}`)
|
|
20
|
+
|
|
21
|
+
export const log = {
|
|
22
|
+
/** @param {string} message */
|
|
23
|
+
info: (message) => console.log(message),
|
|
24
|
+
/** @param {string} label @param {string} message */
|
|
25
|
+
step: (label, message) => line(color.cyan('▸'), label, message),
|
|
26
|
+
/** @param {string} label @param {string} message */
|
|
27
|
+
ok: (label, message) => line(color.green('✔'), label, message),
|
|
28
|
+
/** @param {string} label @param {string} message */
|
|
29
|
+
warn: (label, message) => line(color.yellow('!'), label, message),
|
|
30
|
+
/** @param {string} label @param {string} message */
|
|
31
|
+
fail: (label, message) => line(color.red('✖'), label, message),
|
|
32
|
+
/** @param {string} message */
|
|
33
|
+
detail: (message) => console.log(` ${message}`),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An expected failure: printed without a stack trace. */
|
|
37
|
+
export class CliError extends Error {}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { walk } from './fsutil.js'
|
|
6
|
+
import { rel } from './project.js'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Every file in a geeknative project has one owner:
|
|
10
|
+
* - Framework: the files and AGENTS.md blocks listed here. `geeknative upgrade` replaces them.
|
|
11
|
+
* - Project: everything else (docs, spec, contracts, app code). Upgrades never touch it.
|
|
12
|
+
* - Generated: output of `geeknative gen`, rebuilt from contracts.
|
|
13
|
+
* The lockfile records a hash of each framework-owned piece as installed, so an upgrade can
|
|
14
|
+
* tell untouched files (safe to replace) from locally edited ones (merge instead).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const MANAGED_DIRS = [
|
|
18
|
+
'.geeknative/playbooks',
|
|
19
|
+
'.geeknative/templates',
|
|
20
|
+
'.geeknative/hooks',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export const MANAGED_FILES = [
|
|
24
|
+
'.geeknative/framework.mk',
|
|
25
|
+
'.claude/agents/ios-engineer.md',
|
|
26
|
+
'.claude/agents/android-engineer.md',
|
|
27
|
+
'.claude/agents/web-engineer.md',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** Files with a framework-owned block between the markers below; the rest of the file is the project's. */
|
|
31
|
+
export const MANAGED_BLOCK_FILES = ['AGENTS.md', 'CLAUDE.md', 'apps/ios/AGENTS.md', 'apps/android/AGENTS.md', 'apps/web/AGENTS.md']
|
|
32
|
+
|
|
33
|
+
const BLOCK = /<!-- geeknative:begin[^>]*-->[\s\S]*?<!-- geeknative:end -->/
|
|
34
|
+
|
|
35
|
+
/** @param {string} text */
|
|
36
|
+
export function extractBlock(text) {
|
|
37
|
+
return text.match(BLOCK)?.[0] ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {string | Buffer} content */
|
|
41
|
+
const hash = (content) => `sha256-${createHash('sha256').update(content).digest('hex')}`
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hashes every framework-owned file and block in a project.
|
|
45
|
+
* @param {string} root
|
|
46
|
+
*/
|
|
47
|
+
export function managedHashes(root) {
|
|
48
|
+
/** @type {Record<string, string>} */
|
|
49
|
+
const files = {}
|
|
50
|
+
for (const dir of MANAGED_DIRS) {
|
|
51
|
+
for (const file of walk(path.join(root, dir))) files[rel(root, file)] = hash(readFileSync(file))
|
|
52
|
+
}
|
|
53
|
+
for (const file of MANAGED_FILES) {
|
|
54
|
+
const full = path.join(root, file)
|
|
55
|
+
if (existsSync(full)) files[file] = hash(readFileSync(full))
|
|
56
|
+
}
|
|
57
|
+
/** @type {Record<string, string>} */
|
|
58
|
+
const blocks = {}
|
|
59
|
+
for (const file of MANAGED_BLOCK_FILES) {
|
|
60
|
+
const full = path.join(root, file)
|
|
61
|
+
const block = existsSync(full) ? extractBlock(readFileSync(full, 'utf8')) : null
|
|
62
|
+
if (block) blocks[file] = hash(block)
|
|
63
|
+
}
|
|
64
|
+
return { files: sortKeys(files), blocks: sortKeys(blocks) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Writes .geeknative/lock.json: the framework version and the hashes of what it installed.
|
|
69
|
+
* @param {string} root
|
|
70
|
+
* @param {string} version
|
|
71
|
+
*/
|
|
72
|
+
export function writeLock(root, version) {
|
|
73
|
+
const lock = { geeknative: version, ...managedHashes(root) }
|
|
74
|
+
writeFileSync(path.join(root, '.geeknative', 'lock.json'), `${JSON.stringify(lock, null, 2)}\n`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {Record<string, string>} object */
|
|
78
|
+
function sortKeys(object) {
|
|
79
|
+
return Object.fromEntries(Object.entries(object).sort(([a], [b]) => a.localeCompare(b)))
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { CliError } from './log.js'
|
|
6
|
+
|
|
7
|
+
/** Root of the geeknative npm package. */
|
|
8
|
+
export const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
|
9
|
+
|
|
10
|
+
/** The repo's template/ folder, which exists only when running from a git checkout. */
|
|
11
|
+
const REPO_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, '../../template')
|
|
12
|
+
|
|
13
|
+
/** True when this CLI runs from a checkout of the geeknative repo rather than from npm. */
|
|
14
|
+
export function isLocalCheckout() {
|
|
15
|
+
return !existsSync(path.join(PACKAGE_DIR, 'template')) && existsSync(REPO_TEMPLATE_DIR)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The project template: bundled in the npm package, or the repo's template/ in a checkout. */
|
|
19
|
+
export function templateDir() {
|
|
20
|
+
const bundled = path.join(PACKAGE_DIR, 'template')
|
|
21
|
+
if (existsSync(bundled)) return bundled
|
|
22
|
+
if (existsSync(REPO_TEMPLATE_DIR)) return REPO_TEMPLATE_DIR
|
|
23
|
+
throw new CliError('Could not find the geeknative template.')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function packageVersion() {
|
|
27
|
+
return JSON.parse(readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf8')).version
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Walks up from `start` to the project root: the folder that contains contracts/app.json. */
|
|
31
|
+
export function findProjectRoot(start = process.cwd()) {
|
|
32
|
+
let dir = path.resolve(start)
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (existsSync(path.join(dir, 'contracts', 'app.json'))) return dir
|
|
35
|
+
const parent = path.dirname(dir)
|
|
36
|
+
if (parent === dir) {
|
|
37
|
+
throw new CliError(
|
|
38
|
+
'Not inside a geeknative project (no contracts/app.json found). Run this from your project folder.',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
dir = parent
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Path relative to the project root, with forward slashes.
|
|
47
|
+
* @param {string} root
|
|
48
|
+
* @param {string} file
|
|
49
|
+
*/
|
|
50
|
+
export function rel(root, file) {
|
|
51
|
+
return path.relative(root, file).split(path.sep).join('/')
|
|
52
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { capture } from './exec.js'
|
|
6
|
+
|
|
7
|
+
/** Android API level the app compiles against (apps/android/app/build.gradle.kts). */
|
|
8
|
+
export const ANDROID_COMPILE_SDK = 37
|
|
9
|
+
|
|
10
|
+
// ---------- Android ----------
|
|
11
|
+
|
|
12
|
+
/** @param {string} root */
|
|
13
|
+
function localPropertiesPath(root) {
|
|
14
|
+
return path.join(root, 'apps', 'android', 'local.properties')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Finds an Android SDK: local.properties, then ANDROID_HOME / ANDROID_SDK_ROOT, then common install locations.
|
|
19
|
+
* @param {string} root
|
|
20
|
+
* @returns {string | null}
|
|
21
|
+
*/
|
|
22
|
+
export function findAndroidSdk(root) {
|
|
23
|
+
const local = localPropertiesPath(root)
|
|
24
|
+
if (existsSync(local)) {
|
|
25
|
+
const match = readFileSync(local, 'utf8').match(/^sdk\.dir=(.+)$/m)
|
|
26
|
+
if (match && existsSync(match[1].trim())) return match[1].trim()
|
|
27
|
+
}
|
|
28
|
+
const home = os.homedir()
|
|
29
|
+
const candidates = [
|
|
30
|
+
process.env.ANDROID_HOME,
|
|
31
|
+
process.env.ANDROID_SDK_ROOT,
|
|
32
|
+
path.join(home, 'Library', 'Android', 'sdk'),
|
|
33
|
+
'/opt/homebrew/share/android-commandlinetools',
|
|
34
|
+
'/usr/local/share/android-commandlinetools',
|
|
35
|
+
path.join(home, 'Android', 'Sdk'),
|
|
36
|
+
]
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (candidate && existsSync(path.join(candidate, 'platform-tools'))) return candidate
|
|
39
|
+
if (candidate && existsSync(path.join(candidate, 'platforms'))) return candidate
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Points Gradle at the SDK. local.properties is machine-specific and git-ignored.
|
|
46
|
+
* @param {string} root
|
|
47
|
+
* @param {string} sdk
|
|
48
|
+
*/
|
|
49
|
+
export function writeLocalProperties(root, sdk) {
|
|
50
|
+
writeFileSync(localPropertiesPath(root), `sdk.dir=${sdk}\n`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {string} sdk */
|
|
54
|
+
export function hasCompilePlatform(sdk) {
|
|
55
|
+
const platforms = path.join(sdk, 'platforms')
|
|
56
|
+
if (!existsSync(platforms)) return false
|
|
57
|
+
return readdirSync(platforms).some((name) => name === `android-${ANDROID_COMPILE_SDK}` || name.startsWith(`android-${ANDROID_COMPILE_SDK}.`))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {string} sdk */
|
|
61
|
+
export function sdkManager(sdk) {
|
|
62
|
+
const bundled = path.join(sdk, 'cmdline-tools', 'latest', 'bin', 'sdkmanager')
|
|
63
|
+
return existsSync(bundled) ? bundled : 'sdkmanager'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {string} sdk */
|
|
67
|
+
export function adb(sdk) {
|
|
68
|
+
const bundled = path.join(sdk, 'platform-tools', 'adb')
|
|
69
|
+
return existsSync(bundled) ? bundled : 'adb'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {string} sdk */
|
|
73
|
+
export function emulator(sdk) {
|
|
74
|
+
const bundled = path.join(sdk, 'emulator', 'emulator')
|
|
75
|
+
return existsSync(bundled) ? bundled : 'emulator'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------- iOS ----------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @typedef {{ udid: string, name: string, state: string, runtime: string, version: number[] }} Simulator
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Picks the simulator to build and test on: GEEKNATIVE_IOS_SIMULATOR (name or UDID) if set,
|
|
86
|
+
* otherwise a booted iPhone, otherwise an iPhone on the newest installed iOS runtime.
|
|
87
|
+
* @returns {Simulator | null}
|
|
88
|
+
*/
|
|
89
|
+
export function pickSimulator() {
|
|
90
|
+
const json = capture('xcrun', ['simctl', 'list', 'devices', 'available', '--json'])
|
|
91
|
+
if (!json) return null
|
|
92
|
+
/** @type {Simulator[]} */
|
|
93
|
+
const all = []
|
|
94
|
+
for (const [runtime, devices] of Object.entries(JSON.parse(json).devices ?? {})) {
|
|
95
|
+
const match = runtime.match(/iOS-(\d+)-(\d+)(?:-(\d+))?$/)
|
|
96
|
+
if (!match) continue
|
|
97
|
+
const version = match.slice(1).filter(Boolean).map(Number)
|
|
98
|
+
for (const device of /** @type {any[]} */ (devices)) {
|
|
99
|
+
if (device.isAvailable === false) continue
|
|
100
|
+
all.push({ udid: device.udid, name: device.name, state: device.state, runtime: `iOS ${version.join('.')}`, version })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const wanted = process.env.GEEKNATIVE_IOS_SIMULATOR
|
|
104
|
+
if (wanted) return all.find((sim) => sim.udid === wanted || sim.name === wanted) ?? null
|
|
105
|
+
const iphones = all.filter((sim) => sim.name.startsWith('iPhone'))
|
|
106
|
+
const booted = iphones.find((sim) => sim.state === 'Booted')
|
|
107
|
+
if (booted) return booted
|
|
108
|
+
iphones.sort((a, b) => compareVersions(b.version, a.version))
|
|
109
|
+
return iphones[0] ?? null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {number[]} a @param {number[]} b */
|
|
113
|
+
function compareVersions(a, b) {
|
|
114
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
115
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0)
|
|
116
|
+
if (diff) return diff
|
|
117
|
+
}
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Xcode's major version, or null when Xcode isn't installed. */
|
|
122
|
+
export function xcodeMajorVersion() {
|
|
123
|
+
const out = capture('xcodebuild', ['-version'])
|
|
124
|
+
const match = out?.match(/^Xcode (\d+)/m)
|
|
125
|
+
return match ? Number(match[1]) : null
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: android-engineer
|
|
3
|
+
description: Implements and verifies changes in the Android app (apps/android, Kotlin and Jetpack Compose). Use for the Android part of any change to the apps, in parallel with ios-engineer and web-engineer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You build the Android app in apps/android. You're one of three platform engineers implementing the same spec; the lead agent coordinates.
|
|
7
|
+
|
|
8
|
+
Your brief from the lead has the spec path, the acceptance criteria IDs and what changed in contracts/. The Android conventions are in apps/android/AGENTS.md (given to you at start).
|
|
9
|
+
|
|
10
|
+
1. Read the spec. Look at an existing feature (features/settings) and follow the same patterns.
|
|
11
|
+
2. Implement from the spec. Don't port code from apps/ios or apps/web.
|
|
12
|
+
3. Edit only files under apps/android/. Never edit spec/, contracts/, PRODUCT.md, DESIGN.md or generated files. If the spec is ambiguous or missing something, build what's clear and report the gap instead of guessing.
|
|
13
|
+
4. Add or update tests named after the acceptance criteria: ``fun `AC-<id>-<n> ...`()``.
|
|
14
|
+
5. Run `make check-android` until it passes.
|
|
15
|
+
6. Report: the files you changed, the acceptance criteria your tests cover, the check result, and any spec gaps or questions.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ios-engineer
|
|
3
|
+
description: Implements and verifies changes in the iOS app (apps/ios, SwiftUI). Use for the iOS part of any change to the apps, in parallel with android-engineer and web-engineer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You build the iOS app in apps/ios. You're one of three platform engineers implementing the same spec; the lead agent coordinates.
|
|
7
|
+
|
|
8
|
+
Your brief from the lead has the spec path, the acceptance criteria IDs and what changed in contracts/. The iOS conventions are in apps/ios/AGENTS.md (given to you at start).
|
|
9
|
+
|
|
10
|
+
1. Read the spec. Look at an existing feature (App/Features/Settings) and follow the same patterns.
|
|
11
|
+
2. Implement from the spec. Don't port code from apps/android or apps/web.
|
|
12
|
+
3. Edit only files under apps/ios/. Never edit spec/, contracts/, PRODUCT.md, DESIGN.md or generated files. If the spec is ambiguous or missing something, build what's clear and report the gap instead of guessing.
|
|
13
|
+
4. Add or update tests named after the acceptance criteria: `@Test("AC-<id>-<n>: ...")`.
|
|
14
|
+
5. Run `make check-ios` until it passes.
|
|
15
|
+
6. Report: the files you changed, the acceptance criteria your tests cover, the check result, and any spec gaps or questions.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-engineer
|
|
3
|
+
description: Implements and verifies changes in the web app (apps/web, React with TanStack Start). Use for the web part of any change to the apps, in parallel with ios-engineer and android-engineer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You build the web app in apps/web. You're one of three platform engineers implementing the same spec; the lead agent coordinates.
|
|
7
|
+
|
|
8
|
+
Your brief from the lead has the spec path, the acceptance criteria IDs and what changed in contracts/. The web conventions are in apps/web/AGENTS.md (given to you at start).
|
|
9
|
+
|
|
10
|
+
1. Read the spec. Look at an existing feature (src/features/settings) and follow the same patterns.
|
|
11
|
+
2. Implement from the spec. Don't port code from apps/ios or apps/android.
|
|
12
|
+
3. Edit only files under apps/web/. Never edit spec/, contracts/, PRODUCT.md, DESIGN.md or generated files. If the spec is ambiguous or missing something, build what's clear and report the gap instead of guessing.
|
|
13
|
+
4. Add or update tests named after the acceptance criteria: `test('AC-<id>-<n>: ...')`.
|
|
14
|
+
5. Run `make check-web` until it passes.
|
|
15
|
+
6. Report: the files you changed, the acceptance criteria your tests cover, the check result, and any spec gaps or questions.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(make *)",
|
|
5
|
+
"Bash(npx geeknative *)",
|
|
6
|
+
"Bash(./node_modules/.bin/geeknative *)",
|
|
7
|
+
"Bash(npm run *)",
|
|
8
|
+
"Bash(xcodebuild *)",
|
|
9
|
+
"Bash(xcrun simctl *)",
|
|
10
|
+
"Bash(./gradlew *)"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"hooks": {
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.geeknative/hooks/after-edit.mjs\""
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"SubagentStart": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "ios-engineer",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.geeknative/hooks/subagent-context.mjs\" ios"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"matcher": "android-engineer",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.geeknative/hooks/subagent-context.mjs\" android"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"matcher": "web-engineer",
|
|
46
|
+
"hooks": [
|
|
47
|
+
{
|
|
48
|
+
"type": "command",
|
|
49
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.geeknative/hooks/subagent-context.mjs\" web"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"Stop": [
|
|
55
|
+
{
|
|
56
|
+
"hooks": [
|
|
57
|
+
{
|
|
58
|
+
"type": "command",
|
|
59
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.geeknative/hooks/parity-guard.mjs\""
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Managed by geeknative: replaced on upgrade. Add your own targets to the root Makefile.
|
|
2
|
+
|
|
3
|
+
GEEKNATIVE ?= ./node_modules/.bin/geeknative
|
|
4
|
+
|
|
5
|
+
.PHONY: help doctor gen check check-contracts check-ios check-android check-web parity run-ios run-android run-web
|
|
6
|
+
|
|
7
|
+
help: ## List the commands
|
|
8
|
+
@grep -hE '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " make %-16s %s\n", $$1, $$2}'
|
|
9
|
+
|
|
10
|
+
doctor: ## Check toolchains (FIX=1 installs what it can)
|
|
11
|
+
@$(GEEKNATIVE) doctor $(if $(FIX),--fix)
|
|
12
|
+
|
|
13
|
+
gen: ## Regenerate app code from contracts/
|
|
14
|
+
@$(GEEKNATIVE) gen
|
|
15
|
+
|
|
16
|
+
check: ## Validate contracts, then build and test all three apps
|
|
17
|
+
@$(GEEKNATIVE) check all
|
|
18
|
+
|
|
19
|
+
check-contracts: ## Validate contracts and check generated code is current
|
|
20
|
+
@$(GEEKNATIVE) check contracts
|
|
21
|
+
|
|
22
|
+
check-ios: ## Build and test the iOS app
|
|
23
|
+
@$(GEEKNATIVE) check ios
|
|
24
|
+
|
|
25
|
+
check-android: ## Build and test the Android app
|
|
26
|
+
@$(GEEKNATIVE) check android
|
|
27
|
+
|
|
28
|
+
check-web: ## Type-check, test and build the web app
|
|
29
|
+
@$(GEEKNATIVE) check web
|
|
30
|
+
|
|
31
|
+
parity: ## Update spec/parity.md from feature specs and tests
|
|
32
|
+
@$(GEEKNATIVE) parity
|
|
33
|
+
|
|
34
|
+
run-ios: ## Build and launch the iOS app in a simulator
|
|
35
|
+
@$(GEEKNATIVE) run ios
|
|
36
|
+
|
|
37
|
+
run-android: ## Build and launch the Android app on an emulator or device
|
|
38
|
+
@$(GEEKNATIVE) run android
|
|
39
|
+
|
|
40
|
+
run-web: ## Start the web dev server
|
|
41
|
+
@$(GEEKNATIVE) run web
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Claude Code PostToolUse hook (managed by geeknative).
|
|
2
|
+
// When a file in contracts/ is edited, regenerate app code right away, so the next build
|
|
3
|
+
// sees the change and mistakes in the contract are reported immediately.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
let input = ''
|
|
8
|
+
for await (const chunk of process.stdin) input += chunk
|
|
9
|
+
const file = JSON.parse(input || '{}').tool_input?.file_path
|
|
10
|
+
const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
11
|
+
|
|
12
|
+
if (!file || !path.resolve(root, file).startsWith(path.join(root, 'contracts') + path.sep)) process.exit(0)
|
|
13
|
+
|
|
14
|
+
const result = spawnSync(path.join(root, 'node_modules', '.bin', 'geeknative'), ['gen'], { cwd: root, encoding: 'utf8' })
|
|
15
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
16
|
+
|
|
17
|
+
if (result.error || result.status !== 0) {
|
|
18
|
+
console.error(`\`make gen\` failed after editing ${path.relative(root, file)}. Fix the contract:\n${output || result.error}`)
|
|
19
|
+
process.exit(2)
|
|
20
|
+
}
|
|
21
|
+
console.log(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
hookSpecificOutput: {
|
|
24
|
+
hookEventName: 'PostToolUse',
|
|
25
|
+
additionalContext: `Contracts changed, so geeknative regenerated the app code:\n${output}`,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
)
|