@mytegroupinc/myte-core 0.0.18 → 0.0.20
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/README.md +14 -142
- package/cli.js +528 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,151 +1,23 @@
|
|
|
1
1
|
# @mytegroupinc/myte-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Implementation package for the `myte` CLI.
|
|
4
4
|
|
|
5
|
-
Most users should install the
|
|
6
|
-
- `npm install myte` then `npx myte ai "Explain this repository"`
|
|
7
|
-
- `npm install myte` then `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
|
|
8
|
-
- `npm install myte` then `npx myte bootstrap`
|
|
9
|
-
- `npm install myte` then `npx myte run-qaqc --mission-ids M001 --wait --sync`
|
|
10
|
-
- `npm install myte` then `npx myte mission status --mission-ids M001 --status done`
|
|
11
|
-
- `npm install myte` then `npx myte sync-qaqc`
|
|
12
|
-
- `npm install myte` then `npx myte feedback-sync`
|
|
13
|
-
- `npm install myte` then `npx myte suggestions sync`
|
|
14
|
-
- `npm install myte` then `npx myte suggestions create`
|
|
15
|
-
- `npm install myte` then `npx myte suggestions revise`
|
|
16
|
-
- `npm install myte` then `npx myte suggestions review`
|
|
17
|
-
- `npm install myte` then `npx myte query "..." --with-diff`
|
|
18
|
-
- `npm install myte` then `npm exec myte -- query "..." --with-diff`
|
|
19
|
-
- `npm install myte` then `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
20
|
-
- `npm install myte` then `npx myte update-owner --subject "QAQC progress" --body-file ./updates/owner.md`
|
|
21
|
-
- `npm install myte` then `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
22
|
-
- `npm i -g myte` then `myte bootstrap`
|
|
23
|
-
- `npm i -g myte` then `myte run-qaqc --mission-ids M001 --wait --sync`
|
|
24
|
-
- `npm i -g myte` then `myte mission status --mission-ids M001 --status done`
|
|
25
|
-
- `npm i -g myte` then `myte sync-qaqc`
|
|
26
|
-
- `npm i -g myte` then `myte feedback-sync`
|
|
27
|
-
- `npm i -g myte` then `myte suggestions sync`
|
|
28
|
-
- `npm i -g myte` then `myte suggestions create`
|
|
29
|
-
- `npm i -g myte` then `myte suggestions revise`
|
|
30
|
-
- `npm i -g myte` then `myte suggestions review`
|
|
31
|
-
- `npm i -g myte` then `myte query "..." --with-diff`
|
|
32
|
-
- `npm i -g myte` then `myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
33
|
-
- `npm i -g myte` then `myte update-owner --subject "QAQC progress" --body-file ./updates/owner.md`
|
|
34
|
-
- `npm i -g myte` then `myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
35
|
-
- `npx myte@latest bootstrap`
|
|
36
|
-
- `npx myte@latest run-qaqc --mission-ids M001 --wait --sync`
|
|
37
|
-
- `npx myte@latest mission status --mission-ids M001 --status done`
|
|
38
|
-
- `npx myte@latest sync-qaqc`
|
|
39
|
-
- `npx myte@latest feedback-sync`
|
|
40
|
-
- `npx myte@latest suggestions sync`
|
|
41
|
-
- `npx myte@latest suggestions create`
|
|
42
|
-
- `npx myte@latest suggestions revise`
|
|
43
|
-
- `npx myte@latest suggestions review`
|
|
44
|
-
- `npx myte@latest query "..." --with-diff`
|
|
45
|
-
- `npx myte@latest update-team "Backend deploy completed; QAQC rerun queued."`
|
|
46
|
-
- `npx myte@latest update-owner --subject "QAQC progress" --body-file ./updates/owner.md`
|
|
47
|
-
- `npx myte@latest update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
48
|
-
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
|
|
49
|
-
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md`
|
|
50
|
-
- `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
|
|
5
|
+
Most users should install the public wrapper instead:
|
|
51
6
|
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
- macOS, Linux, or Windows
|
|
55
|
-
- `git` in `PATH` for `--with-diff`
|
|
56
|
-
- `MYTEAI_API_KEY=<inference_api_key>` in env or `.env` for `myte ai`
|
|
57
|
-
- `MYTE_API_KEY=<project_api_key>` in env or `.env`
|
|
58
|
-
- repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
|
|
7
|
+
- `npm install myte`
|
|
8
|
+
- run with `npx myte ...`
|
|
59
9
|
|
|
60
|
-
|
|
61
|
-
- `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
|
|
62
|
-
- `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
|
|
63
|
-
- `bootstrap` is a local file materialization path, not a hosted file download.
|
|
64
|
-
- `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
|
|
65
|
-
- `bootstrap` writes `MyteCommandCenter/data/project.yml` plus itemized `phases`, `epics`, `stories`, and `missions`.
|
|
66
|
-
- `bootstrap` materializes a public Command Center DTO, not raw backend documents.
|
|
67
|
-
- `bootstrap` mission cards now include richer execution context like `complexity`, `estimated_hours`, `due_date`, `subtasks`, `technical_requirements`, `resources_needed`, `labels`, and normalized `test_cases`.
|
|
68
|
-
- `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
|
|
69
|
-
- rerunning current commands on an older workspace automatically prunes legacy artifacts like `bootstrap-manifest.json`, `data/qaqc/`, and `data/feedback/` as the new files are written.
|
|
70
|
-
- `run-qaqc` queues QAQC for up to 10 explicit mission ids through `/api/project-assistant/run-qaqc`.
|
|
71
|
-
- On PowerShell, quote comma-separated multi-id values: `--mission-ids "M001,M002"`.
|
|
72
|
-
- `run-qaqc --wait` polls `/api/project-assistant/run-qaqc/<batch_id>` until the batch is terminal.
|
|
73
|
-
- `run-qaqc --sync` refreshes `MyteCommandCenter/data/qaqc.yml` after a completed batch.
|
|
74
|
-
- `mission status` updates one or many mission business ids through `/api/project-assistant/mission-status-update`.
|
|
75
|
-
- `mission status` normalizes status aliases like `todo`, `in_progress`, and `done`, then sends the canonical mission status values `Todo`, `In Progress`, or `Done`.
|
|
76
|
-
- `mission status` updates only the mission `status` field used by the app. It does not run QAQC and it does not rewrite `MyteCommandCenter/data/qaqc.yml`.
|
|
77
|
-
- project-key QAQC runs through the dedicated `project_api_qaqc` queue inside the existing Celery service, with a global budget of `20` dispatch starts/minute and `20` live jobs.
|
|
78
|
-
- a saturated `run-qaqc --wait` batch can take roughly `5-10` minutes before `--sync` has final data to write.
|
|
79
|
-
- `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc.yml` automatically if missing.
|
|
80
|
-
- `sync-qaqc` writes the active mission QAQC working set to `MyteCommandCenter/data/qaqc.yml`.
|
|
81
|
-
- `sync-qaqc` only exports active `Todo` / `In Progress` missions plus a public QAQC summary and sanitized latest batch metadata.
|
|
82
|
-
- `sync-qaqc` keeps QAQC state in one deterministic file so the working set grows and shrinks with current active-mission reality.
|
|
83
|
-
- `sync-qaqc` fully rewrites `MyteCommandCenter/data/qaqc.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
|
|
84
|
-
- `feedback-sync` writes one deterministic feedback snapshot under `MyteCommandCenter/data/feedback.yml`.
|
|
85
|
-
- `feedback-sync` defaults to all non-archived feedback unless `--status` is provided.
|
|
86
|
-
- `feedback-sync` keeps feedback metadata plus comment turns in `MyteCommandCenter/data/feedback.yml`.
|
|
87
|
-
- `feedback-sync` writes full PRD context into `MyteCommandCenter/PRD/feedback-sync/*.md` and points to those files from `feedback.yml`.
|
|
88
|
-
- `feedback-sync` fully replaces the feedback-owned sync file to avoid stale local feedback noise.
|
|
89
|
-
- `feedback-sync` fully rewrites `MyteCommandCenter/data/feedback.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
|
|
90
|
-
- `suggestions sync` writes one merge-safe workflow file at `MyteCommandCenter/data/mission-ops.yml`.
|
|
91
|
-
- `suggestions sync` should be the first step before ideating locally so new submissions start from the latest aggregated thread state.
|
|
92
|
-
- `suggestions sync` rewrites the canonical synced state into `threads[]` and preserves top-level `workspace.<actor_scope>` blocks plus per-thread `workspace.<actor_scope>` drafts from the existing file.
|
|
93
|
-
- synced `threads[]` include aggregate diffs, conflict summaries, and archived decision lineage so local agents do not need to reconstruct review state.
|
|
94
|
-
- `suggestions create` reads a structured file payload or local `workspace.<actor_scope>.draft_submissions[]` blocks from `mission-ops.yml`.
|
|
95
|
-
- `suggestions revise` reads structured file payloads or local per-thread `workspace.<actor_scope>` drafts only when `draft_status` is `submit`, `ready`, or `pending_submit`.
|
|
96
|
-
- `suggestions review` is owner-only, reads structured file payloads or local per-thread owner review intent, and refreshes `data/missions/*.yml` when approvals apply changes.
|
|
97
|
-
- `suggestions review` requires an explicit `review_action` of `request_changes`, `approve`, or `reject`; there is no persisted save-draft review action.
|
|
98
|
-
- suggestion notifications deep-link into the project Reviews workspace for the affected thread.
|
|
99
|
-
- create/revise/review resync `mission-ops.yml` by default unless `--no-sync` is passed.
|
|
100
|
-
- create/revise/review automatically send `X-Idempotency-Key` and a default `client_session_id` so retries stay deterministic and auditable.
|
|
101
|
-
- `--print-context` prints the JSON payload that would be submitted for create/revise/review.
|
|
102
|
-
- `mission-ops.yml` keeps synced thread lineage plus actor-local draft space in one file; terminal items fall out of `queue` after sync but remain in `threads[]`.
|
|
103
|
-
- `create-prd` is the deterministic PRD upload path, not an LLM generation command.
|
|
104
|
-
- `update-team` creates a project comment through `/api/project-assistant/project-comment`.
|
|
105
|
-
- `update-owner` sends a direct owner email through `/api/project-assistant/update-owner`.
|
|
106
|
-
- `update-owner` requires `--subject` plus body markdown from `--body-markdown`, `--body-file`, positional input, or `--stdin`.
|
|
107
|
-
- `update-client` creates a client update draft through `/api/project-assistant/client-update-drafts`.
|
|
108
|
-
- `update-client` requires `--subject` plus body markdown from `--body-markdown`, `--body-file`, positional input, or `--stdin`.
|
|
109
|
-
- `update-client` accepts optional `--target-contact-id` repeats or `--target-contact-ids <id1,id2>`.
|
|
110
|
-
- If no linked client contacts exist, the backend falls back to the project owner for internal projects.
|
|
111
|
-
- `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
|
|
112
|
-
- `--with-diff` includes per-repo diagnostics in `print-context` payload:
|
|
113
|
-
- missing repo directories
|
|
114
|
-
- per-repo errors (for example fetch or command failures)
|
|
115
|
-
- clean/no-change repo summaries
|
|
116
|
-
- `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
|
|
10
|
+
This package exists so the public wrapper can stay small and versioned cleanly. It is not the recommended public install target.
|
|
117
11
|
|
|
118
|
-
|
|
119
|
-
- Required: `MYTE_API_KEY`, a PRD markdown body, and a title.
|
|
120
|
-
- Accepts one file or many files per command. The CLI uses `/project-assistant/create-prd` for one item and automatically uses the batch upload path for multi-file requests.
|
|
121
|
-
- Title source: `myte-kanban.title`, the first markdown `# Heading`, or `--title`.
|
|
122
|
-
- Description source: `myte-kanban.description` or `--description`.
|
|
123
|
-
- The markdown body (`prd_markdown`, or the body portion of `ticket_markdown`) is stored verbatim as PRD content and is what the backend uses to build the PRD DOCX.
|
|
124
|
-
- Legacy `feedback_text` is still accepted for older payloads, but new callers should use `description`.
|
|
125
|
-
- Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
|
|
12
|
+
## Requirements
|
|
126
13
|
|
|
127
|
-
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
- `
|
|
131
|
-
- `npx myte run-qaqc --mission-ids "M001,M002" --wait --sync`
|
|
132
|
-
- `npx myte mission status --mission-ids "M001,M002" --status done`
|
|
133
|
-
- `npx myte sync-qaqc`
|
|
134
|
-
- `npx myte feedback-sync`
|
|
135
|
-
- `npx myte suggestions sync`
|
|
136
|
-
- `npx myte suggestions create --file ./changes/create.yml`
|
|
137
|
-
- `npx myte suggestions create`
|
|
138
|
-
- `npx myte suggestions revise`
|
|
139
|
-
- `npx myte suggestions review`
|
|
140
|
-
- `npx myte bootstrap --dry-run --json`
|
|
141
|
-
- `npx myte sync-qaqc --dry-run --json`
|
|
142
|
-
- `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
|
|
143
|
-
- `npx myte create-prd ./drafts/auth-prd.md ./drafts/billing-prd.md`
|
|
144
|
-
- `npx myte create-prd ./drafts/auth-prd.md --print-context`
|
|
145
|
-
- `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
146
|
-
- `npx myte update-owner --subject "QAQC progress" --body-file ./updates/owner.md`
|
|
147
|
-
- `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
14
|
+
- Node `18+`
|
|
15
|
+
- `MYTE_API_KEY` for project-scoped commands
|
|
16
|
+
- `MYTEAI_API_KEY` for `myte ai`
|
|
17
|
+
- `git` only when using `query --with-diff`
|
|
148
18
|
|
|
149
|
-
|
|
19
|
+
## Behavior Summary
|
|
150
20
|
|
|
151
|
-
|
|
21
|
+
- Snapshot-style commands such as `bootstrap`, `sync-qaqc`, `feedback-sync`, and `suggestions sync` write local `MyteCommandCenter` data.
|
|
22
|
+
- `query --with-diff` requires project repos to be configured for diff collection and fails fast when no matching local project repo can be resolved.
|
|
23
|
+
- Public package documentation is intentionally minimal. Internal rollout and design notes are not part of the npm package contract.
|
package/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - Auth: MYTE_API_KEY (project key) from `.env` or env
|
|
6
6
|
* - Default API: https://api.myte.dev (override with MYTE_API_BASE or --base-url)
|
|
7
|
-
* - Deterministic diffs: fetch
|
|
7
|
+
* - Deterministic diffs: fetch project config, resolve local project repos, then collect scoped git context
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require("fs");
|
|
@@ -194,7 +194,7 @@ function parseArgs(argv) {
|
|
|
194
194
|
|
|
195
195
|
function printHelp() {
|
|
196
196
|
const text = [
|
|
197
|
-
"myte - Myte
|
|
197
|
+
"myte - Myte CLI",
|
|
198
198
|
"",
|
|
199
199
|
"Usage:",
|
|
200
200
|
" myte query \"<text>\" [--with-diff] [--context \"...\"]",
|
|
@@ -232,13 +232,13 @@ function printHelp() {
|
|
|
232
232
|
" - Set MYTEAI_API_KEY in a workspace .env (or env var) for `myte ai`",
|
|
233
233
|
"",
|
|
234
234
|
"bootstrap contract:",
|
|
235
|
-
" - Run from
|
|
235
|
+
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
236
236
|
" - Writes MyteCommandCenter/data/project.yml plus phases, epics, stories, and missions locally",
|
|
237
237
|
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
238
238
|
" - Mission cards include richer execution context like complexity, estimated_hours, due_date, subtasks, technical_requirements, resources_needed, labels, and normalized test_cases",
|
|
239
239
|
"",
|
|
240
240
|
"sync-qaqc contract:",
|
|
241
|
-
" - Run from
|
|
241
|
+
" - Run from any workspace where you want local MyteCommandCenter data written",
|
|
242
242
|
" - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc.yml",
|
|
243
243
|
" - Writes the active mission QAQC working set into one deterministic file: MyteCommandCenter/data/qaqc.yml",
|
|
244
244
|
"",
|
|
@@ -285,13 +285,13 @@ function printHelp() {
|
|
|
285
285
|
" - If the project has no linked client contacts, the backend falls back to the project owner for internal projects",
|
|
286
286
|
"",
|
|
287
287
|
"feedback-sync contract:",
|
|
288
|
-
" - Runs from
|
|
289
|
-
" - Syncs
|
|
288
|
+
" - Runs from any workspace where you want local MyteCommandCenter data written",
|
|
289
|
+
" - Syncs pending feedback by default so local Command Center data stays focused on active work",
|
|
290
290
|
" - Writes project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
|
|
291
291
|
" - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
|
|
292
292
|
"",
|
|
293
293
|
"Options:",
|
|
294
|
-
" --with-diff Include deterministic git diffs (project-scoped)",
|
|
294
|
+
" --with-diff Include deterministic git diffs (project-scoped; fails fast if no project repos are configured or resolved)",
|
|
295
295
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
296
296
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
297
297
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
@@ -299,7 +299,7 @@ function printHelp() {
|
|
|
299
299
|
" --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
|
|
300
300
|
" --max-output-tokens Output token cap for `myte ai` simple queries",
|
|
301
301
|
" --temperature <num> Temperature for `myte ai` simple queries",
|
|
302
|
-
" --output-dir <path> Command Center output directory (default: <
|
|
302
|
+
" --output-dir <path> Command Center output directory (default: <current-workspace>/MyteCommandCenter)",
|
|
303
303
|
" --file <path> YAML/JSON payload file for suggestions create/revise/review",
|
|
304
304
|
" --stdin Read supported command content from stdin instead of inline text or a file path",
|
|
305
305
|
" --title <text> Override PRD title for raw markdown uploads",
|
|
@@ -310,7 +310,7 @@ function printHelp() {
|
|
|
310
310
|
" --body-file <path> Read update-owner or update-client markdown body from a file",
|
|
311
311
|
" --target-contact-id Add one client contact ObjectId (repeatable)",
|
|
312
312
|
" --target-contact-ids Comma-separated client contact ObjectIds",
|
|
313
|
-
" --status <value> For mission status: required target status (todo|in_progress|done). For feedback-sync: optional filter.",
|
|
313
|
+
" --status <value> For mission status: required target status (todo|in_progress|done). For feedback-sync: optional filter (default: Pending).",
|
|
314
314
|
" --source <value> Feedback source filter for feedback-sync",
|
|
315
315
|
" --with-prd-text Include extracted PRD text so local PRD files are materialized during feedback-sync (default: on)",
|
|
316
316
|
" --no-with-prd-text Skip PRD text download and write only feedback metadata/comment turns",
|
|
@@ -589,18 +589,25 @@ function summarizeDiffDiagnosticsForContext(diagnostics) {
|
|
|
589
589
|
project_id: diagnostics.project_id || null,
|
|
590
590
|
mode: diagnostics.mode,
|
|
591
591
|
requested_repos: diagnostics.requested_repo_names || [],
|
|
592
|
+
requested_repo_bindings: diagnostics.requested_repo_bindings || [],
|
|
592
593
|
found_repos: diagnostics.found_repos || [],
|
|
593
594
|
missing_repos: diagnostics.missing_repos || [],
|
|
594
595
|
collected_any: Boolean(diagnostics.collected_any),
|
|
595
596
|
truncation: diagnostics.truncated ? "truncated" : "full",
|
|
596
597
|
repos: repos.map((repo) => ({
|
|
597
598
|
name: repo.name,
|
|
599
|
+
role: repo.role || null,
|
|
598
600
|
status: repo.status || "ok",
|
|
599
601
|
head_branch: repo.head_branch || null,
|
|
600
602
|
base_ref: repo.base_ref || null,
|
|
603
|
+
base_ref_label: repo.base_ref_label || repo.base_ref || null,
|
|
604
|
+
baseline_branch: repo.baseline_branch || null,
|
|
605
|
+
compare_mode: repo.compare_mode || "local_origin",
|
|
606
|
+
compare_remote_url: repo.compare_remote_url || null,
|
|
601
607
|
has_changes: Boolean(repo.has_changes),
|
|
602
608
|
changed_blocks: repo.changed_blocks || {},
|
|
603
609
|
untracked_file_count: repo.untracked_file_count || 0,
|
|
610
|
+
matched_by: repo.matched_by || [],
|
|
604
611
|
error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
|
|
605
612
|
})),
|
|
606
613
|
warnings: diagnostics.warnings || [],
|
|
@@ -642,8 +649,29 @@ const IGNORED_PATTERNS = [
|
|
|
642
649
|
/^out\//,
|
|
643
650
|
];
|
|
644
651
|
|
|
652
|
+
const DIFF_MARKDOWN_ALLOWLIST = new Set([
|
|
653
|
+
"README.md",
|
|
654
|
+
"AGENTS.md",
|
|
655
|
+
"CodexContext.MD",
|
|
656
|
+
"CLAUDE.md",
|
|
657
|
+
"GEMINI.md",
|
|
658
|
+
]);
|
|
659
|
+
|
|
660
|
+
function normalizeRepoPathForDiff(p) {
|
|
661
|
+
return String(p || "").replace(/\\/g, "/");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function shouldIgnoreMarkdownPath(p) {
|
|
665
|
+
const normalized = normalizeRepoPathForDiff(p);
|
|
666
|
+
if (!/\.md$/i.test(normalized)) return false;
|
|
667
|
+
const base = path.posix.basename(normalized);
|
|
668
|
+
return !DIFF_MARKDOWN_ALLOWLIST.has(base);
|
|
669
|
+
}
|
|
670
|
+
|
|
645
671
|
function shouldIgnore(p) {
|
|
646
|
-
|
|
672
|
+
const normalized = normalizeRepoPathForDiff(p);
|
|
673
|
+
if (shouldIgnoreMarkdownPath(normalized)) return true;
|
|
674
|
+
return IGNORED_PATTERNS.some((re) => re.test(normalized));
|
|
647
675
|
}
|
|
648
676
|
|
|
649
677
|
function hasGitDir(repoPath) {
|
|
@@ -833,12 +861,359 @@ function resolveConfiguredRepos(repoNames) {
|
|
|
833
861
|
return { mode: "none", root: null, repos: [], missing: names };
|
|
834
862
|
}
|
|
835
863
|
|
|
836
|
-
function
|
|
864
|
+
function findGitTopLevel(startPath) {
|
|
865
|
+
const result = runGitRaw(startPath, ["rev-parse", "--show-toplevel"]);
|
|
866
|
+
if (!result.ok) return null;
|
|
867
|
+
const value = String(result.stdout || "").trim();
|
|
868
|
+
return value || null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function normalizeRemoteFingerprint(value) {
|
|
872
|
+
const raw = String(value || "").trim();
|
|
873
|
+
if (!raw) return "";
|
|
874
|
+
|
|
875
|
+
const cleaned = raw.replace(/\\/g, "/").replace(/\.git$/i, "");
|
|
876
|
+
const looksLikeLocalPath =
|
|
877
|
+
!cleaned.includes("://") &&
|
|
878
|
+
(/^[a-zA-Z]:\//.test(cleaned) || cleaned.startsWith("/") || cleaned.startsWith("./") || cleaned.startsWith("../"));
|
|
879
|
+
if (looksLikeLocalPath) {
|
|
880
|
+
return `local/${cleaned.toLowerCase()}`;
|
|
881
|
+
}
|
|
882
|
+
const sshMatch = cleaned.match(/^(?:.+@)?([^:/]+)[:/](.+)$/);
|
|
883
|
+
if (!cleaned.includes("://") && sshMatch) {
|
|
884
|
+
const host = String(sshMatch[1] || "").trim().toLowerCase();
|
|
885
|
+
const repoPath = String(sshMatch[2] || "")
|
|
886
|
+
.trim()
|
|
887
|
+
.replace(/^\/+/, "")
|
|
888
|
+
.replace(/\/+$/, "")
|
|
889
|
+
.toLowerCase();
|
|
890
|
+
return host && repoPath ? `${host}/${repoPath}` : "";
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
try {
|
|
894
|
+
const parsed = new URL(cleaned);
|
|
895
|
+
const host = String(parsed.hostname || "").trim().toLowerCase();
|
|
896
|
+
const repoPath = String(parsed.pathname || "")
|
|
897
|
+
.trim()
|
|
898
|
+
.replace(/^\/+/, "")
|
|
899
|
+
.replace(/\/+$/, "")
|
|
900
|
+
.toLowerCase();
|
|
901
|
+
return host && repoPath ? `${host}/${repoPath}` : "";
|
|
902
|
+
} catch {
|
|
903
|
+
return cleaned.toLowerCase();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function uniqueNormalizedStrings(values) {
|
|
908
|
+
const seen = new Set();
|
|
909
|
+
const ordered = [];
|
|
910
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
911
|
+
const text = String(value || "").trim();
|
|
912
|
+
if (!text) continue;
|
|
913
|
+
const key = text.toLowerCase();
|
|
914
|
+
if (seen.has(key)) continue;
|
|
915
|
+
seen.add(key);
|
|
916
|
+
ordered.push(text);
|
|
917
|
+
}
|
|
918
|
+
return ordered;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function normalizeRepoBindingEntry(binding, index = 0) {
|
|
922
|
+
if (!binding || typeof binding !== "object") return null;
|
|
923
|
+
|
|
924
|
+
const role = String(binding.role || "").trim().toLowerCase();
|
|
925
|
+
const canonicalRepoName = String(binding.canonical_repo_name || "").trim();
|
|
926
|
+
const canonicalRemoteUrl = String(binding.canonical_remote_url || "").trim();
|
|
927
|
+
const clientRepoName = String(binding.client_repo_name || "").trim();
|
|
928
|
+
const clientRemoteUrl = String(binding.client_remote_url || "").trim();
|
|
929
|
+
const localRepoAliases = uniqueNormalizedStrings(binding.local_repo_aliases || []);
|
|
930
|
+
const baselineBranch = String(binding.baseline_branch || "").trim();
|
|
931
|
+
|
|
932
|
+
const remoteFingerprints = uniqueNormalizedStrings([
|
|
933
|
+
...(Array.isArray(binding.remote_fingerprints) ? binding.remote_fingerprints : []),
|
|
934
|
+
normalizeRemoteFingerprint(canonicalRemoteUrl),
|
|
935
|
+
normalizeRemoteFingerprint(clientRemoteUrl),
|
|
936
|
+
]);
|
|
937
|
+
|
|
938
|
+
const matchingNames = uniqueNormalizedStrings([
|
|
939
|
+
...(Array.isArray(binding.matching_names) ? binding.matching_names : []),
|
|
940
|
+
canonicalRepoName,
|
|
941
|
+
clientRepoName,
|
|
942
|
+
...localRepoAliases,
|
|
943
|
+
]);
|
|
944
|
+
|
|
945
|
+
if (!role && !matchingNames.length && !remoteFingerprints.length) return null;
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
role: role || `repo_${index + 1}`,
|
|
949
|
+
canonical_repo_name: canonicalRepoName || null,
|
|
950
|
+
canonical_remote_url: canonicalRemoteUrl || null,
|
|
951
|
+
client_repo_name: clientRepoName || null,
|
|
952
|
+
client_remote_url: clientRemoteUrl || null,
|
|
953
|
+
local_repo_aliases: localRepoAliases,
|
|
954
|
+
baseline_branch: baselineBranch || null,
|
|
955
|
+
matching_names: matchingNames,
|
|
956
|
+
remote_fingerprints: remoteFingerprints,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function normalizeRepoBindings(bindings) {
|
|
961
|
+
const normalized = [];
|
|
962
|
+
const seenRoles = new Set();
|
|
963
|
+
for (const [index, binding] of (Array.isArray(bindings) ? bindings : []).entries()) {
|
|
964
|
+
const item = normalizeRepoBindingEntry(binding, index);
|
|
965
|
+
if (!item) continue;
|
|
966
|
+
const roleKey = String(item.role || "").trim().toLowerCase();
|
|
967
|
+
if (seenRoles.has(roleKey)) continue;
|
|
968
|
+
seenRoles.add(roleKey);
|
|
969
|
+
normalized.push(item);
|
|
970
|
+
}
|
|
971
|
+
return normalized;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function getRepoRemoteFingerprints(repoPath) {
|
|
975
|
+
const output = runGitTry(repoPath, ["remote", "-v"]);
|
|
976
|
+
if (!output) return [];
|
|
977
|
+
const fingerprints = [];
|
|
978
|
+
for (const line of String(output).split(/\r?\n/)) {
|
|
979
|
+
const parts = line.trim().split(/\s+/);
|
|
980
|
+
if (parts.length < 2) continue;
|
|
981
|
+
const normalized = normalizeRemoteFingerprint(parts[1]);
|
|
982
|
+
if (normalized) fingerprints.push(normalized);
|
|
983
|
+
}
|
|
984
|
+
return uniqueNormalizedStrings(fingerprints);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function listGitRepoCandidates(searchRoot, currentRepoRoot) {
|
|
988
|
+
const candidates = [];
|
|
989
|
+
const seen = new Set();
|
|
990
|
+
|
|
991
|
+
function pushCandidate(absPath, rootForDir) {
|
|
992
|
+
const abs = path.resolve(absPath);
|
|
993
|
+
if (!hasGitDir(abs) || seen.has(abs)) return;
|
|
994
|
+
seen.add(abs);
|
|
995
|
+
const rootBase = rootForDir ? path.resolve(rootForDir) : path.dirname(abs);
|
|
996
|
+
const rel = path.relative(rootBase, abs);
|
|
997
|
+
const dir = rel && rel !== "" ? rel : ".";
|
|
998
|
+
const repoName = path.basename(abs);
|
|
999
|
+
candidates.push({
|
|
1000
|
+
abs,
|
|
1001
|
+
dir,
|
|
1002
|
+
name: repoName,
|
|
1003
|
+
prefix: `${repoName.replace(/\\\\/g, "/")}/`,
|
|
1004
|
+
remote_fingerprints: getRepoRemoteFingerprints(abs),
|
|
1005
|
+
is_current_repo: currentRepoRoot ? path.resolve(currentRepoRoot) === abs : false,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (currentRepoRoot) {
|
|
1010
|
+
pushCandidate(currentRepoRoot, path.dirname(currentRepoRoot));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
pushCandidate(searchRoot, path.dirname(searchRoot));
|
|
1014
|
+
|
|
1015
|
+
let entries = [];
|
|
1016
|
+
try {
|
|
1017
|
+
entries = fs.readdirSync(searchRoot, { withFileTypes: true });
|
|
1018
|
+
} catch {
|
|
1019
|
+
entries = [];
|
|
1020
|
+
}
|
|
1021
|
+
for (const entry of entries) {
|
|
1022
|
+
if (!entry || !entry.isDirectory || !entry.isDirectory()) continue;
|
|
1023
|
+
const abs = path.join(searchRoot, entry.name);
|
|
1024
|
+
pushCandidate(abs, searchRoot);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return candidates;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function scoreBindingCandidate(binding, candidate) {
|
|
1031
|
+
const candidateName = String(candidate?.name || "").trim().toLowerCase();
|
|
1032
|
+
const bindingNames = new Set((binding?.matching_names || []).map((name) => String(name || "").trim().toLowerCase()).filter(Boolean));
|
|
1033
|
+
const bindingRemotes = new Set((binding?.remote_fingerprints || []).map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));
|
|
1034
|
+
const candidateRemotes = new Set((candidate?.remote_fingerprints || []).map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));
|
|
1035
|
+
const canonicalRemoteFingerprint = normalizeRemoteFingerprint(binding?.canonical_remote_url).toLowerCase();
|
|
1036
|
+
const clientRemoteFingerprint = normalizeRemoteFingerprint(binding?.client_remote_url).toLowerCase();
|
|
1037
|
+
|
|
1038
|
+
let score = 0;
|
|
1039
|
+
const matchedBy = [];
|
|
1040
|
+
|
|
1041
|
+
if (binding.canonical_repo_name && candidateName === String(binding.canonical_repo_name).trim().toLowerCase()) {
|
|
1042
|
+
score = Math.max(score, 240);
|
|
1043
|
+
matchedBy.push("canonical_name");
|
|
1044
|
+
}
|
|
1045
|
+
if (binding.client_repo_name && candidateName === String(binding.client_repo_name).trim().toLowerCase()) {
|
|
1046
|
+
score = Math.max(score, 260);
|
|
1047
|
+
matchedBy.push("client_name");
|
|
1048
|
+
}
|
|
1049
|
+
if (bindingNames.has(candidateName)) {
|
|
1050
|
+
score = Math.max(score, 200);
|
|
1051
|
+
matchedBy.push("alias");
|
|
1052
|
+
}
|
|
1053
|
+
for (const remote of candidateRemotes) {
|
|
1054
|
+
if (clientRemoteFingerprint && remote === clientRemoteFingerprint) {
|
|
1055
|
+
score = Math.max(score, 340);
|
|
1056
|
+
matchedBy.push("client_remote");
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
if (canonicalRemoteFingerprint && remote === canonicalRemoteFingerprint) {
|
|
1060
|
+
score = Math.max(score, 300);
|
|
1061
|
+
matchedBy.push("canonical_remote");
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
if (bindingRemotes.has(remote)) {
|
|
1065
|
+
score = Math.max(score, 320);
|
|
1066
|
+
matchedBy.push("remote");
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return { score, matchedBy: uniqueNormalizedStrings(matchedBy) };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function resolveConfiguredRepoBindings(bindings) {
|
|
1075
|
+
const normalizedBindings = normalizeRepoBindings(bindings);
|
|
1076
|
+
if (!normalizedBindings.length) {
|
|
1077
|
+
return { mode: "none", root: null, repos: [], missing: [], bindings: [] };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const cwd = process.cwd();
|
|
1081
|
+
const currentRepoRoot = findGitTopLevel(cwd);
|
|
1082
|
+
const scanStart = currentRepoRoot ? path.dirname(currentRepoRoot) : cwd;
|
|
1083
|
+
|
|
1084
|
+
const ancestors = [];
|
|
1085
|
+
let cur = scanStart;
|
|
1086
|
+
for (let i = 0; i < 8; i += 1) {
|
|
1087
|
+
ancestors.push(cur);
|
|
1088
|
+
const parent = path.dirname(cur);
|
|
1089
|
+
if (parent === cur) break;
|
|
1090
|
+
cur = parent;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
let fallbackCandidates = currentRepoRoot ? listGitRepoCandidates(path.dirname(currentRepoRoot), currentRepoRoot) : [];
|
|
1094
|
+
let fallbackRoot = currentRepoRoot ? path.dirname(currentRepoRoot) : null;
|
|
1095
|
+
|
|
1096
|
+
for (const candidateRoot of ancestors) {
|
|
1097
|
+
const candidates = listGitRepoCandidates(candidateRoot, currentRepoRoot);
|
|
1098
|
+
if (!fallbackCandidates.length && candidates.length) {
|
|
1099
|
+
fallbackCandidates = candidates;
|
|
1100
|
+
fallbackRoot = candidateRoot;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const assigned = new Set();
|
|
1104
|
+
const found = [];
|
|
1105
|
+
for (const binding of normalizedBindings) {
|
|
1106
|
+
let best = null;
|
|
1107
|
+
for (const candidate of candidates) {
|
|
1108
|
+
if (assigned.has(candidate.abs)) continue;
|
|
1109
|
+
const match = scoreBindingCandidate(binding, candidate);
|
|
1110
|
+
if (!match.score) continue;
|
|
1111
|
+
if (!best || match.score > best.match.score) {
|
|
1112
|
+
best = { candidate, match };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (!best) continue;
|
|
1116
|
+
assigned.add(best.candidate.abs);
|
|
1117
|
+
found.push({
|
|
1118
|
+
name: best.candidate.name,
|
|
1119
|
+
dir: best.candidate.dir,
|
|
1120
|
+
abs: best.candidate.abs,
|
|
1121
|
+
prefix: best.candidate.prefix,
|
|
1122
|
+
role: binding.role,
|
|
1123
|
+
canonical_repo_name: binding.canonical_repo_name,
|
|
1124
|
+
canonical_remote_url: binding.canonical_remote_url,
|
|
1125
|
+
client_repo_name: binding.client_repo_name,
|
|
1126
|
+
client_remote_url: binding.client_remote_url,
|
|
1127
|
+
baseline_branch: binding.baseline_branch || null,
|
|
1128
|
+
matched_by: best.match.matchedBy,
|
|
1129
|
+
remote_fingerprints: best.candidate.remote_fingerprints,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (found.length) {
|
|
1134
|
+
const foundRoles = new Set(found.map((repo) => repo.role));
|
|
1135
|
+
const missing = normalizedBindings
|
|
1136
|
+
.filter((binding) => !foundRoles.has(binding.role))
|
|
1137
|
+
.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role);
|
|
1138
|
+
return {
|
|
1139
|
+
mode: currentRepoRoot && found.length === 1 && found[0].abs === currentRepoRoot ? "repo" : "workspace",
|
|
1140
|
+
root: candidateRoot,
|
|
1141
|
+
repos: found,
|
|
1142
|
+
missing,
|
|
1143
|
+
bindings: normalizedBindings,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return {
|
|
1149
|
+
mode: currentRepoRoot ? "repo" : "none",
|
|
1150
|
+
root: fallbackRoot,
|
|
1151
|
+
repos: [],
|
|
1152
|
+
missing: normalizedBindings.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role),
|
|
1153
|
+
bindings: normalizedBindings,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function fetchBaseBranches(repoPath, preferredBranch) {
|
|
1158
|
+
const preferred = String(preferredBranch || "").trim();
|
|
1159
|
+
if (preferred && runGitOk(repoPath, ["fetch", "origin", preferred, "--prune", "--quiet"])) return true;
|
|
837
1160
|
if (runGitOk(repoPath, ["fetch", "origin", "main", "master", "--prune", "--quiet"])) return true;
|
|
838
1161
|
return runGitOk(repoPath, ["fetch", "--all", "--prune", "--quiet"]);
|
|
839
1162
|
}
|
|
840
1163
|
|
|
841
|
-
function
|
|
1164
|
+
function buildExternalBaseRefName(remoteUrl, branch) {
|
|
1165
|
+
const branchSlug = String(branch || "main")
|
|
1166
|
+
.trim()
|
|
1167
|
+
.toLowerCase()
|
|
1168
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1169
|
+
.replace(/^-+|-+$/g, "") || "main";
|
|
1170
|
+
const digest = createHash("sha1")
|
|
1171
|
+
.update(`${String(remoteUrl || "").trim()}::${branchSlug}`)
|
|
1172
|
+
.digest("hex")
|
|
1173
|
+
.slice(0, 12);
|
|
1174
|
+
return `refs/myte/bases/${branchSlug}-${digest}`;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function fetchExternalBaseRef(repoPath, remoteUrl, preferredBranch) {
|
|
1178
|
+
const remote = String(remoteUrl || "").trim();
|
|
1179
|
+
if (!remote) return null;
|
|
1180
|
+
|
|
1181
|
+
const candidateBranches = uniqueNormalizedStrings([preferredBranch, "main", "master"]);
|
|
1182
|
+
for (const branch of candidateBranches) {
|
|
1183
|
+
const refName = buildExternalBaseRefName(remote, branch);
|
|
1184
|
+
if (runGitOk(repoPath, ["fetch", "--quiet", "--no-tags", remote, `${branch}:${refName}`])) {
|
|
1185
|
+
return { ref: refName, branch };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function resolveExternalCompareRemoteUrl(repo) {
|
|
1192
|
+
const canonicalRemoteUrl = String(repo?.canonical_remote_url || "").trim();
|
|
1193
|
+
const clientRemoteUrl = String(repo?.client_remote_url || "").trim();
|
|
1194
|
+
if (!canonicalRemoteUrl || !clientRemoteUrl) return null;
|
|
1195
|
+
|
|
1196
|
+
const canonicalFingerprint = normalizeRemoteFingerprint(canonicalRemoteUrl);
|
|
1197
|
+
const clientFingerprint = normalizeRemoteFingerprint(clientRemoteUrl);
|
|
1198
|
+
if (!canonicalFingerprint || !clientFingerprint || canonicalFingerprint === clientFingerprint) return null;
|
|
1199
|
+
|
|
1200
|
+
const candidateFingerprints = new Set(
|
|
1201
|
+
(Array.isArray(repo?.remote_fingerprints) ? repo.remote_fingerprints : [])
|
|
1202
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
1203
|
+
.filter(Boolean)
|
|
1204
|
+
);
|
|
1205
|
+
if (!candidateFingerprints.has(clientFingerprint.toLowerCase())) return null;
|
|
1206
|
+
if (candidateFingerprints.has(canonicalFingerprint.toLowerCase())) return null;
|
|
1207
|
+
|
|
1208
|
+
return canonicalRemoteUrl;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function resolveBaseRef(repoPath, preferredBranch) {
|
|
1212
|
+
const preferred = String(preferredBranch || "").trim();
|
|
1213
|
+
if (preferred) {
|
|
1214
|
+
if (runGitOk(repoPath, ["rev-parse", "--verify", `refs/remotes/origin/${preferred}`])) return `origin/${preferred}`;
|
|
1215
|
+
if (runGitOk(repoPath, ["rev-parse", "--verify", `refs/heads/${preferred}`])) return preferred;
|
|
1216
|
+
}
|
|
842
1217
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/main"])) return "origin/main";
|
|
843
1218
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/master"])) return "origin/master";
|
|
844
1219
|
if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/heads/main"])) return "main";
|
|
@@ -849,13 +1224,24 @@ function resolveBaseRef(repoPath) {
|
|
|
849
1224
|
function collectGitDiffWithDiagnostics({
|
|
850
1225
|
projectId,
|
|
851
1226
|
repoNames,
|
|
1227
|
+
repoBindings,
|
|
852
1228
|
maxChars,
|
|
853
1229
|
fetchRemote = true,
|
|
854
1230
|
} = {}) {
|
|
855
1231
|
const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
1232
|
+
const configuredBindings = normalizeRepoBindings(repoBindings);
|
|
856
1233
|
const diagnostics = {
|
|
857
1234
|
project_id: projectId || null,
|
|
858
1235
|
requested_repo_names: configuredRepos,
|
|
1236
|
+
requested_repo_bindings: configuredBindings.map((binding) => ({
|
|
1237
|
+
role: binding.role,
|
|
1238
|
+
canonical_repo_name: binding.canonical_repo_name,
|
|
1239
|
+
canonical_remote_url: binding.canonical_remote_url,
|
|
1240
|
+
client_repo_name: binding.client_repo_name,
|
|
1241
|
+
client_remote_url: binding.client_remote_url,
|
|
1242
|
+
local_repo_aliases: binding.local_repo_aliases,
|
|
1243
|
+
baseline_branch: binding.baseline_branch,
|
|
1244
|
+
})),
|
|
859
1245
|
fetch_remote: Boolean(fetchRemote),
|
|
860
1246
|
mode: "none",
|
|
861
1247
|
search_root: null,
|
|
@@ -870,7 +1256,9 @@ function collectGitDiffWithDiagnostics({
|
|
|
870
1256
|
};
|
|
871
1257
|
|
|
872
1258
|
try {
|
|
873
|
-
const resolved =
|
|
1259
|
+
const resolved = configuredBindings.length
|
|
1260
|
+
? resolveConfiguredRepoBindings(configuredBindings)
|
|
1261
|
+
: resolveConfiguredRepos(configuredRepos);
|
|
874
1262
|
diagnostics.mode = resolved.mode || "none";
|
|
875
1263
|
diagnostics.search_root = resolved.root || null;
|
|
876
1264
|
diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
|
|
@@ -888,7 +1276,15 @@ function collectGitDiffWithDiagnostics({
|
|
|
888
1276
|
const sections = [];
|
|
889
1277
|
if (projectId) sections.push(`# Project: ${projectId}`);
|
|
890
1278
|
sections.push(`# Mode: ${resolved.mode}`);
|
|
891
|
-
|
|
1279
|
+
if (configuredBindings.length) {
|
|
1280
|
+
sections.push(
|
|
1281
|
+
`# Configured repo bindings: ${configuredBindings
|
|
1282
|
+
.map((binding) => `${binding.role}=${binding.client_repo_name || binding.canonical_repo_name || binding.role}`)
|
|
1283
|
+
.join(", ")}`
|
|
1284
|
+
);
|
|
1285
|
+
} else {
|
|
1286
|
+
sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
|
|
1287
|
+
}
|
|
892
1288
|
if (resolved.missing && resolved.missing.length) {
|
|
893
1289
|
sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
|
|
894
1290
|
}
|
|
@@ -900,8 +1296,12 @@ function collectGitDiffWithDiagnostics({
|
|
|
900
1296
|
dir: repo.dir || ".",
|
|
901
1297
|
root: repo.abs,
|
|
902
1298
|
status: "ok",
|
|
1299
|
+
role: repo.role || null,
|
|
903
1300
|
head_branch: null,
|
|
904
1301
|
base_ref: null,
|
|
1302
|
+
base_ref_label: null,
|
|
1303
|
+
compare_mode: "local_origin",
|
|
1304
|
+
compare_remote_url: null,
|
|
905
1305
|
has_changes: false,
|
|
906
1306
|
changed_blocks: {
|
|
907
1307
|
base_vs_head: false,
|
|
@@ -914,22 +1314,55 @@ function collectGitDiffWithDiagnostics({
|
|
|
914
1314
|
};
|
|
915
1315
|
|
|
916
1316
|
const { name, dir, abs, prefix } = repo;
|
|
917
|
-
const
|
|
1317
|
+
const externalCompareRemoteUrl = resolveExternalCompareRemoteUrl(repo);
|
|
1318
|
+
const fetchDiag = {
|
|
1319
|
+
attempted: false,
|
|
1320
|
+
ok: false,
|
|
1321
|
+
message: "",
|
|
1322
|
+
compare_mode: externalCompareRemoteUrl ? "external_remote" : "local_origin",
|
|
1323
|
+
compare_remote_url: externalCompareRemoteUrl || null,
|
|
1324
|
+
};
|
|
1325
|
+
let baseRef = null;
|
|
1326
|
+
let baseRefLabel = null;
|
|
918
1327
|
if (fetchRemote) {
|
|
919
1328
|
fetchDiag.attempted = true;
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
fetchDiag.
|
|
923
|
-
|
|
924
|
-
|
|
1329
|
+
if (externalCompareRemoteUrl) {
|
|
1330
|
+
const externalBase = fetchExternalBaseRef(abs, externalCompareRemoteUrl, repo.baseline_branch);
|
|
1331
|
+
fetchDiag.ok = Boolean(externalBase?.ref);
|
|
1332
|
+
if (externalBase?.ref) {
|
|
1333
|
+
baseRef = externalBase.ref;
|
|
1334
|
+
baseRefLabel = `${externalBase.branch}@canonical`;
|
|
1335
|
+
} else {
|
|
1336
|
+
fetchDiag.message = `failed to refresh canonical compare branch from ${externalCompareRemoteUrl}`;
|
|
1337
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
1338
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
1339
|
+
}
|
|
1340
|
+
} else {
|
|
1341
|
+
fetchDiag.ok = fetchBaseBranches(abs, repo.baseline_branch);
|
|
1342
|
+
if (!fetchDiag.ok) {
|
|
1343
|
+
fetchDiag.message = "failed to refresh origin main/master";
|
|
1344
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
1345
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
1346
|
+
}
|
|
925
1347
|
}
|
|
926
1348
|
}
|
|
927
1349
|
|
|
928
1350
|
const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
929
|
-
|
|
1351
|
+
if (!baseRef && !externalCompareRemoteUrl) {
|
|
1352
|
+
baseRef = resolveBaseRef(abs, repo.baseline_branch);
|
|
1353
|
+
}
|
|
930
1354
|
repoSummary.head_branch = headBranch;
|
|
931
1355
|
repoSummary.base_ref = baseRef || "base-unresolved";
|
|
1356
|
+
repoSummary.base_ref_label = baseRefLabel || repoSummary.base_ref;
|
|
932
1357
|
repoSummary.fetch = fetchDiag;
|
|
1358
|
+
repoSummary.compare_mode = fetchDiag.compare_mode;
|
|
1359
|
+
repoSummary.compare_remote_url = fetchDiag.compare_remote_url;
|
|
1360
|
+
if (repo.matched_by) repoSummary.matched_by = repo.matched_by;
|
|
1361
|
+
if (repo.baseline_branch) repoSummary.baseline_branch = repo.baseline_branch;
|
|
1362
|
+
if (repo.canonical_repo_name) repoSummary.canonical_repo_name = repo.canonical_repo_name;
|
|
1363
|
+
if (repo.canonical_remote_url) repoSummary.canonical_remote_url = repo.canonical_remote_url;
|
|
1364
|
+
if (repo.client_repo_name) repoSummary.client_repo_name = repo.client_repo_name;
|
|
1365
|
+
if (repo.client_remote_url) repoSummary.client_remote_url = repo.client_remote_url;
|
|
933
1366
|
|
|
934
1367
|
let baseDiff = "";
|
|
935
1368
|
if (baseRef) {
|
|
@@ -999,8 +1432,16 @@ function collectGitDiffWithDiagnostics({
|
|
|
999
1432
|
repoSummary.status = "partial";
|
|
1000
1433
|
}
|
|
1001
1434
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1435
|
+
const headingParts = [`${name} (${dir || "."})`];
|
|
1436
|
+
if (repo.role) headingParts.push(`role=${repo.role}`);
|
|
1437
|
+
let section = `### ${headingParts.join(" | ")}\n\n`;
|
|
1438
|
+
section += `# ?? Base vs HEAD (${repoSummary.base_ref_label}...HEAD | head=${headBranch})\n`;
|
|
1439
|
+
if (repoSummary.matched_by?.length) {
|
|
1440
|
+
section += `# ?? Matched by ${repoSummary.matched_by.join(", ")}\n`;
|
|
1441
|
+
}
|
|
1442
|
+
if (repoSummary.compare_remote_url) {
|
|
1443
|
+
section += `# ?? Compare remote ${repoSummary.compare_remote_url}\n`;
|
|
1444
|
+
}
|
|
1004
1445
|
if (baseDiff) section += `${baseDiff}\n\n`;
|
|
1005
1446
|
if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
|
|
1006
1447
|
if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
|
|
@@ -1469,7 +1910,7 @@ async function runRunQaqc(args) {
|
|
|
1469
1910
|
|
|
1470
1911
|
let resolved;
|
|
1471
1912
|
try {
|
|
1472
|
-
resolved =
|
|
1913
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
1473
1914
|
} catch (err) {
|
|
1474
1915
|
console.error(err?.message || err);
|
|
1475
1916
|
process.exit(1);
|
|
@@ -2063,6 +2504,20 @@ function resolveBootstrapWorkspace(repoNames) {
|
|
|
2063
2504
|
return resolved;
|
|
2064
2505
|
}
|
|
2065
2506
|
|
|
2507
|
+
function resolvePortableWorkspace(repoNames) {
|
|
2508
|
+
const configured = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
2509
|
+
const resolved = resolveConfiguredRepos(configured);
|
|
2510
|
+
if (resolved.root) {
|
|
2511
|
+
return resolved;
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
mode: "cwd",
|
|
2515
|
+
root: process.cwd(),
|
|
2516
|
+
repos: [],
|
|
2517
|
+
missing: configured,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2066
2521
|
function resolveCommandCenterRoots(wrapperRoot, outputDir) {
|
|
2067
2522
|
const targetRoot = outputDir
|
|
2068
2523
|
? path.resolve(process.cwd(), String(outputDir))
|
|
@@ -2545,7 +3000,7 @@ async function resolveSuggestionsOutputContext({ args, key, apiBase, timeoutMs,
|
|
|
2545
3000
|
const repoNames = Array.isArray(snapshot?.repo_names) && snapshot.repo_names.length
|
|
2546
3001
|
? snapshot.repo_names
|
|
2547
3002
|
: (await fetchProjectConfig({ apiBase, key, timeoutMs })).repo_names || [];
|
|
2548
|
-
const resolved =
|
|
3003
|
+
const resolved = resolvePortableWorkspace(repoNames);
|
|
2549
3004
|
return {
|
|
2550
3005
|
wrapperRoot: resolved.root,
|
|
2551
3006
|
targetRoot: path.join(resolved.root, "MyteCommandCenter"),
|
|
@@ -3155,11 +3610,15 @@ async function runConfig(args) {
|
|
|
3155
3610
|
}
|
|
3156
3611
|
|
|
3157
3612
|
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
3158
|
-
const
|
|
3613
|
+
const repoBindings = Array.isArray(cfg.repo_bindings) ? cfg.repo_bindings : [];
|
|
3614
|
+
const resolved = repoBindings.length
|
|
3615
|
+
? resolveConfiguredRepoBindings(repoBindings)
|
|
3616
|
+
: resolveConfiguredRepos(repoNames);
|
|
3159
3617
|
const payload = {
|
|
3160
3618
|
api_base: apiBase,
|
|
3161
3619
|
project_id: cfg.project_id,
|
|
3162
3620
|
repo_names: repoNames,
|
|
3621
|
+
repo_bindings: repoBindings,
|
|
3163
3622
|
local: {
|
|
3164
3623
|
mode: resolved.mode,
|
|
3165
3624
|
root: resolved.root,
|
|
@@ -3174,6 +3633,13 @@ async function runConfig(args) {
|
|
|
3174
3633
|
console.log(`Project: ${payload.project_id || "(unknown)"}`);
|
|
3175
3634
|
console.log(`API base: ${payload.api_base}`);
|
|
3176
3635
|
console.log(`Configured repos: ${repoNames.join(", ") || "(none)"}`);
|
|
3636
|
+
if (repoBindings.length) {
|
|
3637
|
+
console.log(
|
|
3638
|
+
`Repo bindings: ${repoBindings
|
|
3639
|
+
.map((binding) => `${binding.role}=${binding.client_repo_name || binding.canonical_repo_name || binding.role}`)
|
|
3640
|
+
.join(", ")}`
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3177
3643
|
console.log(`Local mode: ${payload.local.mode}`);
|
|
3178
3644
|
if (payload.local.found.length) console.log(`Found locally: ${payload.local.found.join(", ")}`);
|
|
3179
3645
|
if (payload.local.missing.length) console.log(`Missing locally: ${payload.local.missing.join(", ")}`);
|
|
@@ -3209,7 +3675,7 @@ async function runBootstrap(args) {
|
|
|
3209
3675
|
|
|
3210
3676
|
let resolved;
|
|
3211
3677
|
try {
|
|
3212
|
-
resolved =
|
|
3678
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3213
3679
|
} catch (err) {
|
|
3214
3680
|
console.error(err?.message || err);
|
|
3215
3681
|
process.exit(1);
|
|
@@ -3303,7 +3769,7 @@ async function runSyncQaqc(args) {
|
|
|
3303
3769
|
|
|
3304
3770
|
let resolved;
|
|
3305
3771
|
try {
|
|
3306
|
-
resolved =
|
|
3772
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3307
3773
|
} catch (err) {
|
|
3308
3774
|
console.error(err?.message || err);
|
|
3309
3775
|
process.exit(1);
|
|
@@ -3376,7 +3842,7 @@ async function runFeedbackSync(args) {
|
|
|
3376
3842
|
const apiBase = resolveApiBase(args);
|
|
3377
3843
|
const includePrdText = resolveBooleanFlag(args, "with-prd-text", true);
|
|
3378
3844
|
const filters = {
|
|
3379
|
-
status: firstNonEmptyString(args.status) || "",
|
|
3845
|
+
status: firstNonEmptyString(args.status) || "Pending",
|
|
3380
3846
|
source: firstNonEmptyString(args.source) || "",
|
|
3381
3847
|
includePrdText,
|
|
3382
3848
|
includeCommentTurns: true,
|
|
@@ -3397,7 +3863,7 @@ async function runFeedbackSync(args) {
|
|
|
3397
3863
|
|
|
3398
3864
|
let resolved;
|
|
3399
3865
|
try {
|
|
3400
|
-
resolved =
|
|
3866
|
+
resolved = resolvePortableWorkspace(snapshot.repo_names || []);
|
|
3401
3867
|
} catch (err) {
|
|
3402
3868
|
console.error(err?.message || err);
|
|
3403
3869
|
process.exit(1);
|
|
@@ -4037,30 +4503,41 @@ async function runQuery(args) {
|
|
|
4037
4503
|
try {
|
|
4038
4504
|
cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
4039
4505
|
} catch (err) {
|
|
4040
|
-
console.
|
|
4041
|
-
|
|
4042
|
-
cfg = null;
|
|
4506
|
+
console.error("Failed to fetch project config for --with-diff:", err?.message || err);
|
|
4507
|
+
process.exit(1);
|
|
4043
4508
|
}
|
|
4044
4509
|
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4510
|
+
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
4511
|
+
const repoBindings = Array.isArray(cfg.repo_bindings) ? cfg.repo_bindings : [];
|
|
4512
|
+
if (!repoBindings.length && !repoNames.length) {
|
|
4513
|
+
console.error("No project repositories are configured for --with-diff.");
|
|
4514
|
+
console.error("Ask the project owner or builder to configure the project repos in Myte first.");
|
|
4515
|
+
process.exit(1);
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
const diffResult = collectGitDiffWithDiagnostics({
|
|
4519
|
+
projectId: cfg.project_id,
|
|
4520
|
+
repoNames,
|
|
4521
|
+
repoBindings,
|
|
4522
|
+
maxChars: diffLimit,
|
|
4523
|
+
fetchRemote,
|
|
4524
|
+
});
|
|
4525
|
+
diffText = diffResult.text;
|
|
4526
|
+
diffDiagnostics = diffResult.diagnostics;
|
|
4527
|
+
if (diffDiagnostics?.errors?.length) {
|
|
4528
|
+
for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
|
|
4061
4529
|
}
|
|
4062
|
-
if (!
|
|
4063
|
-
console.error("
|
|
4530
|
+
if (!Array.isArray(diffDiagnostics?.found_repos) || !diffDiagnostics.found_repos.length) {
|
|
4531
|
+
console.error("No configured project repos were found locally for --with-diff.");
|
|
4532
|
+
const expectedBindings = Array.isArray(diffDiagnostics?.requested_repo_bindings)
|
|
4533
|
+
? diffDiagnostics.requested_repo_bindings
|
|
4534
|
+
.map((binding) => binding.client_repo_name || binding.canonical_repo_name || binding.role)
|
|
4535
|
+
.filter(Boolean)
|
|
4536
|
+
: [];
|
|
4537
|
+
const expected = expectedBindings.length ? expectedBindings : repoNames;
|
|
4538
|
+
console.error(`Expected repo mapping or folder match for: ${expected.join(", ")}`);
|
|
4539
|
+
console.error("Run `myte config --json` to inspect the configured repos and local resolution.");
|
|
4540
|
+
process.exit(1);
|
|
4064
4541
|
}
|
|
4065
4542
|
}
|
|
4066
4543
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mytegroupinc/myte-core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Myte CLI core implementation
|
|
3
|
+
"version": "0.0.20",
|
|
4
|
+
"description": "Myte CLI core implementation.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "cli.js",
|
|
7
7
|
"files": [
|