@joshuafolkken/kit 1.1387.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-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- 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 +288 -229
- 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 +62 -9
- 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-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 +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 +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 +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 +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- 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 +24 -0
- 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 +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/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { parseArgs } from 'node:util'
|
|
6
|
+
import { claude_token } from '#scripts/auth/claude-token'
|
|
7
|
+
import { gh_cli_token } from '#scripts/gh/gh-cli-token'
|
|
8
|
+
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
9
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
10
|
+
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
11
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
12
|
+
import { fresh_machine_docker } from './fresh-machine-docker'
|
|
13
|
+
import { fresh_machine_log } from './fresh-machine-log'
|
|
14
|
+
import { fresh_machine_patterns } from './fresh-machine-patterns'
|
|
15
|
+
import { fresh_machine_run, type RunRequest } from './fresh-machine-run'
|
|
16
|
+
import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
|
|
17
|
+
|
|
18
|
+
// `josh fresh:run <pattern>` / `josh fresh:run --spec <file>` — run one scenario on a machine that
|
|
19
|
+
// has nothing installed, and keep what happened as a log.
|
|
20
|
+
|
|
21
|
+
const ARGV_OFFSET = 2
|
|
22
|
+
const FAILURE_EXIT_CODE = 1
|
|
23
|
+
const USAGE =
|
|
24
|
+
'Usage: josh fresh:run <pattern> | --spec <file.json> [--package-version <version>] [--log-dir <dir>] [--rebuild] | --list'
|
|
25
|
+
// The scratch directory kit's distributed `.gitignore` already ignores, so a log is never committed
|
|
26
|
+
// from kit or from a consumer toolkit.
|
|
27
|
+
const DEFAULT_LOG_DIRECTORY = '.claude/tmp/fresh-machine'
|
|
28
|
+
const NO_GITHUB_TOKEN =
|
|
29
|
+
'This scenario uses GitHub and `gh auth token` printed no token. Run `gh auth login` on this machine first.'
|
|
30
|
+
const NO_CLAUDE_TOKEN = `This scenario uses Claude Code and neither the macOS keychain nor ${claude_token.VARIABLE} holds a token. ${claude_token.SETUP_HINT}`
|
|
31
|
+
// The patterns install kit by name, while the tarball and the published version are those of the
|
|
32
|
+
// checkout the command is run from — in a toolkit's checkout the two would be different packages.
|
|
33
|
+
const NOT_A_KIT_CHECKOUT = `The named patterns install ${KIT_PACKAGE_NAME} and run from its checkout only. Write this package's scenario as a JSON file and pass it with --spec.`
|
|
34
|
+
const OWNED_REPOSITORIES = 'user/repos?affiliation=owner&per_page=100'
|
|
35
|
+
const REPOSITORY_NAMES = '.[].name'
|
|
36
|
+
|
|
37
|
+
// The option names are the CLI's, so the type is read off the parser rather than restated.
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
39
|
+
function parse(argv: ReadonlyArray<string>) {
|
|
40
|
+
return parseArgs({
|
|
41
|
+
args: [...argv],
|
|
42
|
+
allowPositionals: true,
|
|
43
|
+
options: {
|
|
44
|
+
spec: { type: 'string' },
|
|
45
|
+
'package-version': { type: 'string' },
|
|
46
|
+
'log-dir': { type: 'string' },
|
|
47
|
+
rebuild: { type: 'boolean' },
|
|
48
|
+
list: { type: 'boolean' },
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type CliValues = ReturnType<typeof parse>['values']
|
|
54
|
+
|
|
55
|
+
function pattern_of(name: string): Scenario | undefined {
|
|
56
|
+
const scenario = fresh_machine_patterns.find(name)
|
|
57
|
+
|
|
58
|
+
if (scenario === undefined) return undefined
|
|
59
|
+
|
|
60
|
+
const is_kit_checkout = self_sync_guard.read_package_name(PROJECT_ROOT) === KIT_PACKAGE_NAME
|
|
61
|
+
|
|
62
|
+
if (!is_kit_checkout) throw new Error(NOT_A_KIT_CHECKOUT)
|
|
63
|
+
|
|
64
|
+
return scenario
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scenario_of(values: CliValues, pattern: string | undefined): Scenario | undefined {
|
|
68
|
+
if (values.spec !== undefined) {
|
|
69
|
+
return fresh_machine_spec.parse(readFileSync(path.resolve(PROJECT_ROOT, values.spec), 'utf8'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return pattern === undefined ? undefined : pattern_of(pattern)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A GitHub run's name is also the repository it creates under the account of the token, and the
|
|
76
|
+
// logs of this checkout are not the only place such a run was started from.
|
|
77
|
+
function repositories_of(scenario: Scenario): Array<string> {
|
|
78
|
+
if (!scenario.requires_github) return []
|
|
79
|
+
|
|
80
|
+
const names = git_gh_exec.exec_gh_api_sync({
|
|
81
|
+
path: OWNED_REPOSITORIES,
|
|
82
|
+
should_paginate: true,
|
|
83
|
+
jq_filter: REPOSITORY_NAMES,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return names.split('\n').filter((name) => name !== '')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function request_of(
|
|
90
|
+
scenario: Scenario,
|
|
91
|
+
values: CliValues,
|
|
92
|
+
secrets: Record<string, string>,
|
|
93
|
+
): RunRequest {
|
|
94
|
+
const log_directory = path.resolve(PROJECT_ROOT, values['log-dir'] ?? DEFAULT_LOG_DIRECTORY)
|
|
95
|
+
|
|
96
|
+
mkdirSync(log_directory, { recursive: true })
|
|
97
|
+
|
|
98
|
+
const run_name = fresh_machine_log.next_name(scenario.name, {
|
|
99
|
+
logs: readdirSync(log_directory),
|
|
100
|
+
repositories: repositories_of(scenario),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
scenario,
|
|
105
|
+
run_name,
|
|
106
|
+
log_path: path.join(log_directory, fresh_machine_log.file_name(run_name)),
|
|
107
|
+
package_version: values['package-version'],
|
|
108
|
+
secrets,
|
|
109
|
+
is_rebuild: values.rebuild === true,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function refuse(message: string): number {
|
|
114
|
+
console.error(message)
|
|
115
|
+
|
|
116
|
+
return FAILURE_EXIT_CODE
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// One secret a scenario asks for: where its value is read on the host, and what to say when there is
|
|
120
|
+
// none. Every one is read before Docker starts, so a missing token stops the run with nothing built.
|
|
121
|
+
interface Secret {
|
|
122
|
+
variable: string
|
|
123
|
+
is_required: (scenario: Scenario) => boolean
|
|
124
|
+
read: () => string | undefined
|
|
125
|
+
missing: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SECRETS: ReadonlyArray<Secret> = [
|
|
129
|
+
{
|
|
130
|
+
variable: fresh_machine_docker.TOKEN_VARIABLE,
|
|
131
|
+
is_required: (scenario) => scenario.requires_github,
|
|
132
|
+
read: gh_cli_token.get,
|
|
133
|
+
missing: NO_GITHUB_TOKEN,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
variable: claude_token.VARIABLE,
|
|
137
|
+
is_required: (scenario) => scenario.requires_claude,
|
|
138
|
+
read: claude_token.read,
|
|
139
|
+
missing: NO_CLAUDE_TOKEN,
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
// The values found, by variable name, and the message of the first secret that has none.
|
|
144
|
+
interface Secrets {
|
|
145
|
+
values: Record<string, string>
|
|
146
|
+
missing: string | undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function secrets_of(scenario: Scenario): Secrets {
|
|
150
|
+
const required = SECRETS.filter((secret) => secret.is_required(scenario))
|
|
151
|
+
const read = required.map((secret) => ({ secret, value: secret.read() }))
|
|
152
|
+
const found = read.flatMap(({ secret, value }) =>
|
|
153
|
+
value === undefined ? [] : [[secret.variable, value] as const],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
values: Object.fromEntries(found),
|
|
158
|
+
missing: read.find(({ value }) => value === undefined)?.secret.missing,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function run_scenario(scenario: Scenario, values: CliValues): Promise<number> {
|
|
163
|
+
const secrets = secrets_of(scenario)
|
|
164
|
+
|
|
165
|
+
if (secrets.missing !== undefined) return refuse(secrets.missing)
|
|
166
|
+
|
|
167
|
+
const request = request_of(scenario, values, secrets.values)
|
|
168
|
+
const exit_code = await fresh_machine_run.run(request)
|
|
169
|
+
|
|
170
|
+
console.info(`Log: ${request.log_path}`)
|
|
171
|
+
|
|
172
|
+
return exit_code
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
176
|
+
const { values, positionals } = parse(argv)
|
|
177
|
+
const patterns = fresh_machine_patterns.names().join('\n')
|
|
178
|
+
|
|
179
|
+
if (values.list === true) {
|
|
180
|
+
console.info(patterns)
|
|
181
|
+
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const scenario = scenario_of(values, positionals[0])
|
|
186
|
+
|
|
187
|
+
if (scenario === undefined) return refuse(`${USAGE}\n\nPatterns:\n${patterns}`)
|
|
188
|
+
|
|
189
|
+
return await run_scenario(scenario, values)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
process.exitCode = await run(argv)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
process.exitCode = refuse(error instanceof Error ? error.message : String(error))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const fresh_machine_cli = { run }
|
|
201
|
+
|
|
202
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
203
|
+
|
|
204
|
+
export { fresh_machine_cli }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fresh_machine_script } from './fresh-machine-script'
|
|
2
|
+
|
|
3
|
+
// The two docker invocations of `josh fresh:run`, as argument lists. A token is passed by name only
|
|
4
|
+
// (`--env GH_TOKEN`): docker reads its value from the environment of the client, so it is in no
|
|
5
|
+
// argument, no image layer and no printed command.
|
|
6
|
+
|
|
7
|
+
const TOKEN_VARIABLE = 'GH_TOKEN'
|
|
8
|
+
const ENV_FLAG = '--env'
|
|
9
|
+
const VOLUME_FLAG = '--volume'
|
|
10
|
+
|
|
11
|
+
interface ContainerRun {
|
|
12
|
+
image: string
|
|
13
|
+
run_name: string
|
|
14
|
+
script: string
|
|
15
|
+
seed_directory: string
|
|
16
|
+
pack_directory: string
|
|
17
|
+
// The packed checkout's file name inside `pack_directory`, for a tarball scenario.
|
|
18
|
+
tarball: string | undefined
|
|
19
|
+
package_version: string | undefined
|
|
20
|
+
// The variables whose values the client's environment carries into the container.
|
|
21
|
+
secret_names: ReadonlyArray<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The Dockerfile arrives on stdin (`-`), so the build has no context to copy a host file from.
|
|
25
|
+
function build_arguments(image: string, is_rebuild: boolean): Array<string> {
|
|
26
|
+
const cache = is_rebuild ? ['--no-cache'] : []
|
|
27
|
+
|
|
28
|
+
return ['build', '--progress', 'plain', ...cache, '--tag', image, '-']
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function variables(container: ContainerRun): Array<string> {
|
|
32
|
+
const { tarball } = container
|
|
33
|
+
const tarball_path = tarball === undefined ? '' : `${fresh_machine_script.PACK_MOUNT}/${tarball}`
|
|
34
|
+
const assignments = [
|
|
35
|
+
`RUN_NAME=${container.run_name}`,
|
|
36
|
+
`PACKAGE_TARBALL=${tarball_path}`,
|
|
37
|
+
`PACKAGE_VERSION=${container.package_version ?? ''}`,
|
|
38
|
+
...container.secret_names,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
return assignments.flatMap((assignment) => [ENV_FLAG, assignment])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `bash -i` is the new terminal the prerequisites guide asks for: it reads the profile the pnpm
|
|
45
|
+
// installer edited, which a non-interactive shell on Debian returns from before reaching.
|
|
46
|
+
function run_arguments(container: ContainerRun): Array<string> {
|
|
47
|
+
return [
|
|
48
|
+
'run',
|
|
49
|
+
'--rm',
|
|
50
|
+
...variables(container),
|
|
51
|
+
VOLUME_FLAG,
|
|
52
|
+
`${container.seed_directory}:${fresh_machine_script.SEED_MOUNT}:ro`,
|
|
53
|
+
VOLUME_FLAG,
|
|
54
|
+
`${container.pack_directory}:${fresh_machine_script.PACK_MOUNT}:ro`,
|
|
55
|
+
container.image,
|
|
56
|
+
'bash',
|
|
57
|
+
'-ic',
|
|
58
|
+
container.script,
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fresh_machine_docker = { build_arguments, run_arguments, TOKEN_VARIABLE }
|
|
63
|
+
|
|
64
|
+
export { fresh_machine_docker }
|
|
65
|
+
export type { ContainerRun }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The "fresh machine" of `josh fresh:run`: a Debian image with curl and without Node.js, pnpm, git or
|
|
2
|
+
// gh, on which a normal user runs the Linux route of `docs/setup/prerequisites.md` and nothing else.
|
|
3
|
+
// The Dockerfile is built here rather than kept as a file because `docs/` is not in the published
|
|
4
|
+
// package and a consumer toolkit builds the same machine; `fresh-machine-image.test.ts` holds the
|
|
5
|
+
// commands below to the guide's own lines.
|
|
6
|
+
|
|
7
|
+
const BASE_IMAGE = 'buildpack-deps:bookworm-curl'
|
|
8
|
+
const MACHINE_USER = 'dev'
|
|
9
|
+
const HOME_DIRECTORY = `/home/${MACHINE_USER}`
|
|
10
|
+
const TAG_PREFIX = 'josh-fresh-machine'
|
|
11
|
+
|
|
12
|
+
// `docs/setup/prerequisites.md` → "Linux and macOS". The first line runs in the shell a new machine
|
|
13
|
+
// starts with; the guide then says to open a new terminal, which is what `NEW_TERMINAL` does.
|
|
14
|
+
const INSTALL_PNPM = 'curl -fsSL https://get.pnpm.io/install.sh | sh -'
|
|
15
|
+
const NEW_TERMINAL_COMMANDS = ['pnpm runtime set node 22 -g', 'node --version']
|
|
16
|
+
|
|
17
|
+
// The guide sends a Linux reader to the gh installation page; these are that page's Debian steps,
|
|
18
|
+
// run as root in place of `sudo`, with the curl the image already has. git comes from the same
|
|
19
|
+
// package manager, and so does procps: kit's lane commands read processes with its ps and pgrep, which
|
|
20
|
+
// the slim base image does not ship. Only a GitHub scenario's machine has any of them.
|
|
21
|
+
const KEYRING = '/etc/apt/keyrings/githubcli-archive-keyring.gpg'
|
|
22
|
+
const APT_UPDATE = 'apt-get update'
|
|
23
|
+
const GITHUB_TOOLS = [
|
|
24
|
+
APT_UPDATE,
|
|
25
|
+
'apt-get install -y git procps',
|
|
26
|
+
'mkdir -p -m 755 /etc/apt/keyrings',
|
|
27
|
+
`curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o ${KEYRING}`,
|
|
28
|
+
`chmod go+r ${KEYRING}`,
|
|
29
|
+
`echo "deb [arch=$(dpkg --print-architecture) signed-by=${KEYRING}] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list`,
|
|
30
|
+
APT_UPDATE,
|
|
31
|
+
'apt-get install -y gh',
|
|
32
|
+
].join(' && ')
|
|
33
|
+
|
|
34
|
+
function new_terminal(command: string): string {
|
|
35
|
+
return `RUN bash -ic '${command}'`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `SHELL` is the one line the guide does not have: the pnpm installer reads it to pick the profile
|
|
39
|
+
// it edits, and a container build does not set it.
|
|
40
|
+
function dockerfile(requires_github: boolean): string {
|
|
41
|
+
const github_tools = requires_github ? [`RUN ${GITHUB_TOOLS}`] : []
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
`FROM ${BASE_IMAGE}`,
|
|
45
|
+
...github_tools,
|
|
46
|
+
`RUN useradd --create-home --shell /bin/bash ${MACHINE_USER}`,
|
|
47
|
+
`USER ${MACHINE_USER}`,
|
|
48
|
+
`WORKDIR ${HOME_DIRECTORY}`,
|
|
49
|
+
'ENV SHELL=/bin/bash',
|
|
50
|
+
`RUN ${INSTALL_PNPM}`,
|
|
51
|
+
...NEW_TERMINAL_COMMANDS.map((command) => new_terminal(command)),
|
|
52
|
+
'',
|
|
53
|
+
].join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tag(requires_github: boolean): string {
|
|
57
|
+
return `${TAG_PREFIX}:${requires_github ? 'github' : 'base'}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fresh_machine_image = { dockerfile, tag }
|
|
61
|
+
|
|
62
|
+
export { fresh_machine_image }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The two rules a `josh fresh:run` log follows: a run never reuses a name, and no token passed to
|
|
2
|
+
// the container reaches the file.
|
|
3
|
+
|
|
4
|
+
const LOG_EXTENSION = '.log'
|
|
5
|
+
const FIRST_RUN = 1
|
|
6
|
+
|
|
7
|
+
// What already carries a run's name: the logs of the directory this run writes to and, for a GitHub
|
|
8
|
+
// scenario, the account's repositories — another checkout, or a lane worktree since removed, numbers
|
|
9
|
+
// from its own logs and leaves repositories this directory has no log of.
|
|
10
|
+
interface TakenNames {
|
|
11
|
+
logs: ReadonlyArray<string>
|
|
12
|
+
repositories: ReadonlyArray<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function logged_names(logs: ReadonlyArray<string>): Array<string> {
|
|
16
|
+
return logs
|
|
17
|
+
.filter((file) => file.endsWith(LOG_EXTENSION))
|
|
18
|
+
.map((file) => file.slice(0, -LOG_EXTENSION.length))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function run_number(scenario_name: string, run_name: string): number | undefined {
|
|
22
|
+
const prefix = `${scenario_name}-`
|
|
23
|
+
|
|
24
|
+
if (!run_name.startsWith(prefix)) return undefined
|
|
25
|
+
|
|
26
|
+
const digits = run_name.slice(prefix.length)
|
|
27
|
+
|
|
28
|
+
return /^\d+$/u.test(digits) ? Number(digits) : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The name is the project directory and, for a GitHub scenario, the repository `josh start` creates.
|
|
32
|
+
// It is numbered past every earlier log and repository, so a rerun leaves the last run's repository
|
|
33
|
+
// in place.
|
|
34
|
+
function next_name(scenario_name: string, taken_names: TakenNames): string {
|
|
35
|
+
const taken = [...logged_names(taken_names.logs), ...taken_names.repositories]
|
|
36
|
+
.map((run_name) => run_number(scenario_name, run_name))
|
|
37
|
+
.filter((number) => number !== undefined)
|
|
38
|
+
const next = taken.length === 0 ? FIRST_RUN : Math.max(...taken) + 1
|
|
39
|
+
|
|
40
|
+
return `${scenario_name}-${String(next)}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function file_name(run_name: string): string {
|
|
44
|
+
return `${run_name}${LOG_EXTENSION}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function redact(line: string, secrets: ReadonlyArray<string>): string {
|
|
48
|
+
const present = secrets.filter((secret) => secret !== '')
|
|
49
|
+
let redacted = line
|
|
50
|
+
|
|
51
|
+
for (const secret of present) {
|
|
52
|
+
redacted = redacted.replaceAll(secret, '[REDACTED]')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return redacted
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fresh_machine_log = { next_name, file_name, redact }
|
|
59
|
+
|
|
60
|
+
export { fresh_machine_log }
|
|
61
|
+
export type { TakenNames }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
2
|
+
import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
|
|
3
|
+
|
|
4
|
+
// kit's own six patterns: three projects (one Lua file, one `index.html`, a minimal TypeScript
|
|
5
|
+
// SvelteKit app from `sv`) by two commands (`josh init`, `josh start`), each from the starting state
|
|
6
|
+
// joshuafolkken/kit#2848 gave it. `josh init` installs the packed checkout, so a fix is retested
|
|
7
|
+
// without a release; `josh start` installs a published version, because its setup pull request
|
|
8
|
+
// would carry a tarball path CI cannot resolve.
|
|
9
|
+
|
|
10
|
+
const KIT = KIT_PACKAGE_NAME
|
|
11
|
+
const ALLOW_BUILDS = '--allow-build=esbuild --allow-build=unrs-resolver'
|
|
12
|
+
// What `pnpm dlx @joshuafolkken/kit init` writes before it installs kit.
|
|
13
|
+
const BARE_MANIFEST = String.raw`printf '{ "private": true }\n' > package.json`
|
|
14
|
+
const CREATE_SVELTEKIT =
|
|
15
|
+
'pnpm dlx sv create --template minimal --types ts --no-add-ons --install pnpm .'
|
|
16
|
+
// The `sv` that created the project is the one `pnpm dlx` resolved, so its version is recorded.
|
|
17
|
+
const SV_VERSION = 'pnpm dlx sv --version'
|
|
18
|
+
// The app builds and type-checks before kit is installed and still does after it.
|
|
19
|
+
const SVELTEKIT_CHECKS = 'pnpm build && pnpm check'
|
|
20
|
+
const INSTALL_TARBALL = `pnpm add -D ${ALLOW_BUILDS} "$PACKAGE_TARBALL"`
|
|
21
|
+
// A version published minutes ago is inside pnpm's release-age wait; kit alone is excluded from it
|
|
22
|
+
// (joshuafolkken/kit#2869), so every other dependency keeps the wait.
|
|
23
|
+
const EXCLUDE_RELEASE_AGE = String.raw`printf 'minimumReleaseAgeExclude:\n - "${KIT}"\n' >> pnpm-workspace.yaml`
|
|
24
|
+
const INSTALL_PUBLISHED = `pnpm add -D ${ALLOW_BUILDS} ${KIT}@"$PACKAGE_VERSION"`
|
|
25
|
+
const GATE = 'pnpm josh gate'
|
|
26
|
+
const INIT_COMMANDS = [INSTALL_TARBALL, 'pnpm exec josh init', GATE]
|
|
27
|
+
// `josh start` leaves the project on its setup branch, where the gate asks for the scoped checks
|
|
28
|
+
// first; this is the line its refusal hands back.
|
|
29
|
+
const SCOPED_CHECKS = 'pnpm josh lint:related && pnpm josh test:related'
|
|
30
|
+
const START_COMMANDS = [
|
|
31
|
+
EXCLUDE_RELEASE_AGE,
|
|
32
|
+
INSTALL_PUBLISHED,
|
|
33
|
+
'pnpm exec josh start --yes --github',
|
|
34
|
+
SCOPED_CHECKS,
|
|
35
|
+
GATE,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// `gh auth login` cannot run without a person, and it is what points git at gh for credentials; with
|
|
39
|
+
// the token in the environment this is the same step.
|
|
40
|
+
const SIGN_IN = 'gh auth setup-git'
|
|
41
|
+
// What the prerequisites guide has a person run once per machine. Only the pattern with no repository
|
|
42
|
+
// uses it: `josh start` makes the first commit there, and stops before writing without it (#3754).
|
|
43
|
+
const MACHINE_IDENTITY =
|
|
44
|
+
'git config --global user.name kit-test && git config --global user.email kit-test@example.com'
|
|
45
|
+
// The identity is the repository's own, so the machine stays one nobody has configured git on.
|
|
46
|
+
const FIRST_COMMIT = [
|
|
47
|
+
'git init --initial-branch main',
|
|
48
|
+
'git config user.name kit-test && git config user.email kit-test@example.com',
|
|
49
|
+
'git add --all && git commit --message "Initial commit"',
|
|
50
|
+
]
|
|
51
|
+
const CREATE_REPOSITORY = 'gh repo create "$RUN_NAME" --private --source . --push'
|
|
52
|
+
|
|
53
|
+
// The two states #3712 was found in: a shell that reads no rc file, and a PATH with `node` on it and
|
|
54
|
+
// no `pnpm`.
|
|
55
|
+
const BARE_SHELL =
|
|
56
|
+
'env -i HOME="$HOME" PATH=/usr/local/bin:/usr/bin:/bin bash --noprofile --norc -c'
|
|
57
|
+
const NODE_ONLY = '"$HOME/node-only"'
|
|
58
|
+
const PATH_PROBES = [
|
|
59
|
+
`${BARE_SHELL} '${GATE}'`,
|
|
60
|
+
`mkdir -p ${NODE_ONLY} && ln -sf "$(command -v node)" ${NODE_ONLY}/node && ${BARE_SHELL} 'PATH="$HOME/node-only:$PATH" ./node_modules/.bin/josh gate'`,
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// What a `josh init` run is checked against after the gate: the profile it chose, the files it left
|
|
64
|
+
// (a `./.git` line is a repository it should not have made), and a second run that changes nothing.
|
|
65
|
+
const PROJECT_FILES =
|
|
66
|
+
'find . -name node_modules -prune -o -name .git -prune -o -type f -print | sort'
|
|
67
|
+
const CHECKSUMS = `${PROJECT_FILES} | xargs sha256sum`
|
|
68
|
+
const INIT_PROBES = [
|
|
69
|
+
'pnpm josh profile',
|
|
70
|
+
'find . -name node_modules -prune -o -print | sort',
|
|
71
|
+
`${CHECKSUMS} > "$HOME/before-second-init.sha" && pnpm exec josh init && ${CHECKSUMS} | diff "$HOME/before-second-init.sha" -`,
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const LUA_FILE = 'main.lua'
|
|
75
|
+
const HTML_FILE = 'index.html'
|
|
76
|
+
const LUA_FILES = { [LUA_FILE]: 'print("Hello")\n' }
|
|
77
|
+
const HTML_FILES = { [HTML_FILE]: '<!doctype html><html><body><h1>Hello</h1></body></html>\n' }
|
|
78
|
+
|
|
79
|
+
function init_pattern(
|
|
80
|
+
project: string,
|
|
81
|
+
files: Record<string, string>,
|
|
82
|
+
setup: ReadonlyArray<string>,
|
|
83
|
+
project_probes: ReadonlyArray<string> = [],
|
|
84
|
+
): Scenario {
|
|
85
|
+
return fresh_machine_spec.define({
|
|
86
|
+
name: `kit-test-${project}-init`,
|
|
87
|
+
files,
|
|
88
|
+
commands: [...setup, ...INIT_COMMANDS],
|
|
89
|
+
probes: [...INIT_PROBES, ...project_probes, ...PATH_PROBES],
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function start_pattern(
|
|
94
|
+
project: string,
|
|
95
|
+
files: Record<string, string>,
|
|
96
|
+
setup: ReadonlyArray<string>,
|
|
97
|
+
): Scenario {
|
|
98
|
+
return fresh_machine_spec.define({
|
|
99
|
+
name: `kit-test-${project}-start`,
|
|
100
|
+
requires_github: true,
|
|
101
|
+
package_source: 'published',
|
|
102
|
+
files,
|
|
103
|
+
commands: [SIGN_IN, ...setup, ...START_COMMANDS],
|
|
104
|
+
probes: PATH_PROBES,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PATTERNS: ReadonlyArray<Scenario> = [
|
|
109
|
+
init_pattern('lua', LUA_FILES, [BARE_MANIFEST]),
|
|
110
|
+
// No Git: `josh start` creates the repository, the first commit and the GitHub repository.
|
|
111
|
+
start_pattern('lua', LUA_FILES, [MACHINE_IDENTITY, BARE_MANIFEST]),
|
|
112
|
+
init_pattern('html', HTML_FILES, [BARE_MANIFEST]),
|
|
113
|
+
// Git with a first commit on `main`, no GitHub repository.
|
|
114
|
+
start_pattern('html', HTML_FILES, [...FIRST_COMMIT, BARE_MANIFEST]),
|
|
115
|
+
init_pattern('svelte', {}, [CREATE_SVELTEKIT, SV_VERSION, SVELTEKIT_CHECKS], [SVELTEKIT_CHECKS]),
|
|
116
|
+
// A GitHub repository with `main` already pushed.
|
|
117
|
+
start_pattern('svelte', {}, [CREATE_SVELTEKIT, ...FIRST_COMMIT, CREATE_REPOSITORY]),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
function names(): Array<string> {
|
|
121
|
+
return PATTERNS.map((pattern) => pattern.name)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function find(name: string): Scenario | undefined {
|
|
125
|
+
return PATTERNS.find((pattern) => pattern.name === name)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const fresh_machine_patterns = { names, find }
|
|
129
|
+
|
|
130
|
+
export { fresh_machine_patterns }
|