@joshuafolkken/kit 1.1383.0 → 1.1435.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/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -3,6 +3,7 @@ import { json_value } from '#scripts/lib/json-value'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { init_logic } from './init-logic'
|
|
5
5
|
import type { ProjectShape } from './project-profile'
|
|
6
|
+
import { workflow_mark } from './workflow-mark'
|
|
6
7
|
|
|
7
8
|
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
8
9
|
const record_schema = z.record(z.string(), z.unknown())
|
|
@@ -16,13 +17,17 @@ function initial_manifest(): string {
|
|
|
16
17
|
return json_format.format_json({ private: true })
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function with_recorded_profile(
|
|
20
|
+
function with_recorded_profile(
|
|
21
|
+
content: string,
|
|
22
|
+
profile: ProjectShape['profile'],
|
|
23
|
+
has_workflow = false,
|
|
24
|
+
): string {
|
|
20
25
|
const parsed: unknown = JSON.parse(content)
|
|
21
26
|
const manifest = record_schema.parse(parsed)
|
|
22
27
|
const existing = manifest['josh']
|
|
23
28
|
const josh = json_value.is_record(existing) ? existing : {}
|
|
24
29
|
|
|
25
|
-
manifest['josh'] = { ...josh, profile }
|
|
30
|
+
manifest['josh'] = { ...josh, profile, ...workflow_mark.recorded_fields(has_workflow) }
|
|
26
31
|
|
|
27
32
|
return json_format.format_json(manifest)
|
|
28
33
|
}
|
|
@@ -32,7 +37,7 @@ function merge_basic_manifest(
|
|
|
32
37
|
shape: ProjectShape,
|
|
33
38
|
versions: BasicVersions,
|
|
34
39
|
): string {
|
|
35
|
-
const with_profile = with_recorded_profile(content, shape.profile)
|
|
40
|
+
const with_profile = with_recorded_profile(content, shape.profile, shape.has_workflow)
|
|
36
41
|
const with_script = init_logic.merge_package_scripts(with_profile, {
|
|
37
42
|
preinstall: init_logic.SAFE_CHAIN_CMD,
|
|
38
43
|
josh: 'josh',
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parse_jsonc } from '#scripts/config-merge/parse-jsonc'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { init_logic_json_merge } from './init-logic-json-merge'
|
|
4
|
+
import { kit_development_versions } from './kit-development-versions'
|
|
5
|
+
|
|
6
|
+
// The tools a consumer has to resolve itself, as opposed to the ones only kit's own ESLint config
|
|
7
|
+
// imports. Those are optional kit peers, which pnpm does not install unlisted, so a consumer still
|
|
8
|
+
// needs them — `josh init` writes them, `josh sync` leaves them to it (#3711). A tool belongs here
|
|
9
|
+
// when the consumer runs its bin from the project root (eslint, prettier, tsc, playwright, cspell,
|
|
10
|
+
// lefthook), when a config the consumer owns imports it (`playwright.config.ts` → @playwright/test,
|
|
11
|
+
// @types/node), or when it is named by a string Prettier resolves from the consumer (the preset's
|
|
12
|
+
// plugins). `josh init` and `josh sync` both read this one list, so a tool added here reaches a fresh
|
|
13
|
+
// project and an already-initialized one alike.
|
|
14
|
+
const CONSUMER_RESOLVED_TOOLS = [
|
|
15
|
+
'eslint',
|
|
16
|
+
'prettier',
|
|
17
|
+
'typescript',
|
|
18
|
+
'@playwright/test',
|
|
19
|
+
'@ianvs/prettier-plugin-sort-imports',
|
|
20
|
+
'prettier-plugin-svelte',
|
|
21
|
+
'prettier-plugin-tailwindcss',
|
|
22
|
+
'cspell',
|
|
23
|
+
'@types/node',
|
|
24
|
+
]
|
|
25
|
+
// Only a project with Git gets `lefthook.yml`, so only that one needs the bin.
|
|
26
|
+
const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
|
|
27
|
+
const DECLARING_FIELDS = [
|
|
28
|
+
'dependencies',
|
|
29
|
+
'devDependencies',
|
|
30
|
+
'peerDependencies',
|
|
31
|
+
'optionalDependencies',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function consumer_resolved_names(has_git: boolean): ReadonlyArray<string> {
|
|
35
|
+
return has_git
|
|
36
|
+
? [...CONSUMER_RESOLVED_TOOLS, LEFTHOOK_DEVELOPMENT_DEPENDENCY]
|
|
37
|
+
: CONSUMER_RESOLVED_TOOLS
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// What `josh init` adds: every kit peer — the ones only kit's own configs import included — and the
|
|
41
|
+
// tools above. Versions mirror kit's own devDependencies, so the consumer runs the toolchain kit is
|
|
42
|
+
// verified with.
|
|
43
|
+
function get_toolchain_development_dependencies(has_git: boolean): Record<string, string> {
|
|
44
|
+
return kit_development_versions.versions_of([
|
|
45
|
+
...kit_development_versions.peer_names(),
|
|
46
|
+
...consumer_resolved_names(has_git),
|
|
47
|
+
])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function is_declared(manifest: Record<string, unknown>, name: string): boolean {
|
|
51
|
+
return DECLARING_FIELDS.some((field) => {
|
|
52
|
+
const dependencies = manifest[field]
|
|
53
|
+
|
|
54
|
+
return json_value.is_record(dependencies) && Object.hasOwn(dependencies, name)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// What `josh sync` adds to an already-initialized consumer: only the tools above, and only the ones
|
|
59
|
+
// the manifest declares nowhere — a tool kept under `dependencies` is present, not missing.
|
|
60
|
+
function merge_consumer_resolved_development_deps(content: string, has_git: boolean): string {
|
|
61
|
+
const manifest = parse_jsonc(content)
|
|
62
|
+
const missing = consumer_resolved_names(has_git).filter((name) => !is_declared(manifest, name))
|
|
63
|
+
|
|
64
|
+
return init_logic_json_merge.merge_development_dependencies(
|
|
65
|
+
content,
|
|
66
|
+
kit_development_versions.versions_of(missing),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const init_logic_toolchain = {
|
|
71
|
+
consumer_resolved_names,
|
|
72
|
+
get_toolchain_development_dependencies,
|
|
73
|
+
merge_consumer_resolved_development_deps,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { init_logic_toolchain }
|
|
@@ -7,6 +7,7 @@ import { init_logic_json_merge } from './init-logic-json-merge'
|
|
|
7
7
|
import { init_logic_secretlint } from './init-logic-secretlint'
|
|
8
8
|
import { init_logic_sonar } from './init-logic-sonar'
|
|
9
9
|
import { init_logic_templates } from './init-logic-templates'
|
|
10
|
+
import { init_logic_toolchain } from './init-logic-toolchain'
|
|
10
11
|
import { init_logic_workspace } from './init-logic-workspace'
|
|
11
12
|
import { init_logic_yaml_merge } from './init-logic-yaml-merge'
|
|
12
13
|
|
|
@@ -417,6 +418,7 @@ const init_logic = {
|
|
|
417
418
|
...init_logic_sonar,
|
|
418
419
|
...init_logic_json_merge,
|
|
419
420
|
...init_logic_secretlint,
|
|
421
|
+
...init_logic_toolchain,
|
|
420
422
|
...init_logic_yaml_merge,
|
|
421
423
|
...init_logic_deploy_vps,
|
|
422
424
|
...distributed_paths,
|
package/scripts/init/init.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { init_file_action } from './init-file-action'
|
|
|
23
23
|
import { init_install } from './init-install'
|
|
24
24
|
import { init_logic } from './init-logic'
|
|
25
25
|
import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
|
|
26
|
-
import { kit_development_versions } from './kit-development-versions'
|
|
27
26
|
import { kit_setup_state } from './kit-setup-state'
|
|
28
27
|
import { package_manager_pin } from './package-manager-pin'
|
|
29
28
|
import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
|
|
@@ -71,27 +70,8 @@ function get_kit_self_dependency(): Record<string, string> {
|
|
|
71
70
|
return { [KIT_PACKAGE_NAME]: version }
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
// Each config `init` generates runs a CLI or imports types the consumer has to resolve itself
|
|
75
|
-
//
|
|
76
|
-
// Prettier, its preset plugins and @playwright/test come in as peers. Without them the first
|
|
77
|
-
// `josh gate` after `pnpm install` fails.
|
|
78
|
-
const TOOL_DEVELOPMENT_DEPENDENCIES = ['cspell', '@types/node']
|
|
79
|
-
const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
|
|
80
|
-
|
|
81
|
-
function tool_dependency_names(has_git: boolean): ReadonlyArray<string> {
|
|
82
|
-
return has_git
|
|
83
|
-
? [...TOOL_DEVELOPMENT_DEPENDENCIES, LEFTHOOK_DEVELOPMENT_DEPENDENCY]
|
|
84
|
-
: TOOL_DEVELOPMENT_DEPENDENCIES
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Versions mirror kit's own devDependencies, so the consumer runs the toolchain kit is verified with.
|
|
88
|
-
function get_toolchain_development_dependencies(has_git: boolean): Record<string, string> {
|
|
89
|
-
return kit_development_versions.versions_of([
|
|
90
|
-
...kit_development_versions.peer_names(),
|
|
91
|
-
...tool_dependency_names(has_git),
|
|
92
|
-
])
|
|
93
|
-
}
|
|
94
|
-
|
|
73
|
+
// Each config `init` generates runs a CLI or imports types the consumer has to resolve itself, so
|
|
74
|
+
// the toolchain is added here: without it the first `josh gate` after `pnpm install` fails.
|
|
95
75
|
function apply_dependency_merges(content: string, has_git = true): string {
|
|
96
76
|
const migrated = init_logic.strip_managed_postinstall(content)
|
|
97
77
|
const suggested = init_logic.get_suggested_scripts_for_content(migrated)
|
|
@@ -103,7 +83,7 @@ function apply_dependency_merges(content: string, has_git = true): string {
|
|
|
103
83
|
)
|
|
104
84
|
const with_toolchain = init_logic.merge_development_dependencies(
|
|
105
85
|
merged,
|
|
106
|
-
get_toolchain_development_dependencies(has_git),
|
|
86
|
+
init_logic.get_toolchain_development_dependencies(has_git),
|
|
107
87
|
)
|
|
108
88
|
const with_secretlint = init_logic.merge_secretlint_development_deps(with_toolchain)
|
|
109
89
|
|
|
@@ -147,6 +127,7 @@ function merged_manifest(existing: string, shape: ProjectShape): string {
|
|
|
147
127
|
return init_basic.with_recorded_profile(
|
|
148
128
|
apply_package_json_merges(existing, shape.has_git),
|
|
149
129
|
shape.profile,
|
|
130
|
+
shape.has_workflow,
|
|
150
131
|
)
|
|
151
132
|
}
|
|
152
133
|
|
|
@@ -325,6 +306,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
325
306
|
const init = {
|
|
326
307
|
copy_ai_file: init_ai_copy.copy_ai_file,
|
|
327
308
|
apply_package_json_merges,
|
|
309
|
+
merged_manifest,
|
|
328
310
|
install_lefthook,
|
|
329
311
|
resolve_kit_package_manager,
|
|
330
312
|
}
|
|
@@ -4,6 +4,8 @@ import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
|
4
4
|
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
5
5
|
import { json_value } from '#scripts/lib/json-value'
|
|
6
6
|
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
7
|
+
import { kit_written_paths } from './kit-written-paths'
|
|
8
|
+
import { workflow_mark } from './workflow-mark'
|
|
7
9
|
|
|
8
10
|
type ProjectProfile = 'basic' | 'full'
|
|
9
11
|
|
|
@@ -17,6 +19,8 @@ interface ProjectShape extends ProfileResult {
|
|
|
17
19
|
has_typescript: boolean
|
|
18
20
|
has_git: boolean
|
|
19
21
|
has_github: boolean
|
|
22
|
+
// Set up by `josh start` for the GitHub Issue workflow (`workflow-mark.ts`).
|
|
23
|
+
has_workflow: boolean
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', '.svelte-kit'])
|
|
@@ -136,18 +140,27 @@ function scan_directory(directory: string, pending: Array<string>): Array<string
|
|
|
136
140
|
.map((entry) => path.join(directory, entry.name)),
|
|
137
141
|
)
|
|
138
142
|
|
|
139
|
-
return entries.filter((entry) => entry.isFile()).map((entry) => path.
|
|
143
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path.join(directory, entry.name))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A file kit wrote says nothing about the project: counted, the `.pnpmfile.mjs` of the first
|
|
147
|
+
// `josh init` would read as the user's Web file on the second and bring Prettier in
|
|
148
|
+
// (joshuafolkken/kit#3713).
|
|
149
|
+
function is_project_file(root: string, file: string): boolean {
|
|
150
|
+
const relative_path = path.relative(root, file).split(path.sep).join(path.posix.sep)
|
|
151
|
+
|
|
152
|
+
return !kit_written_paths.is_kit_written(relative_path)
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
function file_extensions(root: string): Array<string> {
|
|
143
156
|
const pending = [root]
|
|
144
|
-
const
|
|
157
|
+
const files: Array<string> = []
|
|
145
158
|
|
|
146
159
|
for (const directory of pending) {
|
|
147
|
-
|
|
160
|
+
files.push(...scan_directory(directory, pending))
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
return
|
|
163
|
+
return files.filter((file) => is_project_file(root, file)).map((file) => path.extname(file))
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
function scan_files(root: string): { has_web: boolean; has_typescript: boolean } {
|
|
@@ -164,17 +177,33 @@ function has_github_remote(root: string): boolean {
|
|
|
164
177
|
return url !== undefined && repo_origin.parse_origin_url(url) !== undefined
|
|
165
178
|
}
|
|
166
179
|
|
|
180
|
+
function has_git(root: string): boolean {
|
|
181
|
+
return existsSync(path.join(root, '.git'))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The run is asked first, so a `josh start` whose setup has not written `package.json` yet reads it.
|
|
185
|
+
function has_workflow(root: string): boolean {
|
|
186
|
+
return workflow_mark.is_requested() || workflow_mark.is_recorded(read_manifest(root))
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
function inspect_project(root: string, requested?: ProjectProfile): ProjectShape {
|
|
168
190
|
const profile = resolve_profile(root, requested)
|
|
169
191
|
const files = scan_files(root)
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
return {
|
|
192
|
+
const is_git = has_git(root)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...profile,
|
|
196
|
+
...files,
|
|
197
|
+
has_git: is_git,
|
|
198
|
+
has_github: is_git && has_github_remote(root),
|
|
199
|
+
has_workflow: has_workflow(root),
|
|
200
|
+
}
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
const project_profile = {
|
|
176
204
|
parse_profile,
|
|
177
205
|
resolve_profile,
|
|
206
|
+
has_git,
|
|
178
207
|
inspect_project,
|
|
179
208
|
requested_profile,
|
|
180
209
|
with_legacy_profile_names,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
2
2
|
import { git_location_environment } from '#scripts/git/git-location-environment'
|
|
3
|
-
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
|
+
import { git_spawn_sync, type GitSyncOptions } from '#scripts/git/git-spawn-sync'
|
|
4
4
|
import { COMMAND_TIMEOUT_MS, SUITE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
5
5
|
import { execaSync } from 'execa'
|
|
6
6
|
import type { Visibility } from './start-plan'
|
|
@@ -16,6 +16,8 @@ import type { Visibility } from './start-plan'
|
|
|
16
16
|
// project's own commit and push hooks, which may run its whole suite, so it takes the suite budget:
|
|
17
17
|
// long enough for that, short enough that a push that never answers ends the run.
|
|
18
18
|
const SUCCESS_EXIT_CODE = 0
|
|
19
|
+
const GIT_DIRECTORY = '.git'
|
|
20
|
+
const IDENTITY_VARIABLES = ['GIT_AUTHOR_IDENT', 'GIT_COMMITTER_IDENT']
|
|
19
21
|
|
|
20
22
|
function root_environment(): Record<string, string | undefined> {
|
|
21
23
|
return git_location_environment.location_free_environment()
|
|
@@ -29,6 +31,29 @@ function git_read(args: ReadonlyArray<string>, root: string): string | undefined
|
|
|
29
31
|
return git_spawn_sync.read(args, { cwd: root, env: root_environment() })
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
// Asked of git itself, so the answer is the one its commit would reach: config, the environment and
|
|
35
|
+
// the identity git can detect all count (#3754).
|
|
36
|
+
//
|
|
37
|
+
// **Only an answer from git counts as a missing identity.** A git that could not be started or never
|
|
38
|
+
// answered is a different failure, which the first step that runs git reports as what it is.
|
|
39
|
+
function is_identity_refused(variable: string, options: GitSyncOptions): boolean {
|
|
40
|
+
const { exit_code } = git_spawn_sync.run(['var', variable], options)
|
|
41
|
+
|
|
42
|
+
return exit_code !== undefined && exit_code !== SUCCESS_EXIT_CODE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A plan that creates the repository asks from where it will be: an identity given per directory
|
|
46
|
+
// (`includeIf "gitdir:…"`) is matched against the git directory, which no `cwd` supplies before
|
|
47
|
+
// `git init` has run. The path stays relative to `root` so git resolves it as its commit will — by
|
|
48
|
+
// the path the user reached the directory through as well as the physical one, which is what a
|
|
49
|
+
// `gitdir:` under a symlinked parent is written against.
|
|
50
|
+
function lacks_git_identity(root: string, is_new_repository: boolean): boolean {
|
|
51
|
+
const location = is_new_repository ? { GIT_DIR: GIT_DIRECTORY } : {}
|
|
52
|
+
const options = { cwd: root, env: { ...root_environment(), ...location } }
|
|
53
|
+
|
|
54
|
+
return IDENTITY_VARIABLES.some((variable) => is_identity_refused(variable, options))
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
function git_run(args: ReadonlyArray<string>, root: string): void {
|
|
33
58
|
const options = { cwd: root, env: root_environment(), timeout: SUITE_TIMEOUT_MS }
|
|
34
59
|
const { exit_code } = git_spawn_sync.run(args, { ...options, stdio: 'inherit' })
|
|
@@ -86,6 +111,7 @@ const start_exec = {
|
|
|
86
111
|
git_succeeds,
|
|
87
112
|
git_read,
|
|
88
113
|
git_run,
|
|
114
|
+
lacks_git_identity,
|
|
89
115
|
is_gh_installed,
|
|
90
116
|
is_gh_signed_in,
|
|
91
117
|
create_github_repository,
|
|
@@ -9,6 +9,7 @@ interface StartOptions {
|
|
|
9
9
|
is_github: boolean
|
|
10
10
|
visibility: Visibility
|
|
11
11
|
init_command: string | undefined
|
|
12
|
+
push_preflight_command: string | undefined
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels' | 'setup_pr'
|
|
@@ -53,9 +54,13 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
|
|
|
53
54
|
setup_pr: 'Open an Issue and a pull request that add the kit setup to main',
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// The GitHub write steps `--yes` alone never consents to.
|
|
57
|
+
// The GitHub write steps `--yes` alone never consents to. Each one pushes — main, or the setup branch —
|
|
58
|
+
// so they are also the steps a caller's push preflight guards (#3702).
|
|
57
59
|
const GITHUB_WRITE_STEPS: ReadonlySet<StartStep> = new Set(['repository', 'setup_pr'])
|
|
58
60
|
|
|
61
|
+
// The steps that create a commit, which git refuses without an author identity (#3754).
|
|
62
|
+
const COMMIT_STEPS: ReadonlySet<StartStep> = new Set(['commit', 'setup_pr'])
|
|
63
|
+
|
|
59
64
|
function refused(refusal: string): StepPlan {
|
|
60
65
|
return { steps: [], refusal }
|
|
61
66
|
}
|
|
@@ -107,6 +112,14 @@ function plan_steps(state: GitState): StepPlan {
|
|
|
107
112
|
return { steps: FULL_STEPS, refusal: undefined }
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
function has_push(steps: ReadonlyArray<StartStep>): boolean {
|
|
116
|
+
return steps.some((step) => GITHUB_WRITE_STEPS.has(step))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function has_commit(steps: ReadonlyArray<StartStep>): boolean {
|
|
120
|
+
return steps.some((step) => COMMIT_STEPS.has(step))
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
|
|
111
124
|
if (options.is_yes) return { value: 'unattended', refusal: undefined }
|
|
112
125
|
if (is_tty) return { value: 'interactive', refusal: undefined }
|
|
@@ -126,9 +139,7 @@ function github_consent(
|
|
|
126
139
|
options: StartOptions,
|
|
127
140
|
interaction: Interaction,
|
|
128
141
|
): Verdict<Consent> {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!is_github_write || options.is_github) return { value: 'granted', refusal: undefined }
|
|
142
|
+
if (!has_push(steps) || options.is_github) return { value: 'granted', refusal: undefined }
|
|
132
143
|
if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
|
|
133
144
|
|
|
134
145
|
return {
|
|
@@ -158,6 +169,17 @@ function progress_report(
|
|
|
158
169
|
return `josh start stopped at: ${labels[failed]}\nCompleted: ${done}\nCause: ${cause}`
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
// What a run prints when the caller's push preflight fails: no step has run yet, so nothing changed.
|
|
173
|
+
function preflight_report(command: string, cause: string): string {
|
|
174
|
+
return `josh start stopped before its first step: the push preflight (${command}) failed. Nothing was changed.\nCause: ${cause}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// What a run prints when the scanner the pre-push audit needs could not be put in place: every push
|
|
178
|
+
// the plan makes would be refused by that hook, so it stops while nothing has changed (#3755).
|
|
179
|
+
function scanner_report(cause: string): string {
|
|
180
|
+
return `josh start stopped before its first step: osv-scanner, which the pre-push audit needs, is not installed and could not be fetched. Nothing was changed.\nCause: ${cause}\nInstall osv-scanner (https://google.github.io/osv-scanner/) or restore the network, then run josh start again.`
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
function plan_summary(
|
|
162
184
|
steps: ReadonlyArray<StartStep>,
|
|
163
185
|
labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
|
|
@@ -169,9 +191,13 @@ function plan_summary(
|
|
|
169
191
|
|
|
170
192
|
const start_plan = {
|
|
171
193
|
plan_steps,
|
|
194
|
+
has_push,
|
|
195
|
+
has_commit,
|
|
172
196
|
interaction_of,
|
|
173
197
|
github_consent,
|
|
174
198
|
progress_report,
|
|
199
|
+
preflight_report,
|
|
200
|
+
scanner_report,
|
|
175
201
|
plan_summary,
|
|
176
202
|
step_labels,
|
|
177
203
|
STEP_LABELS,
|
|
@@ -2,6 +2,8 @@ import path from 'node:path'
|
|
|
2
2
|
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
3
|
import { error_text } from '#scripts/lib/error-message'
|
|
4
4
|
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
5
|
+
import { security_audit } from '#scripts/security/security-audit'
|
|
6
|
+
import { security_audit_provision } from '#scripts/security/security-audit-provision'
|
|
5
7
|
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
6
8
|
import { parseCommandString } from 'execa'
|
|
7
9
|
import { main as init_main } from './init'
|
|
@@ -12,12 +14,14 @@ import type { ProjectProfile, ProjectShape } from './project-profile'
|
|
|
12
14
|
import { start_exec } from './start-exec'
|
|
13
15
|
import { start_plan, type GitState, type StartStep, type Visibility } from './start-plan'
|
|
14
16
|
import { start_setup_pr } from './start-setup-pr'
|
|
17
|
+
import { workflow_mark } from './workflow-mark'
|
|
15
18
|
|
|
16
19
|
interface StepContext {
|
|
17
20
|
root: string
|
|
18
21
|
profile: ProjectProfile
|
|
19
22
|
visibility: Visibility
|
|
20
23
|
init_command: string | undefined
|
|
24
|
+
push_preflight_command: string | undefined
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
interface RunContext extends StepContext {
|
|
@@ -28,6 +32,12 @@ const GH_INSTALL_HINT =
|
|
|
28
32
|
'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, run gh auth login, then run josh start again. Nothing was changed.'
|
|
29
33
|
const GH_AUTH_HINT =
|
|
30
34
|
'GitHub CLI is not signed in. Run gh auth login, then run josh start again. Nothing was changed.'
|
|
35
|
+
const GIT_IDENTITY_HINT =
|
|
36
|
+
'Git has no author identity, which the commit josh start creates needs. Run git config --global user.name "Your Name" and git config --global user.email "you@example.com", then run josh start again. Nothing was changed.'
|
|
37
|
+
// Printed before the scanner is looked for: a first run downloads tens of megabytes, and a terminal
|
|
38
|
+
// that says nothing for minutes reads as a hang the user interrupts (#3755).
|
|
39
|
+
const SCANNER_NOTICE =
|
|
40
|
+
'Checking for osv-scanner, which the pre-push audit needs. A first run downloads it, which can take a few minutes on a slow connection.'
|
|
31
41
|
|
|
32
42
|
const { git_succeeds, git_read, run_local } = start_exec
|
|
33
43
|
|
|
@@ -62,6 +72,17 @@ function github_cli_refusal(root: string): string | undefined {
|
|
|
62
72
|
return undefined
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
// A machine Git was just installed on has no author identity, and the commit that needs one comes
|
|
76
|
+
// after the repository and kit's files are written. It is asked for before the first write, and only
|
|
77
|
+
// when the plan commits — a re-run with nothing left to commit is not stopped by it (#3754).
|
|
78
|
+
function git_identity_refusal(steps: ReadonlyArray<StartStep>, root: string): string | undefined {
|
|
79
|
+
if (!start_plan.has_commit(steps)) return undefined
|
|
80
|
+
|
|
81
|
+
return start_exec.lacks_git_identity(root, steps.includes('git_init'))
|
|
82
|
+
? GIT_IDENTITY_HINT
|
|
83
|
+
: undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
// `josh init` refuses to run inside the kit source repository by printing and setting the exit code
|
|
66
87
|
// rather than throwing, so its step would pass and the later steps would commit and publish the
|
|
67
88
|
// source repository. The same check is raised before the first write instead.
|
|
@@ -69,6 +90,13 @@ function self_run_refusal(root: string): string | undefined {
|
|
|
69
90
|
return self_sync_guard.self_sync_refusal(PACKAGE_DIR, root)
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
// A caller's command line runs without a shell, split the way execa splits one.
|
|
94
|
+
function run_command_line(command_line: string, extra: ReadonlyArray<string>, root: string): void {
|
|
95
|
+
const [command = '', ...args] = parseCommandString(command_line)
|
|
96
|
+
|
|
97
|
+
run_local(command, [...args, ...extra], root)
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
function create_git_repository(context: StepContext): void {
|
|
73
101
|
initial_commit.create_repository(context.root)
|
|
74
102
|
}
|
|
@@ -77,16 +105,17 @@ function create_git_repository(context: StepContext): void {
|
|
|
77
105
|
// A toolkit that layers its files over kit's (`josh-app init`) names its own setup instead, so every
|
|
78
106
|
// other step and guard stays here rather than in a copy of this command (#2872). That command runs
|
|
79
107
|
// without a shell, is given the same profile, and a non-zero exit stops the run before the commit.
|
|
108
|
+
// Either one is told the project is set up for the GitHub Issue workflow (`workflow-mark.ts`).
|
|
80
109
|
function initialize(context: StepContext): void {
|
|
110
|
+
workflow_mark.request()
|
|
111
|
+
|
|
81
112
|
if (context.init_command === undefined) {
|
|
82
113
|
init_main(['--profile', context.profile])
|
|
83
114
|
|
|
84
115
|
return
|
|
85
116
|
}
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
run_local(command, [...args, '--profile', context.profile], context.root)
|
|
118
|
+
run_command_line(context.init_command, ['--profile', context.profile], context.root)
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
function commit_all(context: StepContext): void {
|
|
@@ -122,6 +151,47 @@ function baseline_of(
|
|
|
122
151
|
return start_setup_pr.changed_paths(context.root)
|
|
123
152
|
}
|
|
124
153
|
|
|
154
|
+
// A toolkit's own pre-push hook can need something kit knows nothing about (app-kit's needs a Docker
|
|
155
|
+
// daemon), and a push it refuses lands after `gh repo create` has made the repository. So the caller
|
|
156
|
+
// names a check that runs only when the plan pushes, before the first step: a run that does not push
|
|
157
|
+
// is never stopped by it, and one it stops has written nothing (#3702).
|
|
158
|
+
function run_push_preflight(steps: ReadonlyArray<StartStep>, context: StepContext): void {
|
|
159
|
+
const command_line = context.push_preflight_command
|
|
160
|
+
|
|
161
|
+
if (command_line === undefined || !start_plan.has_push(steps)) return
|
|
162
|
+
|
|
163
|
+
console.info(`\nPush preflight: ${command_line}`)
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
run_command_line(command_line, [], context.root)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const cause = error_text.message_of(error)
|
|
169
|
+
|
|
170
|
+
throw new Error(start_plan.preflight_report(command_line, cause), { cause: error })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The `full` profile installs kit's pre-push hook, whose audit refuses a push without osv-scanner. The
|
|
175
|
+
// scanner is otherwise fetched at an agent's session start, which a first `josh start` from a terminal
|
|
176
|
+
// has not had, so it is fetched here — forced, because a person asked for this run. A scanner that
|
|
177
|
+
// still cannot be found stops the run before the first step rather than at the push (#3755).
|
|
178
|
+
async function ensure_audit_scanner(
|
|
179
|
+
steps: ReadonlyArray<StartStep>,
|
|
180
|
+
context: StepContext,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
if (context.profile !== 'full' || !start_plan.has_push(steps)) return
|
|
183
|
+
|
|
184
|
+
const { platform, arch } = process
|
|
185
|
+
|
|
186
|
+
console.info(`\n${SCANNER_NOTICE}`)
|
|
187
|
+
const outcome = await security_audit_provision.report(context.root, platform, arch, true)
|
|
188
|
+
|
|
189
|
+
console.info(outcome)
|
|
190
|
+
if (security_audit.resolve_scanner(context.root, platform) !== undefined) return
|
|
191
|
+
|
|
192
|
+
throw new Error(start_plan.scanner_report(outcome))
|
|
193
|
+
}
|
|
194
|
+
|
|
125
195
|
type StepAction = (context: RunContext) => void | Promise<void>
|
|
126
196
|
|
|
127
197
|
const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
|
|
@@ -150,6 +220,9 @@ async function run_step(
|
|
|
150
220
|
|
|
151
221
|
async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): Promise<void> {
|
|
152
222
|
const labels = start_plan.step_labels(context.init_command)
|
|
223
|
+
|
|
224
|
+
run_push_preflight(steps, context)
|
|
225
|
+
await ensure_audit_scanner(steps, context)
|
|
153
226
|
const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
|
|
154
227
|
|
|
155
228
|
for (const [index, step] of steps.entries()) {
|
|
@@ -164,6 +237,7 @@ async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext):
|
|
|
164
237
|
const start_steps = {
|
|
165
238
|
read_git_state,
|
|
166
239
|
github_cli_refusal,
|
|
240
|
+
git_identity_refusal,
|
|
167
241
|
self_run_refusal,
|
|
168
242
|
run_steps,
|
|
169
243
|
}
|
package/scripts/init/start.ts
CHANGED
|
@@ -15,8 +15,9 @@ import { start_steps } from './start-steps'
|
|
|
15
15
|
const ARGUMENT_START_INDEX = 2
|
|
16
16
|
const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
|
|
17
17
|
const START_USAGE =
|
|
18
|
-
'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>]'
|
|
18
|
+
'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]'
|
|
19
19
|
const INIT_COMMAND_FLAG = '--init-command'
|
|
20
|
+
const PUSH_PREFLIGHT_COMMAND_FLAG = '--push-preflight-command'
|
|
20
21
|
const YES_FLAG = '--yes'
|
|
21
22
|
const GITHUB_FLAG = '--github'
|
|
22
23
|
const PUBLIC_FLAG = '--public'
|
|
@@ -28,34 +29,38 @@ interface Prepared {
|
|
|
28
29
|
choices: Choices
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
interface
|
|
32
|
-
|
|
32
|
+
interface CommandSplit {
|
|
33
|
+
command: string | undefined
|
|
33
34
|
rest: ReadonlyArray<string>
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
// A value that is blank or another flag means the command itself was left out, which would otherwise
|
|
37
|
-
// swallow the next switch or hand
|
|
38
|
+
// swallow the next switch or hand its step nothing to run.
|
|
38
39
|
function is_command_value(value: string | undefined): value is string {
|
|
39
40
|
return value !== undefined && value.trim() !== '' && !value.startsWith('-')
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// The command arrives as the one argument after its flag and is removed together with it.
|
|
43
|
-
function
|
|
44
|
-
const index = args.indexOf(
|
|
44
|
+
function split_command(args: ReadonlyArray<string>, flag: string): CommandSplit {
|
|
45
|
+
const index = args.indexOf(flag)
|
|
45
46
|
|
|
46
|
-
if (index === -1) return {
|
|
47
|
+
if (index === -1) return { command: undefined, rest: args }
|
|
47
48
|
|
|
48
49
|
const value = args[index + 1]
|
|
49
50
|
|
|
50
51
|
if (!is_command_value(value)) throw new Error(`Usage: ${START_USAGE}`)
|
|
51
52
|
|
|
52
|
-
return {
|
|
53
|
+
return { command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// The switches and the
|
|
56
|
+
// The switches and the caller's commands are peeled off first, so what remains is exactly the
|
|
56
57
|
// `--profile` pair `josh init` accepts — one parser for the profile in both commands.
|
|
57
58
|
function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
58
|
-
const
|
|
59
|
+
const init = split_command(args, INIT_COMMAND_FLAG)
|
|
60
|
+
const { command: push_preflight_command, rest } = split_command(
|
|
61
|
+
init.rest,
|
|
62
|
+
PUSH_PREFLIGHT_COMMAND_FLAG,
|
|
63
|
+
)
|
|
59
64
|
const profile_args = rest.filter((argument) => !SWITCHES.has(argument))
|
|
60
65
|
|
|
61
66
|
return {
|
|
@@ -63,7 +68,8 @@ function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
|
63
68
|
is_yes: rest.includes(YES_FLAG),
|
|
64
69
|
is_github: rest.includes(GITHUB_FLAG),
|
|
65
70
|
visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
|
|
66
|
-
init_command,
|
|
71
|
+
init_command: init.command,
|
|
72
|
+
push_preflight_command,
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -100,6 +106,7 @@ function prepare(args: ReadonlyArray<string>, is_tty: boolean, root: string): Pr
|
|
|
100
106
|
const consent: Consent = settle(start_plan.github_consent(plan.steps, options, interaction))
|
|
101
107
|
|
|
102
108
|
stop_on(start_steps.github_cli_refusal(root))
|
|
109
|
+
stop_on(start_steps.git_identity_refusal(plan.steps, root))
|
|
103
110
|
|
|
104
111
|
return {
|
|
105
112
|
steps: plan.steps,
|
|
@@ -112,12 +119,13 @@ async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void>
|
|
|
112
119
|
const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
|
|
113
120
|
|
|
114
121
|
const { root, options } = choices
|
|
115
|
-
const { visibility, init_command } = options
|
|
122
|
+
const { visibility, init_command, push_preflight_command } = options
|
|
116
123
|
|
|
117
124
|
console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
|
|
118
125
|
const profile = await resolve_profile(choices, interaction)
|
|
126
|
+
const context = { root, profile, visibility, init_command, push_preflight_command }
|
|
119
127
|
|
|
120
|
-
await start_steps.run_steps(steps,
|
|
128
|
+
await start_steps.run_steps(steps, context)
|
|
121
129
|
console.info(`\n${READY_MESSAGE}`)
|
|
122
130
|
}
|
|
123
131
|
|