@joshuafolkken/kit 1.983.0 → 1.1003.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/backlogrun-child.md +4 -5
- package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
- package/.github/workflows/sonar-qube.yml +12 -2
- package/CLAUDE.md +2 -2
- package/README.md +30 -21
- package/dist/CLAUDE.md +2 -2
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-ZT65LR3U.js → chunk-4GDR2YTV.js} +4 -4
- package/dist/hooks/{chunk-ASUZGZFT.js → chunk-56LGMJJV.js} +2 -2
- package/dist/hooks/{chunk-LBXQHXMC.js → chunk-6PSTVOLH.js} +263 -74
- package/dist/hooks/{chunk-P3UX7SFN.js → chunk-A2H5I2VD.js} +2 -2
- package/dist/hooks/{chunk-ERXMJJOV.js → chunk-AWTFKKFW.js} +1 -1
- package/dist/hooks/{chunk-IM3KM25Y.js → chunk-EZLHXKJ6.js} +10 -7
- package/dist/hooks/{chunk-APVOACVT.js → chunk-GBJUERYK.js} +1 -1
- package/dist/hooks/{chunk-5JX6HXDX.js → chunk-KPZN42RT.js} +3 -3
- package/dist/hooks/{chunk-47SPMGOD.js → chunk-PAXMLGSS.js} +26 -7
- package/dist/hooks/{chunk-RFPHKU7C.js → chunk-WMIR5SNX.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +98 -26
- package/dist/josh.js +5 -5
- package/package.json +5 -1
- package/prettier/basic.d.ts +3 -0
- package/prettier/basic.js +10 -0
- package/prettier/index.js +2 -2
- package/prettier/static.js +3 -10
- package/prompts/sonar-hotspot-handling.md +6 -0
- package/prompts/testing-guide.md +1 -1
- package/scripts/agent/agent-diagnostics.ts +1 -0
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/behavior/behavior-rules.ts +8 -1
- package/scripts/build/build-claude-md.ts +13 -4
- package/scripts/ci/pr-classification.ts +11 -2
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/gate/gate-steps.ts +1 -1
- package/scripts/gate/project-checks.ts +12 -11
- package/scripts/gate/type-check-command.ts +1 -1
- package/scripts/git/git-branch.ts +14 -3
- package/scripts/git/git-command.ts +5 -2
- package/scripts/git/git-issue.ts +13 -5
- package/scripts/git/git-pr.ts +16 -10
- package/scripts/git/git-preflight.ts +138 -0
- package/scripts/git/telegram-notify.ts +21 -0
- package/scripts/hooks/stop-guard.ts +2 -0
- package/scripts/init/basic-path-migration.ts +24 -0
- package/scripts/init/distributed-paths.ts +22 -0
- package/scripts/init/init-actions.ts +27 -19
- package/scripts/init/init-ai-copy.ts +86 -28
- package/scripts/init/{init-static.ts → init-basic.ts} +6 -6
- package/scripts/init/init-bootstrap.ts +4 -3
- package/scripts/init/init-install.ts +1 -1
- package/scripts/init/init-logic-templates.ts +1 -1
- package/scripts/init/init-logic.ts +1 -1
- package/scripts/init/init.ts +41 -19
- package/scripts/init/kit-setup-state.ts +30 -0
- package/scripts/init/kit-written-paths.ts +33 -0
- package/scripts/init/project-profile-cli.ts +15 -1
- package/scripts/init/project-profile.ts +41 -12
- package/scripts/init/session-language-environment.ts +83 -0
- package/scripts/init/start-exec.ts +28 -0
- package/scripts/init/start-plan.ts +35 -13
- package/scripts/init/start-prompt.ts +22 -8
- package/scripts/init/start-setup-pr.ts +138 -0
- package/scripts/init/start-steps.ts +30 -30
- package/scripts/init/start.ts +7 -3
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +3 -3
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-project.ts +2 -2
- package/scripts/lane/lane-leftover.ts +143 -0
- package/scripts/lane/lane-open.ts +14 -1
- package/scripts/lint/cspell-cached.ts +3 -3
- package/scripts/lint/format.ts +1 -1
- package/scripts/lint/lint-parallel.ts +2 -2
- package/scripts/propagate/propagate-steps.ts +14 -1
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/issue-citation.ts +30 -1
- package/scripts/rules/last-prompt.ts +79 -0
- package/scripts/rules/stop-rules.ts +5 -3
- package/scripts/run/run-hold.ts +3 -3
- package/scripts/run/run-preflight.ts +75 -5
- package/scripts/run/run-progress-cli.ts +14 -2
- package/scripts/safe-chain/ci-installer-pin.ts +19 -12
- package/scripts/safe-chain/project-config.ts +1 -1
- package/scripts/sync/sync-ai-files.ts +204 -0
- package/scripts/sync/sync.ts +48 -184
- package/scripts/test/josh-harness-environment.ts +1 -1
- package/scripts/test/pilot-files.ts +0 -1
- package/scripts/test/test-declared-changed.ts +1 -1
- package/scripts/test/test-declared-logic.ts +55 -14
- package/scripts/test/test-declared.ts +23 -11
- package/scripts/test/test-unit-guard.ts +6 -6
- package/scripts/time-runtime/time-shell.ts +36 -2
- package/scripts-ai/git-workflow.ts +10 -0
- package/templates/CLAUDE.basic.md +8 -0
- package/templates/workflows/ci.yml +27 -0
- /package/{templates/CLAUDE.static.md → dist/CLAUDE.basic.md} +0 -0
- /package/templates/{cursorrules.static → cursorrules.basic} +0 -0
- /package/templates/{pnpm-workspace.static.yaml → pnpm-workspace.basic.yaml} +0 -0
- /package/templates/{prettierignore.static → prettierignore.basic} +0 -0
package/scripts/sync/sync.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { project_checks } from '#scripts/gate/project-checks'
|
|
5
6
|
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
6
|
-
import {
|
|
7
|
+
import { basic_path_migration } from '#scripts/init/basic-path-migration'
|
|
7
8
|
import { init_logic } from '#scripts/init/init-logic'
|
|
8
9
|
import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
9
10
|
import { plugin_install_hint_module } from '#scripts/init/plugin-install-hint'
|
|
11
|
+
import { project_profile } from '#scripts/init/project-profile'
|
|
10
12
|
import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
|
|
11
13
|
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
12
14
|
import { project_config } from '#scripts/safe-chain/project-config'
|
|
@@ -14,121 +16,13 @@ import { security_updates } from '#scripts/security/security-updates'
|
|
|
14
16
|
import { sonar_file } from '#scripts/security/sonar-file'
|
|
15
17
|
import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
|
|
16
18
|
import { package_manager_version } from '#scripts/version/package-manager-version'
|
|
17
|
-
import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
|
|
18
19
|
import { file_content } from './file-content'
|
|
19
|
-
import {
|
|
20
|
-
import { skill_migration, type MigrationResult } from './skill-migration'
|
|
20
|
+
import { sync_ai_files } from './sync-ai-files'
|
|
21
21
|
import { sync_configs } from './sync-configs'
|
|
22
|
-
import { sync_hook_safety } from './sync-hook-safety'
|
|
23
22
|
|
|
24
|
-
const WORKSPACE_YAML = 'pnpm-workspace.yaml'
|
|
25
23
|
const PACKAGE_JSON = 'package.json'
|
|
26
24
|
const PACKAGE_JSON_UNCHANGED_MSG = ' ✔ unchanged package.json'
|
|
27
|
-
const
|
|
28
|
-
const CLAUDE_SETTINGS_FILE = '.claude/settings.json'
|
|
29
|
-
const CODEX_HOOKS_FILE = '.codex/hooks.json'
|
|
30
|
-
|
|
31
|
-
function sync_ai_file(source_path: string, destination_path: string): boolean {
|
|
32
|
-
const content = readFileSync(source_path, 'utf8')
|
|
33
|
-
|
|
34
|
-
return file_content.write_text_if_changed(
|
|
35
|
-
destination_path,
|
|
36
|
-
transform_copied_content(destination_path, content),
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Both copied hook files must run against the consumer's installed bundle. When sync is run from a
|
|
41
|
-
// newer source than that install, writing either file would leave hooks
|
|
42
|
-
// that fail every prompt (joshuafolkken/kit#1930); skip it and print how to update instead.
|
|
43
|
-
function should_skip_hook_file(filename: string): boolean {
|
|
44
|
-
if (filename !== CLAUDE_SETTINGS_FILE && filename !== CODEX_HOOKS_FILE) return false
|
|
45
|
-
const warning = sync_hook_safety.hook_write_warning(
|
|
46
|
-
PROJECT_ROOT,
|
|
47
|
-
sync_hook_safety.read_version_at(path.join(PACKAGE_DIR, PACKAGE_JSON)),
|
|
48
|
-
filename,
|
|
49
|
-
)
|
|
50
|
-
if (warning === undefined) return false
|
|
51
|
-
console.warn(warning)
|
|
52
|
-
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function sync_file(filename: string): void {
|
|
57
|
-
if (should_skip_hook_file(filename)) return
|
|
58
|
-
const did_change = sync_ai_file(
|
|
59
|
-
path.join(PACKAGE_DIR, filename),
|
|
60
|
-
path.join(PROJECT_ROOT, filename),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sync_file_mapping(source_path: string, destination_path: string): void {
|
|
67
|
-
if (!existsSync(source_path)) {
|
|
68
|
-
console.warn(` ⚠ skipped ${path.basename(destination_path)} (not found in package)`)
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Routed through sync_ai_file rather than cpSync: templates/workflows/ci.yml is a mapped
|
|
74
|
-
// file, so a byte copy would hand the consumer the template's own action pins. The pins
|
|
75
|
-
// have to be resolved from .github/workflows at write time (joshuafolkken/kit#747).
|
|
76
|
-
const did_change = sync_ai_file(source_path, destination_path)
|
|
77
|
-
|
|
78
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${path.basename(destination_path)}`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function sync_workspace_yaml(
|
|
82
|
-
template_path: string,
|
|
83
|
-
destination_path: string,
|
|
84
|
-
is_force = false,
|
|
85
|
-
): boolean {
|
|
86
|
-
const template = readFileSync(template_path, 'utf8')
|
|
87
|
-
const existing =
|
|
88
|
-
!is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
|
|
89
|
-
const merged = init_logic.merge_workspace_yaml(existing, template)
|
|
90
|
-
|
|
91
|
-
return file_content.write_text_if_changed(destination_path, merged)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function sync_ai_copy_file(filename: string, is_force: boolean): void {
|
|
95
|
-
if (filename === WORKSPACE_YAML) {
|
|
96
|
-
const did_change = sync_workspace_yaml(
|
|
97
|
-
path.join(PACKAGE_DIR, WORKSPACE_YAML),
|
|
98
|
-
path.join(PROJECT_ROOT, WORKSPACE_YAML),
|
|
99
|
-
is_force,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${WORKSPACE_YAML}`)
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
sync_file(filename)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// The reason this directory was not synced, or nothing when it was. Both halves answer the same
|
|
111
|
-
// question — what stops the copy — so the caller has one line to print either way.
|
|
112
|
-
function directory_sync_failure(
|
|
113
|
-
directory_name: string,
|
|
114
|
-
on_copy: (did_change: boolean) => void,
|
|
115
|
-
): string | undefined {
|
|
116
|
-
const source_path = path.join(PACKAGE_DIR, directory_name)
|
|
117
|
-
const destination_path = path.join(PROJECT_ROOT, directory_name)
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
directory_copy_blocker(source_path, destination_path) ??
|
|
121
|
-
copy_directory_failure(source_path, destination_path, on_copy)
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function sync_directory(directory_name: string): void {
|
|
126
|
-
const failure = directory_sync_failure(directory_name, (did_change) => {
|
|
127
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${directory_name}/`)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (failure !== undefined) console.warn(` ⚠ skipped ${directory_name}/ (${failure})`)
|
|
131
|
-
}
|
|
25
|
+
const BASIC_PRETTIER_CONFIG = 'prettier.config.mjs'
|
|
132
26
|
|
|
133
27
|
function did_migrate_prettierrc(destination_path: string): boolean {
|
|
134
28
|
const legacy_path = path.join(path.dirname(destination_path), '.prettierrc')
|
|
@@ -171,6 +65,20 @@ function sync_prettier_config(destination_path: string): void {
|
|
|
171
65
|
write_merged_prettier_config(destination_path)
|
|
172
66
|
}
|
|
173
67
|
|
|
68
|
+
// A basic project's prettier config written before joshuafolkken/kit#2829 still imports the static
|
|
69
|
+
// preset path; it is moved onto the basic one with the rule `josh init` applies.
|
|
70
|
+
function sync_basic_prettier_config(destination_path: string): void {
|
|
71
|
+
if (!existsSync(destination_path)) return
|
|
72
|
+
|
|
73
|
+
const existing = readFileSync(destination_path, 'utf8')
|
|
74
|
+
const did_change = file_content.write_text_if_changed(
|
|
75
|
+
destination_path,
|
|
76
|
+
basic_path_migration.migrate_basic_paths(existing),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${BASIC_PRETTIER_CONFIG}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
174
82
|
function sync_playwright_config(destination_path: string): void {
|
|
175
83
|
if (!existsSync(destination_path)) return
|
|
176
84
|
|
|
@@ -220,70 +128,6 @@ function sync_sonar_with_template(name_with_owner: string | undefined, is_force
|
|
|
220
128
|
console.info(` ✔ synced ${destination}`)
|
|
221
129
|
}
|
|
222
130
|
|
|
223
|
-
// A consumer's stale copy of a distributed skill is removed once it still matches the shipment and
|
|
224
|
-
// kept with a warning once it does not (joshuafolkken/kit#1879). Plugin skills compare against the
|
|
225
|
-
// package source; a retired skill compares against the frozen manifest (joshuafolkken/kit#1990). The
|
|
226
|
-
// note distinguishes the two, and `absent` — the consumer never had the copy — prints nothing.
|
|
227
|
-
function report_migration_result(result: MigrationResult): void {
|
|
228
|
-
if (result.action === 'absent') return
|
|
229
|
-
|
|
230
|
-
if (result.action === 'removed') {
|
|
231
|
-
console.info(
|
|
232
|
-
` ✔ removed ${result.directory}/ (${skill_migration.removed_note(result.source)})`,
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.warn(` ⚠ kept ${result.directory}/ (${skill_migration.kept_note(result.source)})`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function migrate_removed_skills(): void {
|
|
242
|
-
const plugin = skill_migration.migrate_removed_skill_directories(PACKAGE_DIR, PROJECT_ROOT)
|
|
243
|
-
const retired = skill_migration.migrate_manifest_skills(PROJECT_ROOT, REMOVED_SKILL_MANIFEST)
|
|
244
|
-
|
|
245
|
-
for (const result of [...plugin, ...retired]) report_migration_result(result)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// CLAUDE.md is not byte-copied (joshuafolkken/kit#1878): a consumer's file is one @import of kit's
|
|
249
|
-
// published rules plus the project's own additions below it. Ensure the import line is present
|
|
250
|
-
// without ever disturbing those additions — so this ignores --force, which would otherwise mean
|
|
251
|
-
// discarding a consumer's content.
|
|
252
|
-
function sync_claude_md(destination_path: string): void {
|
|
253
|
-
const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
|
|
254
|
-
const ensured = init_logic.ensure_claude_md_import(existing)
|
|
255
|
-
|
|
256
|
-
if (ensured === existing) {
|
|
257
|
-
console.info(` ✔ unchanged ${CLAUDE_MD_FILENAME}`)
|
|
258
|
-
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
263
|
-
writeFileSync(destination_path, ensured)
|
|
264
|
-
console.info(` ✔ synced ${CLAUDE_MD_FILENAME}`)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function sync_ai_copy_all(is_force: boolean): void {
|
|
268
|
-
console.info('AI files:')
|
|
269
|
-
|
|
270
|
-
sync_claude_md(path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME))
|
|
271
|
-
|
|
272
|
-
for (const filename of init_logic.get_ai_copy_files()) {
|
|
273
|
-
sync_ai_copy_file(filename, is_force)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const { src, dest } of init_logic.get_ai_copy_file_mappings()) {
|
|
277
|
-
sync_file_mapping(path.join(PACKAGE_DIR, src), path.join(PROJECT_ROOT, dest))
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
for (const directory_name of init_logic.get_ai_copy_directories()) {
|
|
281
|
-
sync_directory(directory_name)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
migrate_removed_skills()
|
|
285
|
-
}
|
|
286
|
-
|
|
287
131
|
function sync_config_files(): void {
|
|
288
132
|
sync_configs.sync_npmrc(path.join(PROJECT_ROOT, '.npmrc'))
|
|
289
133
|
sync_configs.sync_gitignore(path.join(PROJECT_ROOT, '.gitignore'))
|
|
@@ -376,7 +220,7 @@ function report_repository_settings(name_with_owner: string | undefined): void {
|
|
|
376
220
|
}
|
|
377
221
|
|
|
378
222
|
function sync_project_artifacts(is_force: boolean): void {
|
|
379
|
-
sync_ai_copy_all(is_force)
|
|
223
|
+
sync_ai_files.sync_ai_copy_all(is_force)
|
|
380
224
|
sync_prettier_config(path.join(PROJECT_ROOT, 'prettier.config.js'))
|
|
381
225
|
sync_playwright_config(path.join(PROJECT_ROOT, 'playwright.config.ts'))
|
|
382
226
|
sync_deploy_vps(path.join(PROJECT_ROOT, '.github/workflows/deploy-vps.yml'))
|
|
@@ -394,29 +238,47 @@ function sync_project_artifacts(is_force: boolean): void {
|
|
|
394
238
|
report_repository_settings(name_with_owner)
|
|
395
239
|
}
|
|
396
240
|
|
|
241
|
+
// A basic project gets what `josh init` gives that profile and nothing more: its AI file set and the
|
|
242
|
+
// move of its prettier preset path. The full toolchain's configs, Sonar, package.json migrations and
|
|
243
|
+
// repository settings are never written into it (joshuafolkken/kit#2827).
|
|
244
|
+
function sync_basic_artifacts(is_force: boolean): void {
|
|
245
|
+
console.info('profile: basic (package.json josh.profile)\n')
|
|
246
|
+
sync_ai_files.sync_ai_copy_all(is_force, project_profile.inspect_project(PROJECT_ROOT, 'basic'))
|
|
247
|
+
sync_basic_prettier_config(path.join(PROJECT_ROOT, BASIC_PRETTIER_CONFIG))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sync_full_artifacts(is_force: boolean): void {
|
|
251
|
+
sync_project_artifacts(is_force)
|
|
252
|
+
plugin_install_hint_module.report_plugin_install_hint()
|
|
253
|
+
}
|
|
254
|
+
|
|
397
255
|
// Checked before anything is written, never per file: the damage is the whole run, and a partial
|
|
398
256
|
// sync that stopped halfway would leave the source repository in a state neither `git checkout` nor
|
|
399
257
|
// a re-run describes (joshuafolkken/kit#868).
|
|
258
|
+
//
|
|
259
|
+
// Only a recorded basic profile switches the file set; an unrecorded one stays full, as every
|
|
260
|
+
// project initialized before the profile was recorded was set up.
|
|
400
261
|
function main(): void {
|
|
401
262
|
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
402
263
|
|
|
403
264
|
const is_force = process.argv.includes('--force')
|
|
404
265
|
|
|
405
266
|
console.info('\n🔄 Syncing @joshuafolkken/kit AI files\n')
|
|
406
|
-
|
|
407
|
-
|
|
267
|
+
if (project_checks.is_basic(PROJECT_ROOT)) sync_basic_artifacts(is_force)
|
|
268
|
+
else sync_full_artifacts(is_force)
|
|
408
269
|
console.info('\n✅ Done.\n')
|
|
409
270
|
}
|
|
410
271
|
|
|
411
272
|
if (process.argv[1] === fileURLToPath(import.meta.url)) main()
|
|
412
273
|
|
|
413
274
|
const sync = {
|
|
414
|
-
should_skip_hook_file,
|
|
415
|
-
sync_file_mapping,
|
|
416
|
-
sync_ai_file,
|
|
417
|
-
sync_workspace_yaml,
|
|
418
|
-
sync_claude_md,
|
|
275
|
+
should_skip_hook_file: sync_ai_files.should_skip_hook_file,
|
|
276
|
+
sync_file_mapping: sync_ai_files.sync_file_mapping,
|
|
277
|
+
sync_ai_file: sync_ai_files.sync_ai_file,
|
|
278
|
+
sync_workspace_yaml: sync_ai_files.sync_workspace_yaml,
|
|
279
|
+
sync_claude_md: sync_ai_files.sync_claude_md,
|
|
419
280
|
sync_prettier_config,
|
|
281
|
+
sync_basic_prettier_config,
|
|
420
282
|
sync_playwright_config,
|
|
421
283
|
sync_deploy_vps,
|
|
422
284
|
sync_package_manager_version,
|
|
@@ -427,4 +289,6 @@ const sync = {
|
|
|
427
289
|
|
|
428
290
|
// `sync` is the entry point; `sync_directory` and `main` are exported for their own unit tests, the
|
|
429
291
|
// way `git-pr-checks.ts` exports the helpers beside its namespace.
|
|
292
|
+
const { sync_directory } = sync_ai_files
|
|
293
|
+
|
|
430
294
|
export { main, sync, sync_directory }
|
|
@@ -85,7 +85,7 @@ type FixtureFile = readonly [string, string]
|
|
|
85
85
|
|
|
86
86
|
const TOOLCHAIN_FILES: ReadonlyArray<FixtureFile> = [
|
|
87
87
|
['.gitignore', 'node_modules\n.*cache\n.tsbuildinfo\npnpm-lock.yaml\n'],
|
|
88
|
-
['.prettierignore', '*.json\npnpm-lock.yaml\n'],
|
|
88
|
+
['.prettierignore', '*.json\npnpm-lock.yaml\n_tmp_*\n'],
|
|
89
89
|
['.prettierrc', json_file({ semi: false, singleQuote: true, useTabs: true })],
|
|
90
90
|
[
|
|
91
91
|
'cspell.config.yaml',
|
|
@@ -199,7 +199,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
|
|
|
199
199
|
'scripts/hooks/prompt-hooks.test.ts',
|
|
200
200
|
'scripts/hooks/reserved-run.test.ts',
|
|
201
201
|
'scripts/init/claude-plugin-config.test.ts',
|
|
202
|
-
'scripts/init/init-actions.test.ts',
|
|
203
202
|
'scripts/init/init-copy-content.test.ts',
|
|
204
203
|
'scripts/init/init-logic-deploy-vps.test.ts',
|
|
205
204
|
'scripts/init/init-logic-development-engines.test.ts',
|
|
@@ -62,7 +62,7 @@ function source_paths(): ReadonlyArray<string> {
|
|
|
62
62
|
// The verdict over the current working tree. `paths` stays injectable for a direct caller; when it is
|
|
63
63
|
// omitted the injected set is read if one is in force, else the real tree.
|
|
64
64
|
function current_verdict(paths: ReadonlyArray<string> = source_paths()): Verdict {
|
|
65
|
-
return test_declared_logic.verdict_for(paths, project_checks.
|
|
65
|
+
return test_declared_logic.verdict_for(paths, project_checks.is_basic(process.cwd()))
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Put the injection back to what it was before a `with_paths` span — cleared to the live reader when
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// a clone avoided. Here only paths that are mechanically certain to touch no runtime code are exempt —
|
|
15
15
|
// the non-executable config and cosmetic-asset arms of `CLAUDE.md`'s "Non-runtime updates" exception
|
|
16
16
|
// stay `required`, and a person declares that exception in the Step 0 work summary. Under-exempting
|
|
17
|
-
// costs one declared exception; over-exempting ships an untested runtime change.
|
|
17
|
+
// costs one declared exception; over-exempting ships an untested runtime change. The one widening is
|
|
18
|
+
// the basic profile's manual check below, for files no runner kit has could test.
|
|
18
19
|
|
|
19
20
|
type Verdict = 'required' | 'exempt' | 'satisfied'
|
|
20
21
|
|
|
@@ -27,7 +28,30 @@ const TEST_SUFFIXES: ReadonlyArray<string> = ['.e2e.ts', '.test.ts']
|
|
|
27
28
|
const EXEMPT_PATHS: ReadonlyArray<string> = ['.editorconfig']
|
|
28
29
|
const EXEMPT_PREFIXES: ReadonlyArray<string> = ['.idea/', '.vscode/', 'prompts/']
|
|
29
30
|
const EXEMPT_SUFFIXES: ReadonlyArray<string> = ['.md']
|
|
30
|
-
const
|
|
31
|
+
const BASIC_VISUAL_SUFFIXES: ReadonlyArray<string> = ['.html', '.css']
|
|
32
|
+
|
|
33
|
+
// What stays `required` under the basic profile (joshuafolkken/kit#2820): the languages kit's own
|
|
34
|
+
// runner can test, and the data and config formats a JS/TS test can load. Any other file — HTML/CSS, or
|
|
35
|
+
// the source of a project kit does not run, such as Lua — has no test kit could execute, so `required`
|
|
36
|
+
// there would be unsatisfiable; it is confirmed by hand instead. The kept side is enumerated rather than
|
|
37
|
+
// the manual one, because that side is closed.
|
|
38
|
+
const BASIC_REQUIRED_SUFFIXES: ReadonlyArray<string> = [
|
|
39
|
+
'.ts',
|
|
40
|
+
'.tsx',
|
|
41
|
+
'.mts',
|
|
42
|
+
'.cts',
|
|
43
|
+
'.js',
|
|
44
|
+
'.jsx',
|
|
45
|
+
'.mjs',
|
|
46
|
+
'.cjs',
|
|
47
|
+
'.svelte',
|
|
48
|
+
'.json',
|
|
49
|
+
'.jsonc',
|
|
50
|
+
'.json5',
|
|
51
|
+
'.yaml',
|
|
52
|
+
'.yml',
|
|
53
|
+
'.toml',
|
|
54
|
+
]
|
|
31
55
|
|
|
32
56
|
function normalize(paths: ReadonlyArray<string>): Array<string> {
|
|
33
57
|
return paths.map((path) => path.trim()).filter((path) => path !== '')
|
|
@@ -37,12 +61,21 @@ function is_test_file(path: string): boolean {
|
|
|
37
61
|
return TEST_SUFFIXES.some((suffix) => path.endsWith(suffix))
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
// Matched case-insensitively, so `Main.TS` is not mistaken for a language kit cannot test.
|
|
65
|
+
function is_basic_manual(path: string, is_basic: boolean): boolean {
|
|
66
|
+
const lower = path.toLowerCase()
|
|
67
|
+
|
|
68
|
+
return is_basic && BASIC_REQUIRED_SUFFIXES.every((suffix) => !lower.endsWith(suffix))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function is_basic_visual(path: string): boolean {
|
|
72
|
+
const lower = path.toLowerCase()
|
|
73
|
+
|
|
74
|
+
return BASIC_VISUAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))
|
|
42
75
|
}
|
|
43
76
|
|
|
44
|
-
function is_exempt(path: string,
|
|
45
|
-
if (EXEMPT_PATHS.includes(path) ||
|
|
77
|
+
function is_exempt(path: string, is_basic = false): boolean {
|
|
78
|
+
if (EXEMPT_PATHS.includes(path) || is_basic_manual(path, is_basic)) return true
|
|
46
79
|
|
|
47
80
|
return (
|
|
48
81
|
EXEMPT_SUFFIXES.some((suffix) => path.endsWith(suffix)) ||
|
|
@@ -52,31 +85,37 @@ function is_exempt(path: string, is_static = false): boolean {
|
|
|
52
85
|
|
|
53
86
|
// A runtime file is one that is neither a test nor exempt. Its presence with no test file beside it is
|
|
54
87
|
// the whole of what `required` reports.
|
|
55
|
-
function is_runtime(path: string,
|
|
56
|
-
return !is_test_file(path) && !is_exempt(path,
|
|
88
|
+
function is_runtime(path: string, is_basic = false): boolean {
|
|
89
|
+
return !is_test_file(path) && !is_exempt(path, is_basic)
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
// **A test file changed → `satisfied`, whatever else did.** A runtime file with no test beside it →
|
|
60
93
|
// `required`. Everything left — an all-exempt change, and the empty diff — is `exempt`, because there
|
|
61
94
|
// is no runtime file the change failed to test.
|
|
62
|
-
function verdict_for(paths: ReadonlyArray<string>,
|
|
95
|
+
function verdict_for(paths: ReadonlyArray<string>, is_basic = false): Verdict {
|
|
63
96
|
const changed = normalize(paths)
|
|
64
97
|
|
|
65
98
|
if (changed.some((path) => is_test_file(path))) return 'satisfied'
|
|
66
|
-
if (changed.some((path) => is_runtime(path,
|
|
99
|
+
if (changed.some((path) => is_runtime(path, is_basic))) return 'required'
|
|
67
100
|
|
|
68
101
|
return 'exempt'
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
// The runtime files with no test beside them — the `required` detail line, so the answer says which
|
|
72
105
|
// files it is about rather than only that it refused.
|
|
73
|
-
function runtime_files(paths: ReadonlyArray<string>,
|
|
74
|
-
return normalize(paths).filter((path) => is_runtime(path,
|
|
106
|
+
function runtime_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
|
|
107
|
+
return normalize(paths).filter((path) => is_runtime(path, is_basic))
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
// The paths that made the change exempt — the `exempt` detail line.
|
|
78
|
-
function exempt_files(paths: ReadonlyArray<string>,
|
|
79
|
-
return normalize(paths).filter((path) => is_exempt(path,
|
|
111
|
+
function exempt_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
|
|
112
|
+
return normalize(paths).filter((path) => is_exempt(path, is_basic))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The paths exempt only because the basic profile has no runner for them — the ones a person still has
|
|
116
|
+
// to confirm by hand, as opposed to docs and editor settings, which nobody checks.
|
|
117
|
+
function manual_check_files(paths: ReadonlyArray<string>, is_basic: boolean): Array<string> {
|
|
118
|
+
return normalize(paths).filter((path) => is_exempt(path, is_basic) && !is_exempt(path))
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
const test_declared_logic = {
|
|
@@ -85,9 +124,11 @@ const test_declared_logic = {
|
|
|
85
124
|
EXEMPT_SUFFIXES,
|
|
86
125
|
TEST_SUFFIXES,
|
|
87
126
|
exempt_files,
|
|
127
|
+
is_basic_visual,
|
|
88
128
|
is_exempt,
|
|
89
129
|
is_runtime,
|
|
90
130
|
is_test_file,
|
|
131
|
+
manual_check_files,
|
|
91
132
|
runtime_files,
|
|
92
133
|
verdict_for,
|
|
93
134
|
}
|
|
@@ -76,36 +76,48 @@ function typed_runtime_file(path: string): string {
|
|
|
76
76
|
return `${test_type_logic.test_type_for(path)} — ${path}`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const BROWSER_CHECK = 'confirm the rendered page in a browser'
|
|
80
|
+
const RUN_CHECK = 'run the changed code by hand and report what you observed'
|
|
81
|
+
|
|
82
|
+
// The manual confirmation a basic-profile exemption owes, one instruction per kind of file it covers:
|
|
83
|
+
// HTML/CSS is looked at in a browser, a source kit cannot test is run by hand (joshuafolkken/kit#2820).
|
|
84
|
+
function manual_instruction(paths: ReadonlyArray<string>, is_basic: boolean): string {
|
|
85
|
+
const manual = test_declared_logic.manual_check_files(paths, is_basic)
|
|
86
|
+
const has_visual = manual.some((path) => test_declared_logic.is_basic_visual(path))
|
|
87
|
+
const has_source = manual.some((path) => !test_declared_logic.is_basic_visual(path))
|
|
88
|
+
const steps = [has_visual ? BROWSER_CHECK : '', has_source ? RUN_CHECK : ''].filter(Boolean)
|
|
89
|
+
|
|
90
|
+
return steps.length === 0 ? '' : ` — ${steps.join('; ')}`
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
// The word is stdout so a caller can read it alone; the reason — which files, and why — is stderr.
|
|
80
|
-
function exempt_detail(paths: ReadonlyArray<string>,
|
|
81
|
-
const exempt = test_declared_logic.exempt_files(paths,
|
|
82
|
-
const has_visual = is_static && exempt.some((path) => /\.(?:html|css)$/u.test(path))
|
|
83
|
-
const instruction = has_visual ? ' — confirm the rendered page in a browser' : ''
|
|
94
|
+
function exempt_detail(paths: ReadonlyArray<string>, is_basic: boolean): string {
|
|
95
|
+
const exempt = test_declared_logic.exempt_files(paths, is_basic)
|
|
84
96
|
|
|
85
|
-
return `exempt paths: ${exempt.join(', ')}${
|
|
97
|
+
return `exempt paths: ${exempt.join(', ')}${manual_instruction(paths, is_basic)}`
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
function detail_for(verdict: Verdict, paths: ReadonlyArray<string>,
|
|
100
|
+
function detail_for(verdict: Verdict, paths: ReadonlyArray<string>, is_basic = false): string {
|
|
89
101
|
if (verdict === 'required') {
|
|
90
102
|
const typed = test_declared_logic
|
|
91
|
-
.runtime_files(paths,
|
|
103
|
+
.runtime_files(paths, is_basic)
|
|
92
104
|
.map((path) => typed_runtime_file(path))
|
|
93
105
|
|
|
94
106
|
return `runtime files with no test: ${typed.join(', ')}`
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
if (verdict === 'exempt') return exempt_detail(paths,
|
|
109
|
+
if (verdict === 'exempt') return exempt_detail(paths, is_basic)
|
|
98
110
|
|
|
99
111
|
return 'a test file changed'
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
function report(
|
|
103
115
|
paths: ReadonlyArray<string>,
|
|
104
|
-
|
|
116
|
+
is_basic: boolean = project_checks.is_basic(process.cwd()),
|
|
105
117
|
): { detail: string; verdict: Verdict } {
|
|
106
|
-
const verdict = test_declared_logic.verdict_for(paths,
|
|
118
|
+
const verdict = test_declared_logic.verdict_for(paths, is_basic)
|
|
107
119
|
|
|
108
|
-
return { detail: detail_for(verdict, paths,
|
|
120
|
+
return { detail: detail_for(verdict, paths, is_basic), verdict }
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
function run(): void {
|
|
@@ -21,8 +21,8 @@ const NODE_MODULES = 'node_modules'
|
|
|
21
21
|
// - **`vitest` absent is the young project.** `josh init` installs no vitest and writes no
|
|
22
22
|
// `test:unit` script, so a freshly-bootstrapped project always lands here — which is the case the
|
|
23
23
|
// skip was written for, and it keeps it.
|
|
24
|
-
// - **`vitest` present is the
|
|
25
|
-
// files there is a broken state — a mis-scoped glob, or a deleted suite — and fails. A
|
|
24
|
+
// - **`vitest` present is the full project's declaration that it runs unit tests.** Zero matching
|
|
25
|
+
// files there is a broken state — a mis-scoped glob, or a deleted suite — and fails. A basic
|
|
26
26
|
// project may carry vitest as an unused dependency without declaring a unit suite; zero files
|
|
27
27
|
// there are reported as skipped, never as a test pass.
|
|
28
28
|
//
|
|
@@ -47,19 +47,19 @@ type GuardAction = 'run' | typeof SKIP_ACTION | typeof SKIP_NO_TESTS_ACTION | ty
|
|
|
47
47
|
// body, and without this a gate that skipped the whole unit suite printed the same five lines as one
|
|
48
48
|
// that ran it.
|
|
49
49
|
const SKIP_REASON = 'vitest is not installed'
|
|
50
|
-
const SKIP_NO_TESTS_REASON = 'no unit test files were found in this
|
|
50
|
+
const SKIP_NO_TESTS_REASON = 'no unit test files were found in this basic project'
|
|
51
51
|
const FAIL_REASON =
|
|
52
52
|
'vitest is installed but no *.{test,spec}.{ts,js} test file was found — refusing to report a unit check that ran nothing'
|
|
53
53
|
|
|
54
54
|
function resolve_guard_action(
|
|
55
55
|
is_installed: boolean,
|
|
56
56
|
has_tests: boolean,
|
|
57
|
-
|
|
57
|
+
is_basic = false,
|
|
58
58
|
): GuardAction {
|
|
59
59
|
if (!is_installed) return SKIP_ACTION
|
|
60
60
|
if (has_tests) return 'run'
|
|
61
61
|
|
|
62
|
-
return
|
|
62
|
+
return is_basic ? SKIP_NO_TESTS_ACTION : FAIL_ACTION
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function is_vitest_installed(project_directory: string): boolean {
|
|
@@ -160,7 +160,7 @@ async function run_guarded_vitest(
|
|
|
160
160
|
const action = resolve_guard_action(
|
|
161
161
|
is_installed,
|
|
162
162
|
has_tests,
|
|
163
|
-
project_checks.
|
|
163
|
+
project_checks.is_basic(project_directory),
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
if (action !== 'run') return report_no_run(action, command_label)
|
|
@@ -45,7 +45,17 @@ const CHAIN_PATTERN = /&&|\|\||;/u
|
|
|
45
45
|
// invisible here. Erring this way is the cheaper error for every caller: a missed reading costs
|
|
46
46
|
// nothing beyond the reading, while a fragment read as a command answers about work the shell never
|
|
47
47
|
// did, on the shape this repository writes constantly.
|
|
48
|
-
|
|
48
|
+
//
|
|
49
|
+
// **Escapes and heredoc bodies are text too** (joshuafolkken/kit#2841). A backslash outside single
|
|
50
|
+
// quotes makes the next character literal, so `it\'s` opens nothing and a `"` closes only at an
|
|
51
|
+
// unescaped `"`; inside `'…'` a backslash is an ordinary character, so `'C:\'` still closes. A heredoc
|
|
52
|
+
// body is not shell syntax at all — an `it's` there is prose — so it is removed before the quotes are
|
|
53
|
+
// paired, keeping the rest of its marker line (`cat <<EOF > f && git add f`) where the shell reads it.
|
|
54
|
+
// Without both, a stray quote paired with one much later and erased every command between them.
|
|
55
|
+
const QUOTED_SPAN_PATTERN = /\\[\s\S]|'[^']*'|"(?:\\[\s\S]|[^"\\])*"/gu
|
|
56
|
+
const HEREDOC_MARKER = /(?<!<)<<-?\s*(['"]?)(\w+)\1/u
|
|
57
|
+
const HEREDOC_DELIMITER_GROUP = 2
|
|
58
|
+
const LINE_BREAK = '\n'
|
|
49
59
|
// `set -o pipefail` tells the shell to report the pipeline with the first failing command's status,
|
|
50
60
|
// so nothing in it is discarded. The flag letters are loose because `set -eo pipefail` and
|
|
51
61
|
// `set -euo pipefail` are the same instruction, and the `set` word is required so that a path or a
|
|
@@ -146,13 +156,37 @@ function leading_word(command: string): string {
|
|
|
146
156
|
//
|
|
147
157
|
// **A pipeline under `set -o pipefail` discards nothing**, so it is not this function's business —
|
|
148
158
|
// answering otherwise would name the very form a caller is told to use instead.
|
|
159
|
+
// A line outside a heredoc body is kept with its marker blanked, and a marker opens a body whose
|
|
160
|
+
// delimiter is returned; inside one, every line is dropped through the line that is the delimiter alone.
|
|
161
|
+
function open_heredoc(line: string, kept: Array<string>): string | undefined {
|
|
162
|
+
kept.push(line.replace(HEREDOC_MARKER, ' '))
|
|
163
|
+
|
|
164
|
+
return HEREDOC_MARKER.exec(line)?.[HEREDOC_DELIMITER_GROUP]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function close_heredoc(line: string, delimiter: string): string | undefined {
|
|
168
|
+
return line.trim() === delimiter ? undefined : delimiter
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function without_heredoc_bodies(command: string): string {
|
|
172
|
+
const kept: Array<string> = []
|
|
173
|
+
const body: { delimiter?: string | undefined } = {}
|
|
174
|
+
|
|
175
|
+
for (const line of command.split(LINE_BREAK)) {
|
|
176
|
+
body.delimiter =
|
|
177
|
+
body.delimiter === undefined ? open_heredoc(line, kept) : close_heredoc(line, body.delimiter)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return kept.join(LINE_BREAK)
|
|
181
|
+
}
|
|
182
|
+
|
|
149
183
|
// Quoted text replaced by a space, which is what a reader walking into the middle of a chain has to
|
|
150
184
|
// do before it can trust an operator it finds there. Exported because `time-writes.ts` needs the same
|
|
151
185
|
// removal for a different reason — a quoted `sed` script holds `|` and slashes that read as a
|
|
152
186
|
// pipeline and as paths — and two spellings of "remove the quotes" would disagree the first time one
|
|
153
187
|
// of them learned about a quoting form the other did not.
|
|
154
188
|
function unquoted(command: string): string {
|
|
155
|
-
return command.replaceAll(QUOTED_SPAN_PATTERN, ' ')
|
|
189
|
+
return without_heredoc_bodies(command).replaceAll(QUOTED_SPAN_PATTERN, ' ')
|
|
156
190
|
}
|
|
157
191
|
|
|
158
192
|
function discarded_commands(command: string): Array<string> {
|
|
@@ -5,6 +5,7 @@ import { git_commit } from '../scripts/git/git-commit'
|
|
|
5
5
|
import { git_error } from '../scripts/git/git-error'
|
|
6
6
|
import { git_issue, type IssueInfo } from '../scripts/git/git-issue'
|
|
7
7
|
import { git_pr } from '../scripts/git/git-pr'
|
|
8
|
+
import { git_preflight } from '../scripts/git/git-preflight'
|
|
8
9
|
import { git_prompt, type WorkflowConfirmations } from '../scripts/git/git-prompt'
|
|
9
10
|
import { git_push } from '../scripts/git/git-push'
|
|
10
11
|
import { git_staging } from '../scripts/git/git-staging'
|
|
@@ -206,6 +207,14 @@ async function execute_workflow(
|
|
|
206
207
|
const { cli_issue_input, is_auto_mode, commit_suffix } = parse_positionals(positionals)
|
|
207
208
|
const is_non_interactive = is_non_interactive_mode(is_auto_mode, values)
|
|
208
209
|
|
|
210
|
+
// Unattended, nobody can fix a precondition between stages — so all are asked before staging.
|
|
211
|
+
if (is_non_interactive) {
|
|
212
|
+
await git_preflight.check({
|
|
213
|
+
cli_input: cli_issue_input,
|
|
214
|
+
will_open_pr: values['skip-pr'] !== true,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
await git_staging.check_and_confirm_staging(values.yes === true)
|
|
210
219
|
|
|
211
220
|
const issue_info = await prepare_issue_info(cli_issue_input, is_non_interactive)
|
|
@@ -235,6 +244,7 @@ try {
|
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
const git_workflow = {
|
|
247
|
+
execute_workflow,
|
|
238
248
|
parse_positionals,
|
|
239
249
|
get_workflow_confirmations,
|
|
240
250
|
prepare_issue_info,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Project instructions
|
|
2
|
+
|
|
3
|
+
- 既存のファイルと利用者の設定を尊重し、変更する前に内容を確認する。
|
|
4
|
+
- 変更には検証を付け、実行した結果を報告する。
|
|
5
|
+
- HTML、CSS、JavaScript のファイルがある場合は Prettier で整形する。
|
|
6
|
+
- 画面に見える変更をした場合はブラウザーで表示と操作を確認する。
|
|
7
|
+
- JavaScript または TypeScript の動作を変更した場合は、その動作を確認する自動テストを追加する。
|
|
8
|
+
- Git や GitHub を使う場合だけ、それぞれのワークフローを利用する。
|