@joshuafolkken/kit 1.1094.0 → 1.1128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/workflow-commands/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/background-commands.md +36 -85
- package/.claude/skills/workflow-commands/backlogrun-progress.md +29 -32
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
- package/.claude/skills/workflow-commands/fullrun.md +5 -4
- package/.claude/skills/workflow-commands/halfrun.md +5 -4
- package/.claude/skills/workflow-commands/latest-gate.md +7 -15
- package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
- package/.claude/skills/workflow-commands/split-assessment.md +4 -18
- package/.github/actions/setup-node/action.yml +33 -0
- package/.github/actions/setup-pnpm/action.yml +4 -4
- package/.github/workflows/auto-tag.yml +2 -3
- package/.github/workflows/ci.yml +7 -8
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/pr-classification.yml +1 -3
- package/.github/workflows/production.yml +1 -0
- package/.github/workflows/publish.yml +10 -9
- package/.github/workflows/sonar-qube.yml +17 -13
- package/.pnpmfile.mjs +29 -0
- package/AGENTS.md +4 -11
- package/CLAUDE.md +24 -17
- package/GEMINI.md +4 -13
- package/README.md +8 -3
- package/SECURITY.md +1 -0
- package/cspell/index.yaml +4 -0
- package/dist/CLAUDE.basic.md +6 -6
- package/dist/CLAUDE.md +24 -17
- package/dist/CLAUDE.static.md +6 -6
- package/dist/config-merge/index.js +33 -9
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NKOG3XAV.js → chunk-4JZDP5X5.js} +2 -2
- package/dist/hooks/{chunk-3C4YCCEF.js → chunk-5SD2YM7Z.js} +4 -4
- package/dist/hooks/{chunk-OIZUHPJI.js → chunk-77J7IEQM.js} +1 -1
- package/dist/hooks/{chunk-KHWHKJOM.js → chunk-7HVB7SZG.js} +3 -3
- package/dist/hooks/{chunk-UD24EAIL.js → chunk-AIOIOMPX.js} +1875 -104
- package/dist/hooks/{chunk-M2XDCUKD.js → chunk-M7QKEKWO.js} +88 -1783
- package/dist/hooks/{chunk-E54KGFEQ.js → chunk-MKVGBQPA.js} +1 -1
- package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-NXYMNPUQ.js} +3 -3
- package/dist/hooks/{chunk-O4LL4HTI.js → chunk-QVLT52OV.js} +2 -2
- package/dist/hooks/{chunk-IL27YTPH.js → chunk-XYUIQOW3.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +12 -47
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +180 -179
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +17 -12
- package/dist/josh.js +79 -30
- package/dist/version/index.js +36 -20
- package/eslint/rules/unicorn.js +3 -2
- package/package.json +18 -2
- package/pnpm-workspace.yaml +5 -7
- package/prompts/collaboration-workflow/glossary.md +17 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/prompts/collaboration-workflow/operating-rules.md +1 -1
- package/prompts/collaboration-workflow/principles.md +9 -0
- package/prompts/collaboration-workflow.md +1 -0
- package/scripts/backlog/backlog-arrival.ts +30 -10
- package/scripts/backlog/backlog-budget-cli.ts +15 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -14
- package/scripts/backlog/backlog-plan.ts +29 -18
- package/scripts/backlog/backlog-ready.ts +20 -7
- package/scripts/config-merge/parse-jsonc.ts +33 -3
- package/scripts/config-merge/patch-json-key.ts +13 -6
- package/scripts/cost-runtime/cost-cli.ts +3 -7
- package/scripts/doctor/doctor-consumer.ts +3 -5
- package/scripts/doctor/doctor-io.ts +9 -13
- package/scripts/document/backlogrun-parent-read-set.ts +12 -1
- package/scripts/document/document-byte-budget.ts +1 -0
- package/scripts/document/entry-read-budget.ts +13 -7
- package/scripts/document/entry-read-set.ts +32 -8
- package/scripts/document/lane-child-read-set.ts +7 -0
- package/scripts/document/read-set-point-of-use.ts +8 -3
- package/scripts/document/read-set-trim.ts +3 -1
- package/scripts/epic/epic-audit.ts +3 -18
- package/scripts/gh/git-gh-issue-list.ts +12 -11
- package/scripts/gh/git-gh-issue-write.ts +4 -1
- package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
- package/scripts/git/git-command.ts +6 -1
- package/scripts/git/git-spawn-sync.ts +65 -0
- package/scripts/git/git-spawn.ts +5 -4
- package/scripts/git/stash/stash-pop-args.ts +1 -6
- package/scripts/init/init-file-action.ts +54 -0
- package/scripts/init/init-logic-migrate.ts +35 -17
- package/scripts/init/init-logic-secretlint.ts +1 -1
- package/scripts/init/init-logic.ts +6 -22
- package/scripts/init/init.ts +11 -68
- package/scripts/init/kit-written-paths.ts +2 -0
- package/scripts/init/project-profile.ts +3 -6
- package/scripts/init/start-steps.ts +2 -1
- package/scripts/issue/defect-rate-cli.ts +61 -20
- package/scripts/issue/issue-backlinks.ts +7 -14
- package/scripts/issue/issue-cite.ts +12 -6
- package/scripts/issue/issue-file.ts +5 -12
- package/scripts/issue/markdown-section.ts +21 -6
- package/scripts/issue/measure-rerun-cli.ts +73 -14
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/josh/run-github-prerequisite.ts +3 -6
- package/scripts/lane/lane-await.ts +43 -13
- package/scripts/lane/lane-close.ts +4 -1
- package/scripts/lane/lane-launch-cli.ts +10 -16
- package/scripts/lane/openai-review-broker.ts +4 -1
- package/scripts/lib/cli-flags.ts +31 -2
- package/scripts/lib/error-message.ts +12 -1
- package/scripts/lines/line-targets.ts +0 -11
- package/scripts/lines/lines-command.ts +5 -3
- package/scripts/propagate/propagate-git.ts +3 -8
- package/scripts/refactor/refactor-scan.ts +2 -1
- package/scripts/registry-migration/migrate-logic.ts +28 -19
- package/scripts/registry-migration/migrate.ts +6 -5
- package/scripts/release/github-release-cli.ts +14 -3
- package/scripts/release/release-publish.ts +36 -2
- package/scripts/release/release-worktree.ts +97 -2
- package/scripts/review/review-checkout.ts +3 -2
- package/scripts/review/review-record-cli.ts +9 -16
- package/scripts/rules/early-heartbeat.ts +4 -3
- package/scripts/rules/lane-park.ts +9 -0
- package/scripts/rules/stop-rules.ts +7 -4
- package/scripts/run/run-carry-args.ts +8 -12
- package/scripts/run/run-cut-args.ts +1 -6
- package/scripts/run/run-ending-cli.ts +1 -5
- package/scripts/run/run-entry-cli.ts +40 -9
- package/scripts/run/run-entry-stop.ts +104 -0
- package/scripts/run/run-event-stream-emit.ts +7 -2
- package/scripts/run/run-hold.ts +4 -1
- package/scripts/run/run-liveness-cli.ts +1 -5
- package/scripts/run/run-merge-cli.ts +2 -10
- package/scripts/run/run-parent-cut-hook.ts +4 -1
- package/scripts/run/run-prep-cli.ts +4 -1
- package/scripts/run/run-progress-args.ts +20 -4
- package/scripts/run/run-progress-cli.ts +31 -29
- package/scripts/run/run-progress-clock.ts +4 -1
- package/scripts/run/run-ship-cli.ts +8 -16
- package/scripts/run/run-ship-probe.ts +4 -1
- package/scripts/run/run-status-cli.ts +2 -15
- package/scripts/run/run-tidy-lanes.ts +4 -1
- package/scripts/run/run-wake-cli.ts +2 -10
- package/scripts/run/run-watcher-guard.ts +9 -5
- package/scripts/safe-chain/pack-hook.ts +92 -0
- package/scripts/sync/sync-ai-files.ts +4 -0
- package/scripts/test/test-declared-changed.ts +13 -16
- package/scripts/test/test-declared.ts +3 -7
- package/scripts/version/version-commands.ts +11 -1
- package/templates/CLAUDE.basic.md +6 -6
- package/templates/workflows/ci.yml +16 -82
- package/templates/workflows/dependabot-auto-merge.yml +1 -0
- package/templates/workflows/github-release.yml +1 -3
- package/tsconfig.sonar.json +4 -22
package/scripts/init/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { resolve_local_bin } from '#scripts/build/local-bin'
|
|
@@ -7,6 +7,7 @@ import { doctor_io } from '#scripts/doctor/doctor-io'
|
|
|
7
7
|
import { error_text } from '#scripts/lib/error-message'
|
|
8
8
|
import { package_version_schema, with_package_manager_schema } from '#scripts/lib/schemas'
|
|
9
9
|
import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
|
|
10
|
+
import { pack_hook } from '#scripts/safe-chain/pack-hook'
|
|
10
11
|
import { project_config } from '#scripts/safe-chain/project-config'
|
|
11
12
|
import { security_updates } from '#scripts/security/security-updates'
|
|
12
13
|
import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
|
|
@@ -14,10 +15,11 @@ import { sync } from '#scripts/sync/sync'
|
|
|
14
15
|
import { package_manager_version } from '#scripts/version/package-manager-version'
|
|
15
16
|
import { execaSync } from 'execa'
|
|
16
17
|
import { z } from 'zod'
|
|
17
|
-
import { init_actions, PRETTIER_CONFIG_JS
|
|
18
|
+
import { init_actions, PRETTIER_CONFIG_JS } from './init-actions'
|
|
18
19
|
import { init_ai_copy } from './init-ai-copy'
|
|
19
20
|
import { init_basic } from './init-basic'
|
|
20
21
|
import { init_bootstrap } from './init-bootstrap'
|
|
22
|
+
import { init_file_action } from './init-file-action'
|
|
21
23
|
import { init_install } from './init-install'
|
|
22
24
|
import { init_logic } from './init-logic'
|
|
23
25
|
import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
|
|
@@ -27,69 +29,10 @@ import { project_profile, type ProjectProfile, type ProjectShape } from './proje
|
|
|
27
29
|
const PACKAGE_JSON = 'package.json'
|
|
28
30
|
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
29
31
|
const LEFTHOOK_BIN = 'lefthook'
|
|
30
|
-
const SAMPLE_INDENT_WIDTH = 4
|
|
31
32
|
const ARGUMENT_START_INDEX = 2
|
|
32
|
-
const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
|
|
33
33
|
const INSTALL_HINT = '→ run `pnpm install` to install what package.json lists'
|
|
34
34
|
const INIT_USAGE = 'josh init [--profile basic|full] [--no-install]'
|
|
35
35
|
|
|
36
|
-
function write_new_file(action: FileAction, destination_path: string): void {
|
|
37
|
-
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
38
|
-
writeFileSync(destination_path, action.create())
|
|
39
|
-
console.info(` ✔ created ${action.dest}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function show_sample(action: FileAction): void {
|
|
43
|
-
console.info(` ⚠ exists ${action.dest} — add manually:`)
|
|
44
|
-
console.info('')
|
|
45
|
-
console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// A file that already holds the sample needs nothing added, so only one that differs gets it shown.
|
|
49
|
-
function report_unchanged(action: FileAction, existing: string): void {
|
|
50
|
-
const is_sample_missing = existing !== action.create()
|
|
51
|
-
|
|
52
|
-
if (is_sample_missing && action.should_show_sample_when_unchanged === true) show_sample(action)
|
|
53
|
-
else console.info(` ✔ unchanged ${action.dest}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function merge_existing_file(
|
|
57
|
-
merge_function: (existing: string) => string,
|
|
58
|
-
destination_path: string,
|
|
59
|
-
action: FileAction,
|
|
60
|
-
): void {
|
|
61
|
-
const existing = readFileSync(destination_path, 'utf8')
|
|
62
|
-
const merged = merge_function(existing)
|
|
63
|
-
|
|
64
|
-
if (merged === existing) {
|
|
65
|
-
report_unchanged(action, existing)
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
writeFileSync(destination_path, merged)
|
|
71
|
-
console.info(` ✔ updated ${action.dest}`)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function execute_file_action(action: FileAction): void {
|
|
75
|
-
const destination_path = path.join(PROJECT_ROOT, action.dest)
|
|
76
|
-
const is_existing = existsSync(destination_path)
|
|
77
|
-
|
|
78
|
-
if (!is_existing) {
|
|
79
|
-
write_new_file(action, destination_path)
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (action.merge === undefined) {
|
|
85
|
-
show_sample(action)
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
merge_existing_file(action.merge, destination_path, action)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
36
|
const engine_pin_schema = z.looseObject({ name: z.string(), version: z.string() })
|
|
94
37
|
|
|
95
38
|
const published_development_engines_schema = z.looseObject({
|
|
@@ -124,10 +67,10 @@ function get_kit_self_dependency(): Record<string, string> {
|
|
|
124
67
|
}
|
|
125
68
|
|
|
126
69
|
// Each config `init` generates runs a CLI or imports types the consumer has to resolve itself:
|
|
127
|
-
// `
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
const TOOL_DEVELOPMENT_DEPENDENCIES = ['
|
|
70
|
+
// `cspell.config.yaml` → cspell, `playwright.config.ts` → @types/node, `lefthook.yml` → lefthook.
|
|
71
|
+
// Prettier, its preset plugins and @playwright/test come in as peers. Without them the first
|
|
72
|
+
// `josh gate` after `pnpm install` fails (joshuafolkken/kit#2710).
|
|
73
|
+
const TOOL_DEVELOPMENT_DEPENDENCIES = ['cspell', '@types/node']
|
|
131
74
|
const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
|
|
132
75
|
|
|
133
76
|
function tool_dependency_names(has_git: boolean): ReadonlyArray<string> {
|
|
@@ -164,9 +107,8 @@ function apply_dependency_merges(content: string, has_git = true): string {
|
|
|
164
107
|
? suggested
|
|
165
108
|
: Object.fromEntries(Object.entries(suggested).filter(([key]) => key !== 'prepare')),
|
|
166
109
|
)
|
|
167
|
-
const with_prettier = init_logic.merge_prettier_plugin_development_deps(merged)
|
|
168
110
|
const with_toolchain = init_logic.merge_development_dependencies(
|
|
169
|
-
|
|
111
|
+
merged,
|
|
170
112
|
get_toolchain_development_dependencies(has_git),
|
|
171
113
|
)
|
|
172
114
|
const with_secretlint = init_logic.merge_secretlint_development_deps(with_toolchain)
|
|
@@ -262,7 +204,7 @@ function run_config_file_actions(shape: ProjectShape): void {
|
|
|
262
204
|
}
|
|
263
205
|
|
|
264
206
|
for (const action of init_actions.build_file_actions(shape)) {
|
|
265
|
-
execute_file_action(action)
|
|
207
|
+
init_file_action.execute_file_action(action, PROJECT_ROOT)
|
|
266
208
|
}
|
|
267
209
|
}
|
|
268
210
|
|
|
@@ -305,6 +247,7 @@ function initialize_project(shape: ProjectShape): void {
|
|
|
305
247
|
merge_project_package_json(shape)
|
|
306
248
|
|
|
307
249
|
run_ai_file_actions(shape)
|
|
250
|
+
pack_hook.sync_pack_hook(PROJECT_ROOT, PACKAGE_DIR)
|
|
308
251
|
if (shape.profile === 'full') project_config.sync_project_config(PROJECT_ROOT)
|
|
309
252
|
}
|
|
310
253
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PACK_HOOK_FILE } from '#scripts/safe-chain/pack-hook'
|
|
1
2
|
import { PROJECT_CONFIG_FILE } from '#scripts/safe-chain/project-config'
|
|
2
3
|
import { managed_config_scope } from '#scripts/sync/managed-config-scope'
|
|
3
4
|
import { BASIC_PRETTIER_CONFIG } from './init-actions'
|
|
@@ -13,6 +14,7 @@ const SETUP_ONLY_PATHS: ReadonlySet<string> = new Set([
|
|
|
13
14
|
CLAUDE_MD_FILENAME,
|
|
14
15
|
BASIC_PRETTIER_CONFIG,
|
|
15
16
|
PROJECT_CONFIG_FILE,
|
|
17
|
+
PACK_HOOK_FILE,
|
|
16
18
|
MANIFEST_FILE,
|
|
17
19
|
LOCKFILE,
|
|
18
20
|
])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, type Dirent } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
4
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
4
5
|
import { json_value } from '#scripts/lib/json-value'
|
|
5
6
|
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
6
|
-
import { execaSync } from 'execa'
|
|
7
7
|
|
|
8
8
|
type ProjectProfile = 'basic' | 'full'
|
|
9
9
|
|
|
@@ -159,12 +159,9 @@ function scan_files(root: string): { has_web: boolean; has_typescript: boolean }
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function has_github_remote(root: string): boolean {
|
|
162
|
-
const
|
|
163
|
-
cwd: root,
|
|
164
|
-
reject: false,
|
|
165
|
-
})
|
|
162
|
+
const url = git_spawn_sync.origin_url(root)
|
|
166
163
|
|
|
167
|
-
return
|
|
164
|
+
return url !== undefined && repo_origin.parse_origin_url(url) !== undefined
|
|
168
165
|
}
|
|
169
166
|
|
|
170
167
|
function inspect_project(root: string, requested?: ProjectProfile): ProjectShape {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
2
3
|
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
4
5
|
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
@@ -44,7 +45,7 @@ function read_git_state(root: string, shape: ProjectShape): GitState {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const has_origin =
|
|
48
|
+
const has_origin = git_spawn_sync.origin_url(root) !== undefined
|
|
48
49
|
const branch = read_output('git', ['symbolic-ref', '--short', 'HEAD'], root)
|
|
49
50
|
const has_commits = succeeds('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], root)
|
|
50
51
|
const has_kit_committed = kit_setup_state.is_kit_committed(root)
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
5
4
|
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
6
|
-
import {
|
|
5
|
+
import { parse_json } from '#scripts/git/parse-json-array'
|
|
7
6
|
import { issue_label_schema } from '#scripts/git/schemas'
|
|
7
|
+
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
8
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
8
9
|
import { z } from 'zod'
|
|
9
10
|
import { defect_rate, type DefectRate, type RateIssue } from './defect-rate'
|
|
10
11
|
|
|
11
12
|
const SUCCESS_EXIT_CODE = 0
|
|
12
13
|
const FAILURE_EXIT_CODE = 1
|
|
13
14
|
const PER_PAGE = '100'
|
|
15
|
+
const FIRST_PAGE = 1
|
|
16
|
+
const SECOND_PAGE = 2
|
|
17
|
+
// The search API serves at most this many results, so a window past it holds no further page.
|
|
18
|
+
const SEARCH_RESULT_CEILING = 1000
|
|
19
|
+
// The search API allows 30 requests a minute; both searches together stay at the ~10 requests the
|
|
20
|
+
// sequential walk made, so this bounds only how many are in flight at once.
|
|
21
|
+
const PAGE_CONCURRENCY = 4
|
|
14
22
|
const USAGE = 'Usage: pnpm josh defect:rate [--days <n>]'
|
|
15
23
|
const UNKNOWN_REPO_MESSAGE =
|
|
16
24
|
'Could not read this repository from `git remote`, so no issue can be searched — check `gh auth status` and that this is a checkout with an `origin` remote.'
|
|
@@ -33,7 +41,7 @@ const search_item_schema = z.object({
|
|
|
33
41
|
labels: z.array(issue_label_schema),
|
|
34
42
|
})
|
|
35
43
|
|
|
36
|
-
// One page of `search/issues`.
|
|
44
|
+
// One page of `search/issues`.
|
|
37
45
|
const search_page_schema = z.object({
|
|
38
46
|
total_count: z.number(),
|
|
39
47
|
incomplete_results: z.boolean(),
|
|
@@ -48,22 +56,60 @@ interface SearchResult {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
function read_days(argv: ReadonlyArray<string>): number | undefined {
|
|
51
|
-
|
|
52
|
-
const { values } = parseArgs({ args: [...argv], options: OPTIONS, strict: true })
|
|
53
|
-
const raw = values.days ?? String(defect_rate.DEFAULT_WINDOW_DAYS)
|
|
59
|
+
const values = cli_flags.values_of(argv, OPTIONS)
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
if (values === undefined) return undefined
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
const raw = values.days ?? String(defect_rate.DEFAULT_WINDOW_DAYS)
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
if (!DAYS_PATTERN.test(raw)) return undefined
|
|
66
|
+
|
|
67
|
+
const days = Number(raw)
|
|
68
|
+
|
|
69
|
+
return days <= MAX_WINDOW_DAYS ? days : undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function search_path(query: string, page: number = FIRST_PAGE): string {
|
|
73
|
+
const parameters = new URLSearchParams({ q: query, per_page: PER_PAGE })
|
|
74
|
+
|
|
75
|
+
if (page !== FIRST_PAGE) parameters.set('page', String(page))
|
|
76
|
+
|
|
77
|
+
return `search/issues?${parameters.toString()}`
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
// How many pages the search holds, read off the first page's own count. Bounded by the API's
|
|
81
|
+
// ceiling, past which GitHub serves no page at all — so this asks for exactly the pages `gh api
|
|
82
|
+
// --paginate` used to walk to.
|
|
83
|
+
function page_count(first: SearchPage): number {
|
|
84
|
+
return Math.ceil(Math.min(first.total_count, SEARCH_RESULT_CEILING) / Number(PER_PAGE))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function read_page(query: string, page: number): Promise<SearchPage | undefined> {
|
|
88
|
+
const raw = await git_gh_exec.exec_gh_api({ path: search_path(query, page) })
|
|
89
|
+
|
|
90
|
+
return parse_json.parse_json_object_safe(raw, search_page_schema)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Every page of one search. joshuafolkken/kit#3103: `--paginate` follows each page's `next` link, so
|
|
94
|
+
// a two-week window of ~5 pages cost ~1.5 s a page, one after another, on every `backlog:plan`. The
|
|
95
|
+
// first page names the total, so the rest are fetched together; any page that does not read fails
|
|
96
|
+
// the whole search, as a failed `--paginate` did.
|
|
97
|
+
async function read_pages(query: string): Promise<ReadonlyArray<SearchPage> | undefined> {
|
|
98
|
+
const first = await read_page(query, FIRST_PAGE)
|
|
99
|
+
|
|
100
|
+
if (first === undefined) return undefined
|
|
101
|
+
|
|
102
|
+
const rest_numbers = Array.from(
|
|
103
|
+
{ length: Math.max(page_count(first) - FIRST_PAGE, 0) },
|
|
104
|
+
(_value, index) => index + SECOND_PAGE,
|
|
105
|
+
)
|
|
106
|
+
const rest = await bounded_pool.bounded_map(
|
|
107
|
+
rest_numbers,
|
|
108
|
+
PAGE_CONCURRENCY,
|
|
109
|
+
async (page) => await read_page(query, page),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return rest.every((page) => page !== undefined) ? [first, ...rest] : undefined
|
|
67
113
|
}
|
|
68
114
|
|
|
69
115
|
function to_result(pages: ReadonlyArray<SearchPage>): SearchResult {
|
|
@@ -83,12 +129,7 @@ function to_result(pages: ReadonlyArray<SearchPage>): SearchResult {
|
|
|
83
129
|
|
|
84
130
|
async function search(query: string): Promise<SearchResult | undefined> {
|
|
85
131
|
try {
|
|
86
|
-
const
|
|
87
|
-
path: search_path(query),
|
|
88
|
-
should_paginate: true,
|
|
89
|
-
should_slurp: true,
|
|
90
|
-
})
|
|
91
|
-
const pages = parse_json_array_or_undefined(raw, search_page_schema)
|
|
132
|
+
const pages = await read_pages(query)
|
|
92
133
|
|
|
93
134
|
return pages === undefined ? undefined : to_result(pages)
|
|
94
135
|
} catch {
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// undetected. This classifies an origin issue's filed backlinks into one of four fixed words
|
|
5
5
|
// (joshuafolkken/kit#2123). The headings are the single source's, pinned by the document test.
|
|
6
6
|
|
|
7
|
+
import { markdown_section } from './markdown-section'
|
|
8
|
+
|
|
7
9
|
const ORIGIN_HEADING = '## Origin'
|
|
8
10
|
const UPSTREAM_ISSUES_HEADING = '## Upstream issues'
|
|
9
11
|
const UPSTREAM_CANDIDATE_HEADING = '## Upstream candidate'
|
|
10
12
|
const CHECKBOX_PATTERN = /^\s*-\s*\[[ xX]\]/u
|
|
11
13
|
const WRONG_UPSTREAM_HEADING_PATTERN = /^##\s+Upstream\b/iu
|
|
12
|
-
const SUBHEADING_PATTERN = /^##\s/u
|
|
13
14
|
const BARE_REFERENCE_PATTERN = /^#\d+$/u
|
|
14
15
|
const DIGITS_PATTERN = /^\d+$/u
|
|
15
16
|
const REFERENCE_MARK = '#'
|
|
@@ -27,31 +28,23 @@ interface UpstreamEntry {
|
|
|
27
28
|
body: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
return body.split('\n').map((line) => line.trimEnd())
|
|
32
|
-
}
|
|
33
|
-
|
|
31
|
+
// A heading inside a code fence is example text, so neither lookup counts one (joshuafolkken/kit#3066).
|
|
34
32
|
function has_heading(body: string, heading: string): boolean {
|
|
35
|
-
return
|
|
33
|
+
return markdown_section.has_line(body, heading)
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
// The lines under `heading`, up to the next `##` heading; `undefined` when the heading is absent.
|
|
39
37
|
function section_lines(body: string, heading: string): ReadonlyArray<string> | undefined {
|
|
40
|
-
|
|
41
|
-
const start = lines.findIndex((line) => line.trim() === heading)
|
|
42
|
-
if (start === -1) return undefined
|
|
43
|
-
|
|
44
|
-
const rest = lines.slice(start + 1)
|
|
45
|
-
const end = rest.findIndex((line) => SUBHEADING_PATTERN.test(line.trim()))
|
|
38
|
+
if (!has_heading(body, heading)) return undefined
|
|
46
39
|
|
|
47
|
-
return
|
|
40
|
+
return markdown_section.section_lines(body, heading)
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
// A heading that looks like the upstream one but is not exactly it (`## Upstream`, `## Upstreams`,
|
|
51
44
|
// wrong case) — the grep the template promises would miss it, so it is a wrong-heading rather than a
|
|
52
45
|
// missing one. `## Upstream candidate` is a legitimate separate heading and is excluded.
|
|
53
46
|
function has_wrong_upstream_heading(body: string): boolean {
|
|
54
|
-
return
|
|
47
|
+
return markdown_section.unfenced_lines(body).some((line) => {
|
|
55
48
|
const heading = line.trim()
|
|
56
49
|
if (heading === UPSTREAM_ISSUES_HEADING || heading === UPSTREAM_CANDIDATE_HEADING) return false
|
|
57
50
|
|
|
@@ -46,14 +46,19 @@ function citation_line(slug: string, number: string, summary: string): string {
|
|
|
46
46
|
// and the plain `#N` only when the repository could not be read at all.
|
|
47
47
|
type IssueCiter = (number: string) => string
|
|
48
48
|
|
|
49
|
+
// One issue, named from what the caller holds — the single decision every listing's citation goes
|
|
50
|
+
// through (joshuafolkken/kit#3099), so a renderer holding one title never re-derives the fallback.
|
|
51
|
+
function reference(slug: string | undefined, number: string, title: string | undefined): string {
|
|
52
|
+
if (slug === undefined) return `#${number}`
|
|
53
|
+
|
|
54
|
+
return title === undefined
|
|
55
|
+
? `[#${number}](${issue_url(slug, number)})`
|
|
56
|
+
: citation_line(slug, number, title)
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
function citer(slug: string | undefined, titles: ReadonlyMap<string, string>): IssueCiter {
|
|
50
60
|
return function cite(number: string): string {
|
|
51
|
-
|
|
52
|
-
const title = titles.get(number)
|
|
53
|
-
|
|
54
|
-
return title === undefined
|
|
55
|
-
? `[#${number}](${issue_url(slug, number)})`
|
|
56
|
-
: citation_line(slug, number, title)
|
|
61
|
+
return reference(slug, number, titles.get(number))
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -111,6 +116,7 @@ const issue_cite = {
|
|
|
111
116
|
missing_line,
|
|
112
117
|
no_repo_line,
|
|
113
118
|
parse_target,
|
|
119
|
+
reference,
|
|
114
120
|
unreadable_line,
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { parseArgs } from 'node:util'
|
|
1
|
+
import type { parseArgs } from 'node:util'
|
|
2
2
|
import { epic_issue } from '#scripts/epic/epic-issue'
|
|
3
3
|
import { github_issue_url } from '#scripts/gh/github-issue-url'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
4
5
|
import { issue_backlinks } from './issue-backlinks'
|
|
5
6
|
import { issue_classification } from './issue-classification'
|
|
6
7
|
import { DEPTH_LABEL_ORDER, FILING_ROUTE_LABELS, has_label_name } from './issue-labels'
|
|
@@ -103,19 +104,11 @@ function arguments_of(values: ParsedValues, title: string | undefined): FileArgu
|
|
|
103
104
|
return { title, body_file, ...labelled, labels: values.label ?? [], repo: values.repo }
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
// An unknown flag
|
|
107
|
+
// An unknown flag is `undefined` rather than a throw, so the answer is the usage line, not a stack trace.
|
|
107
108
|
function parse(argv: ReadonlyArray<string>): FileArguments | undefined {
|
|
108
|
-
|
|
109
|
-
const { values, positionals } = parseArgs({
|
|
110
|
-
args: [...argv],
|
|
111
|
-
options: OPTIONS,
|
|
112
|
-
allowPositionals: true,
|
|
113
|
-
})
|
|
109
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
} catch {
|
|
117
|
-
return undefined
|
|
118
|
-
}
|
|
111
|
+
return parsed === undefined ? undefined : arguments_of(parsed.values, parsed.positionals[0])
|
|
119
112
|
}
|
|
120
113
|
|
|
121
114
|
// A classification label is the body's to declare, so an extra `--label` naming one the body does not
|
|
@@ -52,13 +52,16 @@ function has_line(body: string, text: string): boolean {
|
|
|
52
52
|
return has_unfenced_line(body, text)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// The lines between `
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
function
|
|
55
|
+
// The lines between the first unfenced line `is_target` accepts and the next unfenced `## ` heading,
|
|
56
|
+
// trimmed of the heading line itself; an empty array when no line is accepted. A `## ` line inside a
|
|
57
|
+
// code fence is example text, so it neither opens nor closes a section (joshuafolkken/kit#3066).
|
|
58
|
+
function section_lines_matching(
|
|
59
|
+
body: string,
|
|
60
|
+
is_target: (line: string) => boolean,
|
|
61
|
+
): ReadonlyArray<string> {
|
|
59
62
|
const lines = body.split('\n')
|
|
60
63
|
const visible = unfenced_lines(body)
|
|
61
|
-
const start = visible.findIndex((line) =>
|
|
64
|
+
const start = visible.findIndex((line) => is_target(line))
|
|
62
65
|
|
|
63
66
|
if (start === -1) return []
|
|
64
67
|
|
|
@@ -68,6 +71,18 @@ function section_lines(body: string, heading: string): ReadonlyArray<string> {
|
|
|
68
71
|
return end === -1 ? rest : rest.slice(0, end)
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
// A heading matched inside a sentence is not the section heading, so only a line that is exactly the
|
|
75
|
+
// heading opens the section.
|
|
76
|
+
function section_lines(body: string, heading: string): ReadonlyArray<string> {
|
|
77
|
+
return section_lines_matching(body, (line) => is_the_heading(line, heading))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const markdown_section = {
|
|
81
|
+
section_lines,
|
|
82
|
+
section_lines_matching,
|
|
83
|
+
unfenced_lines,
|
|
84
|
+
has_line,
|
|
85
|
+
has_unfenced_line,
|
|
86
|
+
}
|
|
72
87
|
|
|
73
88
|
export { markdown_section }
|
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { execSync } from 'node:child_process'
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
4
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
5
|
+
import { parse_json_object_safe } from '#scripts/git/parse-json-array'
|
|
5
6
|
import { error_text } from '#scripts/lib/error-message'
|
|
6
7
|
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
8
|
+
import { z } from 'zod'
|
|
7
9
|
import { baseline_measure, type Baseline } from './baseline-measure'
|
|
8
10
|
|
|
9
|
-
// `josh measure:rerun <
|
|
11
|
+
// `josh measure:rerun <N>` — read a behavior-change Issue body after merge, re-run each baseline
|
|
10
12
|
// command, and print the before/after pair (joshuafolkken/kit#2212). A value that did not move means
|
|
11
13
|
// the premise the rule rested on is refuted, so a line is appended to the observation ledger — reusing
|
|
12
14
|
// that append-only, same-key mechanism rather than a second one.
|
|
15
|
+
//
|
|
16
|
+
// **The baseline is shell, and the body is written by whoever filed the Issue** (joshuafolkken/kit#3064).
|
|
17
|
+
// A baseline needs pipes and loops (`git log … | wc -l`, `for d in $(ls …)`), so an allow-list of
|
|
18
|
+
// commands cannot make it safe; what can is who wrote it. The input is therefore the Issue number, not
|
|
19
|
+
// a file, so the author's `author_association` arrives with the body from the same REST read, and only
|
|
20
|
+
// a body written by someone with write access to the repository is run.
|
|
13
21
|
|
|
14
22
|
const FAILURE_EXIT_CODE = 1
|
|
15
|
-
const
|
|
23
|
+
const ARGV_OFFSET = 2
|
|
24
|
+
const ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/u
|
|
25
|
+
const ISSUE_FIELDS = 'body,author_association'
|
|
26
|
+
const TRUSTED_ASSOCIATIONS: ReadonlySet<string> = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
|
|
27
|
+
const TRUSTED_LIST = [...TRUSTED_ASSOCIATIONS].join(' / ')
|
|
28
|
+
const UNKNOWN_ASSOCIATION = 'unknown'
|
|
29
|
+
const USAGE = 'Usage: josh measure:rerun <issue-number>'
|
|
16
30
|
const NO_BASELINE = 'no `command → value` baseline found in ## ベースライン — nothing to re-run'
|
|
17
31
|
const DATE_END = 10
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
32
|
+
// One baseline command's budget — the same minute `josh-harness.ts` gives one command it spawns. A
|
|
33
|
+
// baseline is a count or a digest that answers in seconds, so a command still running after a minute
|
|
34
|
+
// is stuck, and without a bound it would hold the whole re-measurement open with nothing printed.
|
|
35
|
+
const MEASURE_TIMEOUT_MS = 60_000
|
|
36
|
+
const issue_schema = z.object({
|
|
37
|
+
body: z.string().nullish(),
|
|
38
|
+
author_association: z.string().nullish(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
type IssueFields = z.infer<typeof issue_schema>
|
|
42
|
+
type BodyRead = { kind: 'trusted'; body: string } | { kind: 'refused'; reason: string }
|
|
43
|
+
|
|
44
|
+
// The command's stdout, trimmed to one value. A failing or timed-out command yields its message so the
|
|
45
|
+
// pair still prints and the remaining baselines still run, rather than aborting the whole
|
|
46
|
+
// re-measurement.
|
|
21
47
|
function measure(command: string): string {
|
|
22
48
|
try {
|
|
23
|
-
return execSync(command, { encoding: 'utf8' }).trim()
|
|
49
|
+
return execSync(command, { encoding: 'utf8', timeout: MEASURE_TIMEOUT_MS }).trim()
|
|
24
50
|
} catch (error) {
|
|
25
51
|
return `(command failed: ${error_text.message_of(error)})`
|
|
26
52
|
}
|
|
@@ -56,8 +82,35 @@ function today(now: Date): string {
|
|
|
56
82
|
return now.toISOString().slice(0, DATE_END)
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
async function
|
|
60
|
-
const
|
|
85
|
+
async function read_issue(issue_number: string): Promise<IssueFields | undefined> {
|
|
86
|
+
const json = await git_gh_issue_read.issue_view_json(issue_number, ISSUE_FIELDS)
|
|
87
|
+
|
|
88
|
+
return json === undefined ? undefined : parse_json_object_safe(json, issue_schema)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The body, only when its author can write to the repository. A read that fails is refused as well:
|
|
92
|
+
// an unknown author is not a trusted one.
|
|
93
|
+
async function read_trusted_body(issue_number: string): Promise<BodyRead> {
|
|
94
|
+
const issue = await read_issue(issue_number)
|
|
95
|
+
|
|
96
|
+
if (issue === undefined) {
|
|
97
|
+
return { kind: 'refused', reason: `could not read issue #${issue_number}` }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const association = issue.author_association ?? UNKNOWN_ASSOCIATION
|
|
101
|
+
|
|
102
|
+
if (!TRUSTED_ASSOCIATIONS.has(association)) {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'refused',
|
|
105
|
+
reason: `refusing to run the baseline of #${issue_number}: its author is ${association}, not ${TRUSTED_LIST}`,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { kind: 'trusted', body: issue.body ?? '' }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function rerun(body: string, now: Date): Promise<number> {
|
|
113
|
+
const baselines = baseline_measure.parse_baselines(body)
|
|
61
114
|
|
|
62
115
|
if (baselines.length === 0) {
|
|
63
116
|
console.error(NO_BASELINE)
|
|
@@ -76,14 +129,22 @@ async function rerun(body_path: string, now: Date): Promise<number> {
|
|
|
76
129
|
return 0
|
|
77
130
|
}
|
|
78
131
|
|
|
79
|
-
async function run(
|
|
80
|
-
if (
|
|
132
|
+
async function run(issue_number: string | undefined, now: Date): Promise<number> {
|
|
133
|
+
if (issue_number === undefined || !ISSUE_NUMBER_PATTERN.test(issue_number)) {
|
|
81
134
|
console.error(USAGE)
|
|
82
135
|
|
|
83
136
|
return FAILURE_EXIT_CODE
|
|
84
137
|
}
|
|
85
138
|
|
|
86
|
-
|
|
139
|
+
const read = await read_trusted_body(issue_number)
|
|
140
|
+
|
|
141
|
+
if (read.kind === 'refused') {
|
|
142
|
+
console.error(read.reason)
|
|
143
|
+
|
|
144
|
+
return FAILURE_EXIT_CODE
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return await rerun(read.body, now)
|
|
87
148
|
}
|
|
88
149
|
|
|
89
150
|
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
@@ -92,8 +153,6 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
92
153
|
|
|
93
154
|
const measure_rerun_cli = { run }
|
|
94
155
|
|
|
95
|
-
const ARGV_OFFSET = 2
|
|
96
|
-
|
|
97
156
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
98
157
|
|
|
99
158
|
export { measure_rerun_cli }
|
|
@@ -40,7 +40,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
40
40
|
},
|
|
41
41
|
'registry:migrate': {
|
|
42
42
|
script: 'scripts/registry-migration/migrate.ts',
|
|
43
|
-
description: 'Migrate a
|
|
43
|
+
description: 'Migrate a project from GitHub Packages to public npm',
|
|
44
44
|
category: 'Project',
|
|
45
45
|
reference: ['', 'developer', ['files', 'network', 'processes']],
|
|
46
46
|
},
|
|
@@ -89,7 +89,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
|
|
|
89
89
|
script: 'scripts/issue/measure-rerun-cli.ts',
|
|
90
90
|
description: 'Re-run a merged issue’s baseline command and print the before/after pair',
|
|
91
91
|
category: 'Workflow',
|
|
92
|
-
reference: ['<
|
|
92
|
+
reference: ['<issue-number>', 'automation', ['network', 'processes', 'files']],
|
|
93
93
|
},
|
|
94
94
|
// A script rather than an `sh -c` chain, because it has a precondition to enforce: run inside a
|
|
95
95
|
// linked work tree it would hijack the default branch from every other one (joshuafolkken/kit#1535).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
4
|
-
import {
|
|
4
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
5
5
|
|
|
6
6
|
const GITHUB_RUN_COMMANDS = new Set([
|
|
7
7
|
'run:entry',
|
|
@@ -19,11 +19,8 @@ function explanation(root: string, command: string): string | undefined {
|
|
|
19
19
|
return `${command} requires Git and a GitHub origin. Run git init and add a GitHub origin first.`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
reject: false,
|
|
25
|
-
})
|
|
26
|
-
if (result.exitCode === 0 && repo_origin.parse_origin_url(result.stdout)) return undefined
|
|
22
|
+
const url = git_spawn_sync.origin_url(root)
|
|
23
|
+
if (url !== undefined && repo_origin.parse_origin_url(url)) return undefined
|
|
27
24
|
|
|
28
25
|
return `${command} requires a GitHub origin. Add an origin pointing to GitHub first.`
|
|
29
26
|
}
|