@joshuafolkken/kit 1.883.0 → 1.921.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/.claude/skills/workflow-commands/fullrun-steps.md +4 -2
- package/.claude/skills/workflow-commands/halfrun.md +3 -1
- package/.claude/skills/workflow-commands/kickoff.md +5 -3
- package/.claude/skills/workflow-commands/observation-filing.md +3 -1
- package/.claude/skills/workflow-commands/prerequisite.md +3 -1
- package/.claude/skills/workflow-commands/target-repository.md +1 -1
- package/.github/pull_request_template.md +12 -0
- package/.github/release.yml +3 -0
- package/.github/workflows/ci.yml +8 -0
- package/.github/workflows/production.yml +10 -1
- package/.github/workflows/publish.yml +72 -2
- package/CLAUDE.md +2 -0
- package/dist/CLAUDE.md +2 -0
- package/dist/CLAUDE.static.md +8 -0
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-36UE53LS.js → chunk-3HZ5MJIW.js} +2 -2
- package/dist/hooks/chunk-3V37IID6.js +82 -0
- package/dist/hooks/{chunk-FRJKVSK3.js → chunk-77RLKZ5V.js} +2 -2
- package/dist/hooks/{chunk-VGYSA2NT.js → chunk-LC765SUA.js} +16 -2
- package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NWD47MW3.js} +3 -3
- package/dist/hooks/{chunk-4OXFU3MK.js → chunk-O2FBZPSW.js} +619 -156
- package/dist/hooks/{chunk-6MQCXEEA.js → chunk-OLTAGGHH.js} +25 -88
- package/dist/hooks/{chunk-FNFOLFFV.js → chunk-OYU6KPRA.js} +1 -1
- package/dist/hooks/{chunk-PRDCNGA6.js → chunk-UANRLZV7.js} +4 -4
- package/dist/hooks/{chunk-264QY6KS.js → chunk-UULARQ5Q.js} +23 -1
- package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-XMR3BS35.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +11 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +3 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +10 -9
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/josh.js +155 -12
- package/package.json +70 -19
- package/prettier/index.js +3 -8
- package/prettier/static.d.ts +3 -0
- package/prettier/static.js +10 -0
- package/prompts/collaboration-workflow/issue-template.md +11 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +2 -2
- package/prompts/review.md +2 -2
- package/prompts/testing-guide.md +1 -0
- package/scripts/build/build-claude-md.ts +6 -1
- package/scripts/ci/pr-classification.ts +114 -0
- package/scripts/document/document-byte-budget.ts +3 -2
- package/scripts/gate/gate-report.ts +2 -0
- package/scripts/gate/gate-steps.ts +50 -0
- package/scripts/gate/project-checks.ts +121 -0
- package/scripts/gate/verification-gate.ts +14 -43
- package/scripts/git/git-common-directory.ts +9 -1
- package/scripts/git/git-gh-pr-rollup.ts +29 -1
- package/scripts/git/git-gh-pr.ts +61 -1
- package/scripts/git/git-pr.ts +62 -13
- package/scripts/git/issue-labels.ts +9 -0
- package/scripts/init/distributed-paths.ts +9 -5
- package/scripts/init/hook-command-bootstrap.ts +40 -8
- package/scripts/init/hook-command-rewrite.ts +7 -3
- package/scripts/init/init-actions.ts +94 -7
- package/scripts/init/init-ai-copy.ts +104 -19
- package/scripts/init/init-static.ts +52 -0
- package/scripts/init/init.ts +97 -30
- package/scripts/init/project-profile-cli.ts +14 -0
- package/scripts/init/project-profile.ts +149 -0
- package/scripts/init/start.ts +30 -0
- package/scripts/issue/issue-bug-label.ts +13 -0
- package/scripts/issue/issue-classification.ts +74 -0
- package/scripts/issue/issue-lint-cli.ts +10 -3
- package/scripts/issue/markdown-section.ts +38 -4
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +27 -1
- package/scripts/josh/josh-command-map.ts +3 -0
- package/scripts/josh/josh-commands-development.ts +6 -0
- package/scripts/josh/josh-commands-project.ts +14 -1
- package/scripts/josh/josh-logic.ts +13 -0
- package/scripts/josh/run-github-prerequisite.ts +32 -0
- package/scripts/lane/lane-await.ts +8 -1
- package/scripts/lane/openai-lane-ship.ts +30 -0
- package/scripts/lane/openai-lane-supervisor.ts +35 -8
- package/scripts/lane/openai-review-broker.ts +260 -0
- package/scripts/lint/cspell-cached.ts +21 -0
- package/scripts/lint/lint-parallel.ts +44 -1
- package/scripts/observations/observations-flush.ts +1 -1
- package/scripts/package/production-dependency-graph.ts +68 -0
- package/scripts/package/verify-optional-eslint-install.ts +62 -0
- package/scripts/propagate/propagate-run.ts +6 -0
- package/scripts/propagate/propagate-steps.ts +2 -0
- package/scripts/release/github-release-cli.ts +11 -0
- package/scripts/release/github-release.ts +220 -0
- package/scripts/release/publish-tag-cli.ts +8 -0
- package/scripts/release/publish-tag.ts +11 -0
- package/scripts/release/release-publish.ts +7 -2
- package/scripts/rules/delivered-rules.ts +2 -0
- package/scripts/rules/issue-bug-label.ts +85 -0
- package/scripts/rules/issue-filing-args.ts +50 -0
- package/scripts/run/run-merge-cli.ts +19 -0
- package/scripts/run/run-merge.ts +9 -1
- package/scripts/run/run-ship-cli.ts +20 -0
- package/scripts/run/run-ship-detach.ts +180 -14
- package/scripts/run/run-ship-review-steps.ts +45 -25
- package/scripts/run/run-wake-session.ts +3 -1
- package/scripts/test/pilot-files.ts +0 -2
- package/scripts/test/skip-marker.ts +3 -0
- package/scripts/test/test-declared-changed.ts +2 -1
- package/scripts/test/test-declared-logic.ts +15 -10
- package/scripts/test/test-declared.ts +20 -8
- package/scripts/test/test-red-logic.ts +3 -3
- package/scripts/test/test-unit-guard.ts +24 -12
- package/scripts/version/version-check-arguments.ts +14 -0
- package/scripts/version/version-check.ts +10 -3
- package/templates/CLAUDE.static.md +8 -0
- package/templates/cursorrules.static +1 -0
- package/templates/prettierignore.static +12 -0
- package/templates/workflows/ci.yml +5 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { json_format } from '#scripts/config-merge/json-format'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { init_logic } from './init-logic'
|
|
4
|
+
import type { ProjectShape } from './project-profile'
|
|
5
|
+
|
|
6
|
+
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
7
|
+
const record_schema = z.record(z.string(), z.unknown())
|
|
8
|
+
|
|
9
|
+
function is_record(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StaticVersions {
|
|
14
|
+
kit: string
|
|
15
|
+
prettier: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function initial_manifest(): string {
|
|
19
|
+
return json_format.format_json({ private: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function with_recorded_profile(content: string, profile: ProjectShape['profile']): string {
|
|
23
|
+
const parsed: unknown = JSON.parse(content)
|
|
24
|
+
const manifest = record_schema.parse(parsed)
|
|
25
|
+
const existing = manifest['josh']
|
|
26
|
+
const josh = is_record(existing) ? existing : {}
|
|
27
|
+
|
|
28
|
+
manifest['josh'] = { ...josh, profile }
|
|
29
|
+
|
|
30
|
+
return json_format.format_json(manifest)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function merge_static_manifest(
|
|
34
|
+
content: string,
|
|
35
|
+
shape: ProjectShape,
|
|
36
|
+
versions: StaticVersions,
|
|
37
|
+
): string {
|
|
38
|
+
const with_profile = with_recorded_profile(content, shape.profile)
|
|
39
|
+
const with_script = init_logic.merge_package_scripts(with_profile, { josh: 'josh' })
|
|
40
|
+
const with_kit = init_logic.merge_development_dependencies(with_script, {
|
|
41
|
+
[KIT_PACKAGE_NAME]: versions.kit,
|
|
42
|
+
})
|
|
43
|
+
const with_prettier = shape.has_web
|
|
44
|
+
? init_logic.merge_development_dependencies(with_kit, { prettier: versions.prettier })
|
|
45
|
+
: with_kit
|
|
46
|
+
|
|
47
|
+
return init_logic.sort_package_json_keys(with_prettier)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const init_static = { initial_manifest, with_recorded_profile, merge_static_manifest }
|
|
51
|
+
export { init_static }
|
|
52
|
+
export type { StaticVersions }
|
package/scripts/init/init.ts
CHANGED
|
@@ -12,16 +12,20 @@ import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
|
|
|
12
12
|
import { sync } from '#scripts/sync/sync'
|
|
13
13
|
import { package_manager_version } from '#scripts/version/package-manager-version'
|
|
14
14
|
import { execaSync } from 'execa'
|
|
15
|
+
import { z } from 'zod'
|
|
15
16
|
import { init_actions, PRETTIER_CONFIG_JS, type FileAction } from './init-actions'
|
|
16
17
|
import { init_ai_copy } from './init-ai-copy'
|
|
17
18
|
import { init_logic } from './init-logic'
|
|
18
19
|
import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
|
|
20
|
+
import { init_static } from './init-static'
|
|
19
21
|
import { plugin_install_hint_module } from './plugin-install-hint'
|
|
22
|
+
import { project_profile, type ProjectShape } from './project-profile'
|
|
20
23
|
|
|
21
24
|
const PACKAGE_JSON = 'package.json'
|
|
22
25
|
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
23
26
|
const LEFTHOOK_BIN = 'lefthook'
|
|
24
27
|
const SAMPLE_INDENT_WIDTH = 4
|
|
28
|
+
const ARGUMENT_START_INDEX = 2
|
|
25
29
|
const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
|
|
26
30
|
|
|
27
31
|
function write_new_file(action: FileAction, destination_path: string): void {
|
|
@@ -93,22 +97,48 @@ function get_kit_self_dependency(): Record<string, string> {
|
|
|
93
97
|
return { [KIT_PACKAGE_NAME]: version }
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
function
|
|
100
|
+
function get_eslint_development_dependencies(): Record<string, string> {
|
|
101
|
+
const manifest = z
|
|
102
|
+
.object({
|
|
103
|
+
peerDependencies: z.record(z.string(), z.string()),
|
|
104
|
+
devDependencies: z.record(z.string(), z.string()),
|
|
105
|
+
})
|
|
106
|
+
.parse(init_actions.read_package_json(PACKAGE_JSON))
|
|
107
|
+
const entries = Object.keys(manifest.peerDependencies)
|
|
108
|
+
.filter((name) => name !== '@playwright/test')
|
|
109
|
+
.map((name): [string, string] => {
|
|
110
|
+
const version = manifest.devDependencies[name]
|
|
111
|
+
if (version === undefined) throw new Error(`Missing development version for ${name}`)
|
|
112
|
+
|
|
113
|
+
return [name, version]
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return Object.fromEntries(entries)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function apply_dependency_merges(content: string, has_git = true): string {
|
|
97
120
|
const migrated = init_logic.strip_managed_postinstall(content)
|
|
121
|
+
const suggested = init_logic.get_suggested_scripts_for_content(migrated)
|
|
98
122
|
const merged = init_logic.merge_package_scripts(
|
|
99
123
|
migrated,
|
|
100
|
-
|
|
124
|
+
has_git
|
|
125
|
+
? suggested
|
|
126
|
+
: Object.fromEntries(Object.entries(suggested).filter(([key]) => key !== 'prepare')),
|
|
101
127
|
)
|
|
102
128
|
const with_prettier = init_logic.merge_prettier_plugin_development_deps(merged)
|
|
103
|
-
const
|
|
129
|
+
const with_eslint = init_logic.merge_development_dependencies(
|
|
130
|
+
with_prettier,
|
|
131
|
+
get_eslint_development_dependencies(),
|
|
132
|
+
)
|
|
133
|
+
const with_secretlint = init_logic.merge_secretlint_development_deps(with_eslint)
|
|
104
134
|
|
|
105
135
|
return init_logic.merge_development_dependencies(with_secretlint, get_kit_self_dependency())
|
|
106
136
|
}
|
|
107
137
|
|
|
108
|
-
function apply_package_json_merges(content: string): string {
|
|
109
|
-
const with_kit = apply_dependency_merges(content)
|
|
110
|
-
const upgraded = init_logic.upgrade_prepare_lefthook_warning(with_kit)
|
|
111
|
-
const with_lifecycle = init_logic.merge_prepare_lifecycle_cmd(upgraded)
|
|
138
|
+
function apply_package_json_merges(content: string, has_git = true): string {
|
|
139
|
+
const with_kit = apply_dependency_merges(content, has_git)
|
|
140
|
+
const upgraded = has_git ? init_logic.upgrade_prepare_lefthook_warning(with_kit) : with_kit
|
|
141
|
+
const with_lifecycle = has_git ? init_logic.merge_prepare_lifecycle_cmd(upgraded) : upgraded
|
|
112
142
|
const kit_pm = get_kit_package_manager()
|
|
113
143
|
const with_pm =
|
|
114
144
|
kit_pm === undefined ? with_lifecycle : init_logic.merge_package_manager(with_lifecycle, kit_pm)
|
|
@@ -121,21 +151,46 @@ function apply_package_json_merges(content: string): string {
|
|
|
121
151
|
return package_manager_version.align_development_engines_version(sorted)
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
function
|
|
125
|
-
const
|
|
126
|
-
|
|
154
|
+
function get_static_versions(): { kit: string; prettier: string } {
|
|
155
|
+
const manifest = z
|
|
156
|
+
.object({
|
|
157
|
+
version: z.string(),
|
|
158
|
+
devDependencies: z.record(z.string(), z.string()),
|
|
159
|
+
})
|
|
160
|
+
.parse(init_actions.read_package_json(PACKAGE_JSON))
|
|
161
|
+
const { prettier } = manifest.devDependencies
|
|
162
|
+
if (prettier === undefined) throw new Error('Missing prettier development version')
|
|
163
|
+
|
|
164
|
+
return { kit: manifest.version, prettier }
|
|
165
|
+
}
|
|
127
166
|
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
function merged_manifest(existing: string, shape: ProjectShape): string {
|
|
168
|
+
if (shape.profile === 'static') {
|
|
169
|
+
return init_static.merge_static_manifest(existing, shape, get_static_versions())
|
|
170
|
+
}
|
|
130
171
|
|
|
131
|
-
|
|
172
|
+
return init_static.with_recorded_profile(
|
|
173
|
+
apply_package_json_merges(existing, shape.has_git),
|
|
174
|
+
shape.profile,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function merge_project_package_json(shape: ProjectShape): void {
|
|
179
|
+
const package_json_path = path.join(PROJECT_ROOT, PACKAGE_JSON)
|
|
180
|
+
const is_existing = existsSync(package_json_path)
|
|
181
|
+
const existing = is_existing
|
|
182
|
+
? readFileSync(package_json_path, 'utf8')
|
|
183
|
+
: init_static.initial_manifest()
|
|
184
|
+
const merged = merged_manifest(existing, shape)
|
|
185
|
+
|
|
186
|
+
if (is_existing && merged === existing) {
|
|
132
187
|
console.info(' ✔ unchanged package.json')
|
|
133
188
|
|
|
134
189
|
return
|
|
135
190
|
}
|
|
136
191
|
|
|
137
192
|
writeFileSync(package_json_path, merged)
|
|
138
|
-
console.info(
|
|
193
|
+
console.info(` ✔ ${is_existing ? 'updated' : 'created'} package.json`)
|
|
139
194
|
}
|
|
140
195
|
|
|
141
196
|
function install_lefthook(): void {
|
|
@@ -148,14 +203,17 @@ function install_lefthook(): void {
|
|
|
148
203
|
}
|
|
149
204
|
}
|
|
150
205
|
|
|
151
|
-
function run_config_file_actions(): void {
|
|
206
|
+
function run_config_file_actions(shape: ProjectShape): void {
|
|
152
207
|
console.info('Config files:')
|
|
153
208
|
|
|
154
|
-
if (
|
|
209
|
+
if (
|
|
210
|
+
shape.profile === 'node' &&
|
|
211
|
+
sync.migrate_prettierrc(path.join(PROJECT_ROOT, PRETTIER_CONFIG_JS))
|
|
212
|
+
) {
|
|
155
213
|
console.info(' ✔ migrated .prettierrc → prettier.config.js')
|
|
156
214
|
}
|
|
157
215
|
|
|
158
|
-
for (const action of init_actions.build_file_actions()) {
|
|
216
|
+
for (const action of init_actions.build_file_actions(shape)) {
|
|
159
217
|
execute_file_action(action)
|
|
160
218
|
}
|
|
161
219
|
}
|
|
@@ -179,36 +237,45 @@ function report_repository_settings(name_with_owner: string | undefined): void {
|
|
|
179
237
|
// rather than through `sync`'s own `main()`, so the guard there never ran for it — and on top of the
|
|
180
238
|
// 14 files #868 reproduced, `init` also rewrites `package.json` scripts and devDependencies
|
|
181
239
|
// (joshuafolkken/kit#879). Checked before the first write, for the reason the sync guard is.
|
|
182
|
-
function run_ai_file_actions(): void {
|
|
240
|
+
function run_ai_file_actions(shape: ProjectShape): void {
|
|
183
241
|
console.info('\nAI files:')
|
|
184
242
|
// `init` writes the same npm-disabling `.github/dependabot.yml` that `sync` distributes, so a
|
|
185
243
|
// freshly scaffolded repository is exposed from its first commit — and a new private repository
|
|
186
244
|
// is exactly where the setting is off by default. The name is the one the Sonar config already
|
|
187
245
|
// resolved, so `gh repo view` runs once; the position is the pre-existing one.
|
|
188
|
-
const name_with_owner = init_ai_copy.run_ai_copies()
|
|
246
|
+
const name_with_owner = init_ai_copy.run_ai_copies(shape)
|
|
189
247
|
|
|
190
|
-
install_lefthook()
|
|
248
|
+
if (shape.has_git && shape.profile === 'node') install_lefthook()
|
|
191
249
|
|
|
192
|
-
report_repository_settings(name_with_owner)
|
|
250
|
+
if (shape.has_github) report_repository_settings(name_with_owner)
|
|
193
251
|
|
|
194
|
-
plugin_install_hint_module.report_plugin_install_hint()
|
|
252
|
+
if (shape.profile === 'node') plugin_install_hint_module.report_plugin_install_hint()
|
|
195
253
|
}
|
|
196
254
|
|
|
197
|
-
function
|
|
198
|
-
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
199
|
-
|
|
255
|
+
function initialize_project(shape: ProjectShape): void {
|
|
200
256
|
console.info('\n🚀 Initializing @joshuafolkken/kit\n')
|
|
201
|
-
|
|
257
|
+
console.info(`profile: ${shape.profile} (${shape.reason})`)
|
|
258
|
+
run_config_file_actions(shape)
|
|
202
259
|
|
|
203
260
|
console.info('\nPackage scripts:')
|
|
204
|
-
merge_project_package_json()
|
|
261
|
+
merge_project_package_json(shape)
|
|
205
262
|
|
|
206
|
-
run_ai_file_actions()
|
|
207
|
-
project_config.sync_project_config(PROJECT_ROOT)
|
|
263
|
+
run_ai_file_actions(shape)
|
|
264
|
+
if (shape.profile === 'node') project_config.sync_project_config(PROJECT_ROOT)
|
|
208
265
|
console.info('\n✅ Done.\n')
|
|
209
266
|
}
|
|
210
267
|
|
|
211
|
-
|
|
268
|
+
function main(args: ReadonlyArray<string> = []): void {
|
|
269
|
+
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
270
|
+
const requested = project_profile.requested_profile(args)
|
|
271
|
+
const shape = project_profile.inspect_project(PROJECT_ROOT, requested)
|
|
272
|
+
|
|
273
|
+
initialize_project(shape)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
277
|
+
main(process.argv.slice(ARGUMENT_START_INDEX))
|
|
278
|
+
}
|
|
212
279
|
|
|
213
280
|
const init = {
|
|
214
281
|
copy_ai_file: init_ai_copy.copy_ai_file,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { project_profile } from './project-profile'
|
|
4
|
+
|
|
5
|
+
const ARGUMENT_START_INDEX = 2
|
|
6
|
+
|
|
7
|
+
function main(): void {
|
|
8
|
+
const requested = project_profile.requested_profile(process.argv.slice(ARGUMENT_START_INDEX))
|
|
9
|
+
const result = project_profile.resolve_profile(process.cwd(), requested)
|
|
10
|
+
|
|
11
|
+
console.info(`profile: ${result.profile} (${result.reason})`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, type Dirent } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
4
|
+
import { execaSync } from 'execa'
|
|
5
|
+
|
|
6
|
+
type ProjectProfile = 'static' | 'node'
|
|
7
|
+
|
|
8
|
+
interface ProfileResult {
|
|
9
|
+
profile: ProjectProfile
|
|
10
|
+
reason: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ProjectShape extends ProfileResult {
|
|
14
|
+
has_web: boolean
|
|
15
|
+
has_typescript: boolean
|
|
16
|
+
has_git: boolean
|
|
17
|
+
has_github: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', '.svelte-kit'])
|
|
21
|
+
const WEB_EXTENSIONS = new Set(['.html', '.css', '.js', '.jsx', '.mjs', '.cjs'])
|
|
22
|
+
const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx'])
|
|
23
|
+
const PROFILE_ARG_LENGTH = 2
|
|
24
|
+
|
|
25
|
+
function parse_profile(value: unknown): ProjectProfile | undefined {
|
|
26
|
+
if (value === 'static' || value === 'node') return value
|
|
27
|
+
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requested_profile(args: ReadonlyArray<string>): ProjectProfile | undefined {
|
|
32
|
+
if (args.length === 0) return undefined
|
|
33
|
+
|
|
34
|
+
if (args.length !== PROFILE_ARG_LENGTH || args[0] !== '--profile') {
|
|
35
|
+
throw new Error('Usage: josh init [--profile static|node]')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const profile = parse_profile(args[1])
|
|
39
|
+
if (profile !== undefined) return profile
|
|
40
|
+
throw new Error('Profile must be static or node')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function read_manifest(root: string): Record<string, unknown> | undefined {
|
|
44
|
+
const filename = path.join(root, 'package.json')
|
|
45
|
+
if (!existsSync(filename)) return undefined
|
|
46
|
+
const parsed: unknown = JSON.parse(readFileSync(filename, 'utf8'))
|
|
47
|
+
|
|
48
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
49
|
+
throw new Error('package.json must contain an object')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return parsed as Record<string, unknown>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function has_entries(value: unknown): boolean {
|
|
56
|
+
return typeof value === 'object' && value !== null && Object.keys(value).length > 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function is_record(value: unknown): value is Record<string, unknown> {
|
|
60
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function recorded_profile(manifest: Record<string, unknown>): ProjectProfile | undefined {
|
|
64
|
+
const { josh } = manifest
|
|
65
|
+
|
|
66
|
+
return is_record(josh) ? parse_profile(josh['profile']) : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function has_build_script(value: unknown): boolean {
|
|
70
|
+
return is_record(value) && ('build' in value || 'dev' in value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inferred_profile(manifest: Record<string, unknown>): ProfileResult {
|
|
74
|
+
if (has_entries(manifest['dependencies']) || has_entries(manifest['devDependencies'])) {
|
|
75
|
+
return { profile: 'node', reason: 'package dependencies' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (has_build_script(manifest['scripts'])) {
|
|
79
|
+
return { profile: 'node', reason: 'build or dev script' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { profile: 'static', reason: 'metadata-only package.json' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolve_profile(root: string, requested?: ProjectProfile): ProfileResult {
|
|
86
|
+
if (requested !== undefined) return { profile: requested, reason: 'explicit --profile' }
|
|
87
|
+
const manifest = read_manifest(root)
|
|
88
|
+
if (manifest === undefined) return { profile: 'static', reason: 'no package.json' }
|
|
89
|
+
const profile = recorded_profile(manifest)
|
|
90
|
+
if (profile !== undefined) return { profile, reason: 'package.json josh.profile' }
|
|
91
|
+
|
|
92
|
+
return inferred_profile(manifest)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function should_visit(entry: Dirent): boolean {
|
|
96
|
+
return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scan_directory(directory: string, pending: Array<string>): Array<string> {
|
|
100
|
+
const entries = readdirSync(directory, { withFileTypes: true })
|
|
101
|
+
|
|
102
|
+
pending.push(
|
|
103
|
+
...entries
|
|
104
|
+
.filter((entry) => should_visit(entry))
|
|
105
|
+
.map((entry) => path.join(directory, entry.name)),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path.extname(entry.name))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function file_extensions(root: string): Array<string> {
|
|
112
|
+
const pending = [root]
|
|
113
|
+
const extensions: Array<string> = []
|
|
114
|
+
|
|
115
|
+
for (const directory of pending) {
|
|
116
|
+
extensions.push(...scan_directory(directory, pending))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return extensions
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scan_files(root: string): { has_web: boolean; has_typescript: boolean } {
|
|
123
|
+
const extensions = file_extensions(root)
|
|
124
|
+
const has_web = extensions.some((extension) => WEB_EXTENSIONS.has(extension))
|
|
125
|
+
const has_typescript = extensions.some((extension) => TYPESCRIPT_EXTENSIONS.has(extension))
|
|
126
|
+
|
|
127
|
+
return { has_web, has_typescript }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function has_github_remote(root: string): boolean {
|
|
131
|
+
const result = execaSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
132
|
+
cwd: root,
|
|
133
|
+
reject: false,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return result.exitCode === 0 && repo_origin.parse_origin_url(result.stdout) !== undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inspect_project(root: string, requested?: ProjectProfile): ProjectShape {
|
|
140
|
+
const profile = resolve_profile(root, requested)
|
|
141
|
+
const files = scan_files(root)
|
|
142
|
+
const has_git = existsSync(path.join(root, '.git'))
|
|
143
|
+
|
|
144
|
+
return { ...profile, ...files, has_git, has_github: has_git && has_github_remote(root) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const project_profile = { resolve_profile, inspect_project, requested_profile }
|
|
148
|
+
export { project_profile }
|
|
149
|
+
export type { ProjectProfile, ProjectShape }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { main as init_main } from './init'
|
|
4
|
+
import { project_profile, type ProjectShape } from './project-profile'
|
|
5
|
+
|
|
6
|
+
const ARGUMENT_START_INDEX = 2
|
|
7
|
+
|
|
8
|
+
function prerequisite(shape: ProjectShape): string | undefined {
|
|
9
|
+
if (!shape.has_git) return 'josh start requires Git. Run git init, or use josh init without Git.'
|
|
10
|
+
if (!shape.has_github) return 'josh start requires a GitHub origin. Add one, or use josh init.'
|
|
11
|
+
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function main(args: ReadonlyArray<string> = []): void {
|
|
16
|
+
// --profile uses the same override as josh init.
|
|
17
|
+
const requested = project_profile.requested_profile(args)
|
|
18
|
+
const shape = project_profile.inspect_project(process.cwd(), requested)
|
|
19
|
+
const message = prerequisite(shape)
|
|
20
|
+
if (message !== undefined) throw new Error(message)
|
|
21
|
+
init_main(args)
|
|
22
|
+
console.info('GitHub workflow ready. Use kickoff in your assistant to plan an Issue.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
26
|
+
main(process.argv.slice(ARGUMENT_START_INDEX))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const start = { prerequisite }
|
|
30
|
+
export { start }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { markdown_section } from './markdown-section'
|
|
2
|
+
|
|
3
|
+
const BUG_DECLARATION_LINE = '- 種別: 不具合'
|
|
4
|
+
|
|
5
|
+
function is_bug_fix(body: string): boolean {
|
|
6
|
+
const background = markdown_section.section_lines(body, '## 背景').join('\n')
|
|
7
|
+
|
|
8
|
+
return markdown_section.has_unfenced_line(background, BUG_DECLARATION_LINE)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const issue_bug_label = { BUG_DECLARATION_LINE, is_bug_fix }
|
|
12
|
+
|
|
13
|
+
export { issue_bug_label }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BREAKING_CHANGE_LABEL,
|
|
3
|
+
BUG_LABEL,
|
|
4
|
+
ENHANCEMENT_LABEL,
|
|
5
|
+
has_label_name,
|
|
6
|
+
} from '#scripts/git/issue-labels'
|
|
7
|
+
import { issue_bug_label } from './issue-bug-label'
|
|
8
|
+
import { markdown_section } from './markdown-section'
|
|
9
|
+
|
|
10
|
+
const FEATURE_DECLARATIONS = ['- 目的: 機能追加', '- 目的: 機能改善']
|
|
11
|
+
const BREAKING_DECLARATION = '- 互換性: 破壊的変更'
|
|
12
|
+
const NON_BUG_DECLARATION = '- 種別: 非不具合'
|
|
13
|
+
const CLASSIFICATION_LABELS = [BUG_LABEL, ENHANCEMENT_LABEL, BREAKING_CHANGE_LABEL]
|
|
14
|
+
const CONFLICT = 'bug and enhancement declarations cannot be combined'
|
|
15
|
+
const MISSING_BUG_CLASSIFICATION = 'missing bug classification'
|
|
16
|
+
const CONFLICTING_BUG_CLASSIFICATIONS = 'conflicting bug classifications'
|
|
17
|
+
|
|
18
|
+
function required_labels(body: string): ReadonlyArray<string> {
|
|
19
|
+
const labels: Array<string> = []
|
|
20
|
+
const background = markdown_section.section_lines(body, '## 背景').join('\n')
|
|
21
|
+
|
|
22
|
+
if (issue_bug_label.is_bug_fix(body)) labels.push(BUG_LABEL)
|
|
23
|
+
|
|
24
|
+
if (FEATURE_DECLARATIONS.some((line) => markdown_section.has_unfenced_line(background, line))) {
|
|
25
|
+
labels.push(ENHANCEMENT_LABEL)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (markdown_section.has_unfenced_line(background, BREAKING_DECLARATION)) {
|
|
29
|
+
labels.push(BREAKING_CHANGE_LABEL)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return labels
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function labels_for(body: string, existing: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
36
|
+
const missing = required_labels(body).filter((label) => !has_label_name(existing, label))
|
|
37
|
+
|
|
38
|
+
return [...existing, ...missing]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bug_classification_problem(body: string): string | undefined {
|
|
42
|
+
const background = markdown_section.section_lines(body, '## 背景').join('\n')
|
|
43
|
+
const has_bug = issue_bug_label.is_bug_fix(body)
|
|
44
|
+
const has_non_bug = markdown_section.has_unfenced_line(background, NON_BUG_DECLARATION)
|
|
45
|
+
|
|
46
|
+
if (!has_bug && !has_non_bug) return MISSING_BUG_CLASSIFICATION
|
|
47
|
+
if (has_bug && has_non_bug) return CONFLICTING_BUG_CLASSIFICATIONS
|
|
48
|
+
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function problems(body: string): ReadonlyArray<string> {
|
|
53
|
+
const classification_problem = bug_classification_problem(body)
|
|
54
|
+
const found: Array<string> = []
|
|
55
|
+
|
|
56
|
+
if (classification_problem !== undefined) found.push(classification_problem)
|
|
57
|
+
const labels = required_labels(body)
|
|
58
|
+
|
|
59
|
+
if (labels.includes(BUG_LABEL) && labels.includes(ENHANCEMENT_LABEL)) found.push(CONFLICT)
|
|
60
|
+
|
|
61
|
+
return found
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const issue_classification = {
|
|
65
|
+
BREAKING_DECLARATION,
|
|
66
|
+
CLASSIFICATION_LABELS,
|
|
67
|
+
FEATURE_DECLARATIONS,
|
|
68
|
+
NON_BUG_DECLARATION,
|
|
69
|
+
labels_for,
|
|
70
|
+
problems,
|
|
71
|
+
required_labels,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { issue_classification }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { behavior_change_lint } from './behavior-change-lint'
|
|
5
|
+
import { issue_classification } from './issue-classification'
|
|
5
6
|
import { issue_lint } from './issue-lint'
|
|
6
7
|
|
|
7
8
|
// `josh issue:lint <path>` — read an Issue body from a file and print `ok`, or every template problem
|
|
@@ -13,6 +14,7 @@ import { issue_lint } from './issue-lint'
|
|
|
13
14
|
const SUCCESS_EXIT_CODE = 0
|
|
14
15
|
const FAILURE_EXIT_CODE = 1
|
|
15
16
|
const OK_MESSAGE = 'ok'
|
|
17
|
+
const NO_LABELS = 'none'
|
|
16
18
|
const USAGE = 'Usage: josh issue:lint <path-to-issue-body>'
|
|
17
19
|
|
|
18
20
|
// Every problem a body has: the four base template headings it lacks, then the behavior-change
|
|
@@ -20,12 +22,17 @@ const USAGE = 'Usage: josh issue:lint <path-to-issue-body>'
|
|
|
20
22
|
function problems(body: string): ReadonlyArray<string> {
|
|
21
23
|
const missing = issue_lint.missing_headings(body).map((heading) => `missing heading: ${heading}`)
|
|
22
24
|
|
|
23
|
-
return [
|
|
25
|
+
return [
|
|
26
|
+
...missing,
|
|
27
|
+
...behavior_change_lint.problems(body),
|
|
28
|
+
...issue_classification.problems(body),
|
|
29
|
+
]
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
function report(found: ReadonlyArray<string
|
|
32
|
+
function report(found: ReadonlyArray<string>, body: string): number {
|
|
27
33
|
if (found.length === 0) {
|
|
28
34
|
console.info(OK_MESSAGE)
|
|
35
|
+
console.info(`labels: ${issue_classification.required_labels(body).join(', ') || NO_LABELS}`)
|
|
29
36
|
|
|
30
37
|
return SUCCESS_EXIT_CODE
|
|
31
38
|
}
|
|
@@ -44,7 +51,7 @@ async function run(body_path: string | undefined): Promise<number> {
|
|
|
44
51
|
|
|
45
52
|
const body = await readFile(body_path, 'utf8')
|
|
46
53
|
|
|
47
|
-
return report(problems(body))
|
|
54
|
+
return report(problems(body), body)
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
@@ -15,8 +15,41 @@ function is_the_heading(line: string, heading: string): boolean {
|
|
|
15
15
|
// Whether some body line, trimmed, is exactly `text`. Both linters ask this — of a `## heading` and of
|
|
16
16
|
// the behavior-change declaration line — so the trim-and-match lives here rather than being written in
|
|
17
17
|
// each. A required token mentioned inside a sentence is not the token, so only an exact line matches.
|
|
18
|
+
function next_fence(line: string, fence: string): string | undefined {
|
|
19
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/u.exec(line)?.[1]
|
|
20
|
+
|
|
21
|
+
if (fence === '') return marker
|
|
22
|
+
const closing = new RegExp(
|
|
23
|
+
String.raw`^ {0,3}${fence.charAt(0)}{${String(fence.length)},}\s*$`,
|
|
24
|
+
'u',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (closing.test(line)) return ''
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unfenced_lines(body: string): ReadonlyArray<string> {
|
|
33
|
+
let fence = ''
|
|
34
|
+
const visible: Array<string> = []
|
|
35
|
+
|
|
36
|
+
for (const line of body.split('\n')) {
|
|
37
|
+
const next = next_fence(line, fence)
|
|
38
|
+
const visible_line = line.replace(/^(?: {4}|\t).*/u, '')
|
|
39
|
+
|
|
40
|
+
visible.push(next === undefined && fence === '' ? visible_line : '')
|
|
41
|
+
fence = next ?? fence
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return visible
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function has_unfenced_line(body: string, text: string): boolean {
|
|
48
|
+
return unfenced_lines(body).some((line) => line.trim() === text)
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
function has_line(body: string, text: string): boolean {
|
|
19
|
-
return body
|
|
52
|
+
return has_unfenced_line(body, text)
|
|
20
53
|
}
|
|
21
54
|
|
|
22
55
|
// The lines between `heading` and the next `## ` heading, trimmed of the heading line itself; an empty
|
|
@@ -24,16 +57,17 @@ function has_line(body: string, text: string): boolean {
|
|
|
24
57
|
// only a line that is exactly the heading opens the section.
|
|
25
58
|
function section_lines(body: string, heading: string): ReadonlyArray<string> {
|
|
26
59
|
const lines = body.split('\n')
|
|
27
|
-
const
|
|
60
|
+
const visible = unfenced_lines(body)
|
|
61
|
+
const start = visible.findIndex((line) => is_the_heading(line, heading))
|
|
28
62
|
|
|
29
63
|
if (start === -1) return []
|
|
30
64
|
|
|
31
65
|
const rest = lines.slice(start + 1)
|
|
32
|
-
const end =
|
|
66
|
+
const end = visible.slice(start + 1).findIndex((line) => is_heading(line))
|
|
33
67
|
|
|
34
68
|
return end === -1 ? rest : rest.slice(0, end)
|
|
35
69
|
}
|
|
36
70
|
|
|
37
|
-
const markdown_section = { section_lines, is_heading, has_line }
|
|
71
|
+
const markdown_section = { section_lines, is_heading, has_line, has_unfenced_line }
|
|
38
72
|
|
|
39
73
|
export { markdown_section }
|