@joshuafolkken/kit 1.1387.0 → 1.1453.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-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- 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 +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- 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/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -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-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- 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 +24 -0
- 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 +175 -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 +18 -4
- 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-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -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 +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- 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-ai.ts +11 -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 +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- 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/lane-session-broker.ts +363 -0
- 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/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- 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 +129 -53
- 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 +49 -3
- 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 +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- 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 +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -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 +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- 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/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 +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- 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/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
3
|
+
import { init_dry_run } from './init-dry-run'
|
|
4
|
+
import type { ProjectProfile } from './project-profile'
|
|
5
|
+
import type { StartStep } from './start-plan'
|
|
6
|
+
import type { Choices } from './start-prompt'
|
|
7
|
+
import { start_setup_pr } from './start-setup-pr'
|
|
8
|
+
import { workflow_mark } from './workflow-mark'
|
|
9
|
+
|
|
10
|
+
// What `josh start --dry-run` adds to the plan `josh start` prints: what each GitHub step would
|
|
11
|
+
// create, and what its setup step would write. Nothing here creates, pushes or writes (#3830).
|
|
12
|
+
|
|
13
|
+
const TITLE = 'josh start --dry-run — nothing was written. Its setup step would:'
|
|
14
|
+
const NONE = '(none)'
|
|
15
|
+
const UNREAD_LABELS =
|
|
16
|
+
'add the workflow labels it lacks — the repository’s labels could not be read to name them'
|
|
17
|
+
|
|
18
|
+
// The labels are read from the repository when it already exists — the same listing the real step
|
|
19
|
+
// compares against — and a repository this run would create has none.
|
|
20
|
+
function label_line(steps: ReadonlyArray<StartStep>): string {
|
|
21
|
+
const repo = steps.includes('repository') ? undefined : '{owner}/{repo}'
|
|
22
|
+
const labels = repository_labels.planned_labels(repo)
|
|
23
|
+
|
|
24
|
+
if (labels === undefined) return UNREAD_LABELS
|
|
25
|
+
|
|
26
|
+
return `add the labels it lacks: ${labels.length === 0 ? NONE : labels.join(', ')}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function github_lines({ steps, root, options }: Choices): Array<string> {
|
|
30
|
+
const lines: Readonly<Partial<Record<StartStep, string | undefined>>> = {
|
|
31
|
+
repository: `create the ${options.visibility} repository ${path.basename(root)} as origin, and push main`,
|
|
32
|
+
labels: steps.includes('labels') ? label_line(steps) : undefined,
|
|
33
|
+
setup_pr: `open the Issue "${start_setup_pr.SETUP_TITLE}" (or reuse the open one), push a branch named after it, and open a pull request into main`,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return steps.map((step) => lines[step]).filter((line) => line !== undefined)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function github_report(choices: Choices): string {
|
|
40
|
+
const lines = github_lines(choices).map((line) => ` ${line}`)
|
|
41
|
+
|
|
42
|
+
return ['GitHub:', ...(lines.length === 0 ? [` ${NONE}`] : lines)].join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A caller's setup command writes files kit cannot name, so it is named rather than simulated. Kit's
|
|
46
|
+
// own setup runs on a scratch copy, told it sets up the GitHub workflow as the real step tells it,
|
|
47
|
+
// with the repository the run would have created by then.
|
|
48
|
+
function setup_report({ steps, root, options, shape }: Choices): string | undefined {
|
|
49
|
+
if (!steps.includes('initialize')) return undefined
|
|
50
|
+
const profile: ProjectProfile = options.profile ?? shape.profile
|
|
51
|
+
|
|
52
|
+
if (options.init_command !== undefined) {
|
|
53
|
+
return `The setup step would run ${options.init_command} --profile ${profile}, which is not simulated.`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
workflow_mark.request()
|
|
57
|
+
const has_git = shape.has_git || steps.includes('git_init')
|
|
58
|
+
|
|
59
|
+
return init_dry_run.plan_report({ root, profile, has_git, is_install: true }, TITLE)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function report(choices: Choices): string {
|
|
63
|
+
const setup = setup_report(choices)
|
|
64
|
+
const sections = [github_report(choices), ...(setup === undefined ? [] : [setup])]
|
|
65
|
+
|
|
66
|
+
return `\n${sections.join('\n\n')}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const start_dry_run = { report }
|
|
70
|
+
export { start_dry_run }
|
|
@@ -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,
|
|
@@ -7,6 +7,7 @@ interface StartOptions {
|
|
|
7
7
|
profile: ProjectProfile | undefined
|
|
8
8
|
is_yes: boolean
|
|
9
9
|
is_github: boolean
|
|
10
|
+
is_dry_run: boolean
|
|
10
11
|
visibility: Visibility
|
|
11
12
|
init_command: string | undefined
|
|
12
13
|
push_preflight_command: string | undefined
|
|
@@ -58,6 +59,9 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
|
|
|
58
59
|
// so they are also the steps a caller's push preflight guards (#3702).
|
|
59
60
|
const GITHUB_WRITE_STEPS: ReadonlySet<StartStep> = new Set(['repository', 'setup_pr'])
|
|
60
61
|
|
|
62
|
+
// The steps that create a commit, which git refuses without an author identity (#3754).
|
|
63
|
+
const COMMIT_STEPS: ReadonlySet<StartStep> = new Set(['commit', 'setup_pr'])
|
|
64
|
+
|
|
61
65
|
function refused(refusal: string): StepPlan {
|
|
62
66
|
return { steps: [], refusal }
|
|
63
67
|
}
|
|
@@ -113,8 +117,13 @@ function has_push(steps: ReadonlyArray<StartStep>): boolean {
|
|
|
113
117
|
return steps.some((step) => GITHUB_WRITE_STEPS.has(step))
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
function has_commit(steps: ReadonlyArray<StartStep>): boolean {
|
|
121
|
+
return steps.some((step) => COMMIT_STEPS.has(step))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A dry run asks nothing and changes nothing, so it needs neither a terminal nor `--yes`.
|
|
116
125
|
function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
|
|
117
|
-
if (options.is_yes) return { value: 'unattended', refusal: undefined }
|
|
126
|
+
if (options.is_yes || options.is_dry_run) return { value: 'unattended', refusal: undefined }
|
|
118
127
|
if (is_tty) return { value: 'interactive', refusal: undefined }
|
|
119
128
|
|
|
120
129
|
return {
|
|
@@ -126,13 +135,16 @@ function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interac
|
|
|
126
135
|
|
|
127
136
|
// `--yes` accepts the detected defaults, never the creation of a public-facing resource: an
|
|
128
137
|
// unattended run creates a repository, pushes, or opens the setup Issue and pull request only when
|
|
129
|
-
// `--github` asks for it by name.
|
|
138
|
+
// `--github` asks for it by name. A dry run writes to GitHub no more than it writes anywhere else, so
|
|
139
|
+
// it lists the GitHub steps without that consent.
|
|
130
140
|
function github_consent(
|
|
131
141
|
steps: ReadonlyArray<StartStep>,
|
|
132
142
|
options: StartOptions,
|
|
133
143
|
interaction: Interaction,
|
|
134
144
|
): Verdict<Consent> {
|
|
135
|
-
|
|
145
|
+
const is_consented = options.is_github || options.is_dry_run
|
|
146
|
+
|
|
147
|
+
if (is_consented || !has_push(steps)) return { value: 'granted', refusal: undefined }
|
|
136
148
|
if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
|
|
137
149
|
|
|
138
150
|
return {
|
|
@@ -167,6 +179,12 @@ function preflight_report(command: string, cause: string): string {
|
|
|
167
179
|
return `josh start stopped before its first step: the push preflight (${command}) failed. Nothing was changed.\nCause: ${cause}`
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
// What a run prints when the scanner the pre-push audit needs could not be put in place: every push
|
|
183
|
+
// the plan makes would be refused by that hook, so it stops while nothing has changed (#3755).
|
|
184
|
+
function scanner_report(cause: string): string {
|
|
185
|
+
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.`
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
function plan_summary(
|
|
171
189
|
steps: ReadonlyArray<StartStep>,
|
|
172
190
|
labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
|
|
@@ -179,10 +197,12 @@ function plan_summary(
|
|
|
179
197
|
const start_plan = {
|
|
180
198
|
plan_steps,
|
|
181
199
|
has_push,
|
|
200
|
+
has_commit,
|
|
182
201
|
interaction_of,
|
|
183
202
|
github_consent,
|
|
184
203
|
progress_report,
|
|
185
204
|
preflight_report,
|
|
205
|
+
scanner_report,
|
|
186
206
|
plan_summary,
|
|
187
207
|
step_labels,
|
|
188
208
|
STEP_LABELS,
|
|
@@ -9,28 +9,19 @@ import { IGNORE_FOR_RELEASE_LABEL } from '#scripts/issue/issue-labels'
|
|
|
9
9
|
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
10
10
|
import { kit_written_paths } from './kit-written-paths'
|
|
11
11
|
import { start_exec } from './start-exec'
|
|
12
|
+
import { working_tree } from './working-tree'
|
|
12
13
|
|
|
13
14
|
const SETUP_TITLE = `Set up ${KIT_PACKAGE_NAME}`
|
|
14
15
|
const SETUP_BODY = [
|
|
15
16
|
`Adds the files \`josh start\` wrote while setting up ${KIT_PACKAGE_NAME}, so main carries the`,
|
|
16
17
|
'rules, settings and workflows every later Issue is run with. Only the files the setup wrote are in',
|
|
17
|
-
'the pull request
|
|
18
|
+
'the pull request: `josh start` runs only on a working tree with nothing uncommitted.',
|
|
18
19
|
].join('\n')
|
|
19
20
|
const NAME_ONLY = '--name-only'
|
|
20
21
|
const NOTHING_TO_COMMIT = 'The kit setup has no changes to commit, so no pull request was opened.'
|
|
21
22
|
const OPEN_SETUP_ISSUES_QUERY = `?state=open&labels=${IGNORE_FOR_RELEASE_LABEL}&per_page=100`
|
|
22
23
|
const FIRST_SETUP_ISSUE_FILTER = `[.[] | select(.title == "${SETUP_TITLE}") | .number][0] // empty`
|
|
23
24
|
|
|
24
|
-
// Listings are read NUL-separated (`-z`), so a path is never quoted — a name a caller's initialize
|
|
25
|
-
// command wrote need not be ASCII the way kit's own are (#2872).
|
|
26
|
-
function paths_of(output: string | undefined = ''): Array<string> {
|
|
27
|
-
return output.split('\0').filter((listed) => listed.length > 0)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function listing(args: ReadonlyArray<string>, root: string): Array<string> {
|
|
31
|
-
return paths_of(start_exec.git_read([...args, '-z'], root))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
function setup_issue(issue_number: string): IssueInfo {
|
|
35
26
|
return git_issue.parse(`${SETUP_TITLE} ${issue_cite.plain(issue_number)}`)
|
|
36
27
|
}
|
|
@@ -49,19 +40,10 @@ function is_setup_branch(branch: string | undefined): boolean {
|
|
|
49
40
|
return setup_branch_number(branch) !== undefined
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
// Untracked files the ignore rules let through, plus tracked files the setup changed — two plain
|
|
53
|
-
// listings, so nothing has to be parsed out of `git status`.
|
|
54
|
-
function changed_paths(root: string): Array<string> {
|
|
55
|
-
const untracked = listing(['ls-files', '--others', '--exclude-standard'], root)
|
|
56
|
-
const modified = listing(['diff', NAME_ONLY, 'HEAD'], root)
|
|
57
|
-
|
|
58
|
-
return [...untracked, ...modified]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
43
|
// On the setup branch a failed commit left, that run's `git add` already staged its files, so they
|
|
62
44
|
// read as changed before this run's command ran — the staged ones count as setup output (#2872).
|
|
63
45
|
function carried_paths(root: string, current: string | undefined): Array<string> {
|
|
64
|
-
return is_setup_branch(current) ? listing(['diff', NAME_ONLY, '--cached'], root) : []
|
|
46
|
+
return is_setup_branch(current) ? working_tree.listing(['diff', NAME_ONLY, '--cached'], root) : []
|
|
65
47
|
}
|
|
66
48
|
|
|
67
49
|
// The Issue carries the release classification the pull request is opened with, so the PR step finds
|
|
@@ -124,13 +106,15 @@ function commit_kit_paths(paths: ReadonlyArray<string>, issue: IssueInfo, root:
|
|
|
124
106
|
|
|
125
107
|
// A caller's initialize command writes files kit cannot name, so with a `baseline` — what was already
|
|
126
108
|
// changed before that command ran — everything changed since joins kit's own files, while a change
|
|
127
|
-
// the user had made before the run still stays out (#2872).
|
|
109
|
+
// the user had made before the run still stays out (#2872). `josh start` refuses a dirty tree on
|
|
110
|
+
// `main` (#3835), so the baseline there is empty; it still keeps the two apart on the setup branch a
|
|
111
|
+
// failed commit left, where the re-run starts with that run's output uncommitted.
|
|
128
112
|
function setup_paths(
|
|
129
113
|
root: string,
|
|
130
114
|
baseline: ReadonlyArray<string> | undefined,
|
|
131
115
|
current: string | undefined,
|
|
132
116
|
): Array<string> {
|
|
133
|
-
const changed = changed_paths(root)
|
|
117
|
+
const changed = working_tree.changed_paths(root)
|
|
134
118
|
const kit_paths = kit_written_paths.select(changed)
|
|
135
119
|
|
|
136
120
|
if (baseline === undefined) return kit_paths
|
|
@@ -161,6 +145,11 @@ async function open(root: string, baseline?: ReadonlyArray<string>): Promise<voi
|
|
|
161
145
|
await git_pr.create_with_issue_info(issue)
|
|
162
146
|
}
|
|
163
147
|
|
|
164
|
-
const start_setup_pr = {
|
|
148
|
+
const start_setup_pr = {
|
|
149
|
+
SETUP_TITLE,
|
|
150
|
+
open,
|
|
151
|
+
changed_paths: working_tree.changed_paths,
|
|
152
|
+
is_setup_branch,
|
|
153
|
+
}
|
|
165
154
|
|
|
166
155
|
export { start_setup_pr }
|
|
@@ -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,6 +14,8 @@ 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'
|
|
18
|
+
import { working_tree } from './working-tree'
|
|
15
19
|
|
|
16
20
|
interface StepContext {
|
|
17
21
|
root: string
|
|
@@ -29,6 +33,12 @@ const GH_INSTALL_HINT =
|
|
|
29
33
|
'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.'
|
|
30
34
|
const GH_AUTH_HINT =
|
|
31
35
|
'GitHub CLI is not signed in. Run gh auth login, then run josh start again. Nothing was changed.'
|
|
36
|
+
const GIT_IDENTITY_HINT =
|
|
37
|
+
'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.'
|
|
38
|
+
// Printed before the scanner is looked for: a first run downloads tens of megabytes, and a terminal
|
|
39
|
+
// that says nothing for minutes reads as a hang the user interrupts (#3755).
|
|
40
|
+
const SCANNER_NOTICE =
|
|
41
|
+
'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.'
|
|
32
42
|
|
|
33
43
|
const { git_succeeds, git_read, run_local } = start_exec
|
|
34
44
|
|
|
@@ -63,6 +73,28 @@ function github_cli_refusal(root: string): string | undefined {
|
|
|
63
73
|
return undefined
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
// A machine Git was just installed on has no author identity, and the commit that needs one comes
|
|
77
|
+
// after the repository and kit's files are written. It is asked for before the first write, and only
|
|
78
|
+
// when the plan commits — a re-run with nothing left to commit is not stopped by it (#3754).
|
|
79
|
+
function git_identity_refusal(steps: ReadonlyArray<StartStep>, root: string): string | undefined {
|
|
80
|
+
if (!start_plan.has_commit(steps)) return undefined
|
|
81
|
+
|
|
82
|
+
return start_exec.lacks_git_identity(root, steps.includes('git_init'))
|
|
83
|
+
? GIT_IDENTITY_HINT
|
|
84
|
+
: undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A repository with a history receives the setup as a commit, and the user's own uncommitted change to
|
|
88
|
+
// a file kit also writes would land in it, so the tree must be clean before the first write (#3835).
|
|
89
|
+
// The setup branch a failed commit or push left checked out is the exception: what is uncommitted
|
|
90
|
+
// there is that run's own output, which the re-run resumes with. Without a commit there is nothing to
|
|
91
|
+
// keep apart — the initial commit takes every file.
|
|
92
|
+
function uncommitted_refusal(root: string, state: GitState): string | undefined {
|
|
93
|
+
if (!state.has_commits || start_setup_pr.is_setup_branch(state.branch)) return undefined
|
|
94
|
+
|
|
95
|
+
return working_tree.dirty_tree_refusal(root, 'josh start')
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
// `josh init` refuses to run inside the kit source repository by printing and setting the exit code
|
|
67
99
|
// rather than throwing, so its step would pass and the later steps would commit and publish the
|
|
68
100
|
// source repository. The same check is raised before the first write instead.
|
|
@@ -85,7 +117,10 @@ function create_git_repository(context: StepContext): void {
|
|
|
85
117
|
// A toolkit that layers its files over kit's (`josh-app init`) names its own setup instead, so every
|
|
86
118
|
// other step and guard stays here rather than in a copy of this command (#2872). That command runs
|
|
87
119
|
// without a shell, is given the same profile, and a non-zero exit stops the run before the commit.
|
|
120
|
+
// Either one is told the project is set up for the GitHub Issue workflow (`workflow-mark.ts`).
|
|
88
121
|
function initialize(context: StepContext): void {
|
|
122
|
+
workflow_mark.request()
|
|
123
|
+
|
|
89
124
|
if (context.init_command === undefined) {
|
|
90
125
|
init_main(['--profile', context.profile])
|
|
91
126
|
|
|
@@ -118,7 +153,9 @@ async function open_setup_pull_request(context: RunContext): Promise<void> {
|
|
|
118
153
|
// What was already changed before the setup ran, read whenever a setup pull request will follow, so
|
|
119
154
|
// it can tell the setup's files from the user's own (#2872). Kit's own `josh init` needs it as much as
|
|
120
155
|
// a caller's command: its `josh format` rewrites the project's own sources, which kit cannot name, and
|
|
121
|
-
// leaving them out failed the pull request's Prettier check.
|
|
156
|
+
// leaving them out failed the pull request's Prettier check. On `main` it is empty, since a dirty tree
|
|
157
|
+
// is refused there (#3835); it is kept for the setup branch a failed commit left, where the earlier
|
|
158
|
+
// run's output is already uncommitted when the re-run starts.
|
|
122
159
|
function baseline_of(
|
|
123
160
|
steps: ReadonlyArray<StartStep>,
|
|
124
161
|
context: StepContext,
|
|
@@ -148,6 +185,27 @@ function run_push_preflight(steps: ReadonlyArray<StartStep>, context: StepContex
|
|
|
148
185
|
}
|
|
149
186
|
}
|
|
150
187
|
|
|
188
|
+
// The `full` profile installs kit's pre-push hook, whose audit refuses a push without osv-scanner. The
|
|
189
|
+
// scanner is otherwise fetched at an agent's session start, which a first `josh start` from a terminal
|
|
190
|
+
// has not had, so it is fetched here — forced, because a person asked for this run. A scanner that
|
|
191
|
+
// still cannot be found stops the run before the first step rather than at the push (#3755).
|
|
192
|
+
async function ensure_audit_scanner(
|
|
193
|
+
steps: ReadonlyArray<StartStep>,
|
|
194
|
+
context: StepContext,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
if (context.profile !== 'full' || !start_plan.has_push(steps)) return
|
|
197
|
+
|
|
198
|
+
const { platform, arch } = process
|
|
199
|
+
|
|
200
|
+
console.info(`\n${SCANNER_NOTICE}`)
|
|
201
|
+
const outcome = await security_audit_provision.report(context.root, platform, arch, true)
|
|
202
|
+
|
|
203
|
+
console.info(outcome)
|
|
204
|
+
if (security_audit.resolve_scanner(context.root, platform) !== undefined) return
|
|
205
|
+
|
|
206
|
+
throw new Error(start_plan.scanner_report(outcome))
|
|
207
|
+
}
|
|
208
|
+
|
|
151
209
|
type StepAction = (context: RunContext) => void | Promise<void>
|
|
152
210
|
|
|
153
211
|
const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
|
|
@@ -178,6 +236,7 @@ async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext):
|
|
|
178
236
|
const labels = start_plan.step_labels(context.init_command)
|
|
179
237
|
|
|
180
238
|
run_push_preflight(steps, context)
|
|
239
|
+
await ensure_audit_scanner(steps, context)
|
|
181
240
|
const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
|
|
182
241
|
|
|
183
242
|
for (const [index, step] of steps.entries()) {
|
|
@@ -192,7 +251,9 @@ async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext):
|
|
|
192
251
|
const start_steps = {
|
|
193
252
|
read_git_state,
|
|
194
253
|
github_cli_refusal,
|
|
254
|
+
git_identity_refusal,
|
|
195
255
|
self_run_refusal,
|
|
256
|
+
uncommitted_refusal,
|
|
196
257
|
run_steps,
|
|
197
258
|
}
|
|
198
259
|
export { start_steps }
|
package/scripts/init/start.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { error_text } from '#scripts/lib/error-message'
|
|
4
|
-
import {
|
|
4
|
+
import { init_dry_run } from './init-dry-run'
|
|
5
|
+
import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
|
|
6
|
+
import { start_dry_run } from './start-dry-run'
|
|
5
7
|
import {
|
|
6
8
|
start_plan,
|
|
7
9
|
type Consent,
|
|
@@ -15,13 +17,14 @@ import { start_steps } from './start-steps'
|
|
|
15
17
|
const ARGUMENT_START_INDEX = 2
|
|
16
18
|
const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
|
|
17
19
|
const START_USAGE =
|
|
18
|
-
'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]'
|
|
20
|
+
'josh start [--profile basic|full] [--yes] [--github] [--public] [--dry-run] [--init-command <command>] [--push-preflight-command <command>]'
|
|
19
21
|
const INIT_COMMAND_FLAG = '--init-command'
|
|
20
22
|
const PUSH_PREFLIGHT_COMMAND_FLAG = '--push-preflight-command'
|
|
21
23
|
const YES_FLAG = '--yes'
|
|
22
24
|
const GITHUB_FLAG = '--github'
|
|
23
25
|
const PUBLIC_FLAG = '--public'
|
|
24
|
-
const
|
|
26
|
+
const { DRY_RUN_FLAG } = init_dry_run
|
|
27
|
+
const SWITCHES: ReadonlySet<string> = new Set([YES_FLAG, GITHUB_FLAG, PUBLIC_FLAG, DRY_RUN_FLAG])
|
|
25
28
|
|
|
26
29
|
interface Prepared {
|
|
27
30
|
steps: ReadonlyArray<StartStep>
|
|
@@ -67,6 +70,7 @@ function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
|
67
70
|
profile: project_profile.requested_profile(profile_args, START_USAGE),
|
|
68
71
|
is_yes: rest.includes(YES_FLAG),
|
|
69
72
|
is_github: rest.includes(GITHUB_FLAG),
|
|
73
|
+
is_dry_run: rest.includes(DRY_RUN_FLAG),
|
|
70
74
|
visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
|
|
71
75
|
init_command: init.command,
|
|
72
76
|
push_preflight_command,
|
|
@@ -94,33 +98,34 @@ async function resolve_profile(
|
|
|
94
98
|
return await start_prompt.confirm_choices(choices)
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
function planned_steps(root: string, shape: ProjectShape): ReadonlyArray<StartStep> {
|
|
102
|
+
const state = start_steps.read_git_state(root, shape)
|
|
103
|
+
const plan = start_plan.plan_steps(state)
|
|
104
|
+
|
|
105
|
+
stop_on(plan.refusal)
|
|
106
|
+
stop_on(start_steps.uncommitted_refusal(root, state))
|
|
107
|
+
|
|
108
|
+
return plan.steps
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
// Every refusal is raised here, before the first write, so a run that stops here changed nothing.
|
|
98
112
|
function prepare(args: ReadonlyArray<string>, is_tty: boolean, root: string): Prepared {
|
|
99
113
|
stop_on(start_steps.self_run_refusal(root))
|
|
100
114
|
const options = parse_start_options(args)
|
|
101
115
|
const interaction = settle(start_plan.interaction_of(options, is_tty))
|
|
102
116
|
const shape = project_profile.inspect_project(root, options.profile)
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
stop_on(plan.refusal)
|
|
106
|
-
const consent: Consent = settle(start_plan.github_consent(plan.steps, options, interaction))
|
|
117
|
+
const steps = planned_steps(root, shape)
|
|
118
|
+
const consent: Consent = settle(start_plan.github_consent(steps, options, interaction))
|
|
107
119
|
|
|
108
120
|
stop_on(start_steps.github_cli_refusal(root))
|
|
121
|
+
stop_on(start_steps.git_identity_refusal(steps, root))
|
|
109
122
|
|
|
110
|
-
return {
|
|
111
|
-
steps: plan.steps,
|
|
112
|
-
interaction,
|
|
113
|
-
choices: { options, shape, consent, root, steps: plan.steps },
|
|
114
|
-
}
|
|
123
|
+
return { steps, interaction, choices: { options, shape, consent, root, steps } }
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
async function
|
|
118
|
-
const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
|
|
119
|
-
|
|
126
|
+
async function run_steps({ steps, interaction, choices }: Prepared): Promise<void> {
|
|
120
127
|
const { root, options } = choices
|
|
121
128
|
const { visibility, init_command, push_preflight_command } = options
|
|
122
|
-
|
|
123
|
-
console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
|
|
124
129
|
const profile = await resolve_profile(choices, interaction)
|
|
125
130
|
const context = { root, profile, visibility, init_command, push_preflight_command }
|
|
126
131
|
|
|
@@ -128,6 +133,22 @@ async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void>
|
|
|
128
133
|
console.info(`\n${READY_MESSAGE}`)
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
// A dry run stops after the plan, adding what each step would create, before the first write.
|
|
137
|
+
async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void> {
|
|
138
|
+
const prepared = prepare(args, is_tty, process.cwd())
|
|
139
|
+
const { steps, choices } = prepared
|
|
140
|
+
|
|
141
|
+
console.info(start_plan.plan_summary(steps, start_plan.step_labels(choices.options.init_command)))
|
|
142
|
+
|
|
143
|
+
if (choices.options.is_dry_run) {
|
|
144
|
+
console.info(start_dry_run.report(choices))
|
|
145
|
+
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await run_steps(prepared)
|
|
150
|
+
}
|
|
151
|
+
|
|
131
152
|
async function run(args: ReadonlyArray<string>, is_tty: boolean): Promise<number> {
|
|
132
153
|
try {
|
|
133
154
|
await main(args, is_tty)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
2
|
+
|
|
3
|
+
// `josh start` sets a project up for the GitHub Issue workflow and `josh init` alone does not. The
|
|
4
|
+
// difference decides whether a `basic` project receives `.claude/settings.json`, whose permissions,
|
|
5
|
+
// hooks and plugin an unattended `backlogrun` agent runs on (#3796).
|
|
6
|
+
//
|
|
7
|
+
// `josh start` hands the mark to the setup it runs through this variable, not an argument: that setup
|
|
8
|
+
// may be the project's own kit it hands off to or a toolkit's command (`--init-command`), and an older
|
|
9
|
+
// one refuses an argument it does not know but ignores a variable. The setup records the mark in
|
|
10
|
+
// `package.json`, where every later `josh init` and `josh sync` reads it.
|
|
11
|
+
const WORKFLOW_ENV = 'JOSH_WORKFLOW'
|
|
12
|
+
const GITHUB_WORKFLOW = 'github'
|
|
13
|
+
|
|
14
|
+
function is_requested(environment: NodeJS.ProcessEnv = process.env): boolean {
|
|
15
|
+
return environment[WORKFLOW_ENV] === GITHUB_WORKFLOW
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function is_recorded(manifest: Record<string, unknown> | undefined): boolean {
|
|
19
|
+
const josh = manifest?.['josh']
|
|
20
|
+
|
|
21
|
+
return json_value.is_record(josh) && josh['workflow'] === GITHUB_WORKFLOW
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function request(environment: NodeJS.ProcessEnv = process.env): void {
|
|
25
|
+
environment[WORKFLOW_ENV] = GITHUB_WORKFLOW
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The `josh` record's field for the mark — none for a project `josh start` did not set up, so a
|
|
29
|
+
// `josh init` never writes the mark of its own accord.
|
|
30
|
+
function recorded_fields(has_workflow: boolean): Record<string, string> {
|
|
31
|
+
return has_workflow ? { workflow: GITHUB_WORKFLOW } : {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workflow_mark = { is_requested, is_recorded, request, recorded_fields }
|
|
35
|
+
export { workflow_mark }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { git_location_environment } from '#scripts/git/git-location-environment'
|
|
2
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
|
+
import { start_exec } from './start-exec'
|
|
4
|
+
|
|
5
|
+
const UNTRACKED_ARGUMENTS = ['ls-files', '--others', '--exclude-standard']
|
|
6
|
+
// `--relative` keeps the listing to the project and its paths relative to it, the way `ls-files`
|
|
7
|
+
// already reads them, for a project that sits below its repository's top level.
|
|
8
|
+
const MODIFIED_ARGUMENTS = ['diff', '--name-only', '--relative', 'HEAD']
|
|
9
|
+
// Under an unborn HEAD every indexed file is uncommitted, so the index itself is the listing.
|
|
10
|
+
const INDEXED_ARGUMENTS = ['ls-files', '--cached']
|
|
11
|
+
const HISTORY_ARGUMENTS = ['rev-list', '--max-count=1', '--all']
|
|
12
|
+
const UNBORN_HEAD_EXIT_CODE = 1
|
|
13
|
+
const NOT_A_REPOSITORY = 'not a git repository'
|
|
14
|
+
|
|
15
|
+
// What a project's working tree holds that is not committed, read one way for every setup command:
|
|
16
|
+
// the setup pull request tells the setup's files from the user's own with it, and `josh init` refuses
|
|
17
|
+
// to write over it.
|
|
18
|
+
|
|
19
|
+
// Listings are read NUL-separated (`-z`), so a path is never quoted — a name a caller's initialize
|
|
20
|
+
// command wrote need not be ASCII the way kit's own are (#2872).
|
|
21
|
+
function paths_of(output: string | undefined = ''): Array<string> {
|
|
22
|
+
return output.split('\0').filter((listed) => listed.length > 0)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listing(args: ReadonlyArray<string>, root: string): Array<string> {
|
|
26
|
+
return paths_of(start_exec.git_read([...args, '-z'], root))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Untracked files the ignore rules let through, plus tracked files changed since HEAD, staged or not
|
|
30
|
+
// — two plain listings, so nothing has to be parsed out of `git status`.
|
|
31
|
+
function changed_paths(root: string): Array<string> {
|
|
32
|
+
const untracked = listing(UNTRACKED_ARGUMENTS, root)
|
|
33
|
+
const modified = listing(MODIFIED_ARGUMENTS, root)
|
|
34
|
+
|
|
35
|
+
return [...untracked, ...modified]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A read that failed is thrown rather than read as an empty listing: the refusal guards the user's
|
|
39
|
+
// work, so a git that did not answer must never pass for a clean tree.
|
|
40
|
+
function strict_listing(args: ReadonlyArray<string>, root: string): Array<string> {
|
|
41
|
+
const output = start_exec.git_read([...args, '-z'], root)
|
|
42
|
+
|
|
43
|
+
if (output === undefined) throw new Error(`git ${args.join(' ')} failed in ${root}`)
|
|
44
|
+
|
|
45
|
+
return paths_of(output)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type HeadState = 'born' | 'unborn' | 'absent'
|
|
49
|
+
|
|
50
|
+
// Asked of git rather than of a `.git` beside the project, so a project below its repository's top
|
|
51
|
+
// level is still found. `--verify --quiet` exits 1 for an unborn HEAD, and only `not a git repository`
|
|
52
|
+
// (read under the C locale) proves there is no repository; any other failure is an unanswered
|
|
53
|
+
// question, not a missing commit.
|
|
54
|
+
function head_state(root: string): HeadState {
|
|
55
|
+
const { exit_code, stderr } = git_spawn_sync.run(['rev-parse', '--verify', '--quiet', 'HEAD'], {
|
|
56
|
+
cwd: root,
|
|
57
|
+
env: { ...git_spawn_sync.C_LOCALE, ...git_location_environment.location_free_environment() },
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (exit_code === 0) return 'born'
|
|
61
|
+
if (stderr.includes(NOT_A_REPOSITORY)) return 'absent'
|
|
62
|
+
if (exit_code === UNBORN_HEAD_EXIT_CODE) return 'unborn'
|
|
63
|
+
throw new Error(`git rev-parse could not read HEAD in ${root}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// An unborn HEAD is either the repository `josh start` has yet to make its first commit in, or an
|
|
67
|
+
// orphan branch (`git checkout --orphan`) whose index still holds another branch's files — told
|
|
68
|
+
// apart by whether any ref reaches a commit.
|
|
69
|
+
function has_history(root: string): boolean {
|
|
70
|
+
const output = start_exec.git_read(HISTORY_ARGUMENTS, root)
|
|
71
|
+
|
|
72
|
+
if (output === undefined) throw new Error(`git ${HISTORY_ARGUMENTS.join(' ')} failed in ${root}`)
|
|
73
|
+
|
|
74
|
+
return output.trim().length > 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// What the tracked files are compared with, or undefined when there is nothing to tell the user's
|
|
78
|
+
// work from — a directory outside any repository, or a repository without any commit.
|
|
79
|
+
function tracked_arguments(root: string): ReadonlyArray<string> | undefined {
|
|
80
|
+
const state = head_state(root)
|
|
81
|
+
|
|
82
|
+
if (state === 'born') return MODIFIED_ARGUMENTS
|
|
83
|
+
if (state === 'unborn' && has_history(root)) return INDEXED_ARGUMENTS
|
|
84
|
+
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function uncommitted_paths(root: string): Array<string> {
|
|
89
|
+
const tracked = tracked_arguments(root)
|
|
90
|
+
|
|
91
|
+
if (tracked === undefined) return []
|
|
92
|
+
|
|
93
|
+
const untracked = strict_listing(UNTRACKED_ARGUMENTS, root)
|
|
94
|
+
const modified = strict_listing(tracked, root)
|
|
95
|
+
|
|
96
|
+
return [...untracked, ...modified]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The refusal a setup command stops with before its first write, or undefined when nothing is
|
|
100
|
+
// uncommitted: what kit then writes would land in the same files as the user's own work, and
|
|
101
|
+
// `git diff` could no longer tell the two apart.
|
|
102
|
+
function dirty_tree_refusal(root: string, command: string): string | undefined {
|
|
103
|
+
const paths = uncommitted_paths(root)
|
|
104
|
+
|
|
105
|
+
if (paths.length === 0) return undefined
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
`${command} refuses to run: the working tree has uncommitted changes.`,
|
|
109
|
+
...paths.map((listed) => ` ${listed}`),
|
|
110
|
+
`Commit them, or stash them with \`git stash push -u -- .\`, then run ${command} again.`,
|
|
111
|
+
].join('\n')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const working_tree = { listing, changed_paths, uncommitted_paths, dirty_tree_refusal }
|
|
115
|
+
|
|
116
|
+
export { working_tree }
|