@mytegroupinc/myte-core 0.0.3 → 0.0.4
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 +23 -2
- package/cli.js +421 -66
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -3,8 +3,29 @@
|
|
|
3
3
|
Internal implementation package for the `myte` CLI.
|
|
4
4
|
|
|
5
5
|
Most users should install the unscoped wrapper instead:
|
|
6
|
-
- `npm
|
|
6
|
+
- `npm install myte` then `npx myte query "..." --with-diff`
|
|
7
|
+
- `npm install myte` then `npm exec myte -- query "..." --with-diff`
|
|
8
|
+
- `npm i -g myte` then `myte query "..." --with-diff`
|
|
7
9
|
- `npx myte@latest query "..." --with-diff`
|
|
10
|
+
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
|
|
11
|
+
- `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Requirements:
|
|
14
|
+
- Node `18+`
|
|
15
|
+
- macOS, Linux, or Windows
|
|
16
|
+
- `git` in `PATH` for `--with-diff`
|
|
17
|
+
- `MYTE_API_KEY=<project_api_key>` in env or `.env`
|
|
18
|
+
- repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
|
|
10
19
|
|
|
20
|
+
Notes:
|
|
21
|
+
- `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
|
|
22
|
+
- `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
|
|
23
|
+
- `create-prd` is a deterministic PRD upload path, not an LLM generation command.
|
|
24
|
+
- `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
|
|
25
|
+
- `--with-diff` includes per-repo diagnostics in `print-context` payload:
|
|
26
|
+
- missing repo directories
|
|
27
|
+
- per-repo errors (for example fetch or command failures)
|
|
28
|
+
- clean/no-change repo summaries
|
|
29
|
+
- `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
|
|
30
|
+
|
|
31
|
+
This package is published under the org scope for governance; the public `myte` wrapper delegates here.
|
package/cli.js
CHANGED
|
@@ -49,7 +49,7 @@ function loadEnv() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function splitCommand(argv) {
|
|
52
|
-
const known = new Set(["query", "ask", "chat", "config", "help", "--help", "-h"]);
|
|
52
|
+
const known = new Set(["query", "ask", "chat", "config", "create-prd", "add-prd", "prd", "help", "--help", "-h"]);
|
|
53
53
|
const first = argv[0];
|
|
54
54
|
if (first && known.has(first)) {
|
|
55
55
|
const cmd = first === "--help" || first === "-h" ? "help" : first;
|
|
@@ -62,8 +62,8 @@ function parseArgs(argv) {
|
|
|
62
62
|
try {
|
|
63
63
|
// eslint-disable-next-line global-require
|
|
64
64
|
return require("minimist")(argv, {
|
|
65
|
-
boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json"],
|
|
66
|
-
string: ["query", "q", "context", "ctx", "base-url", "timeout-ms", "diff-limit"],
|
|
65
|
+
boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin"],
|
|
66
|
+
string: ["query", "q", "context", "ctx", "base-url", "timeout-ms", "diff-limit", "title"],
|
|
67
67
|
alias: {
|
|
68
68
|
q: "query",
|
|
69
69
|
d: "with-diff",
|
|
@@ -117,6 +117,15 @@ function printHelp() {
|
|
|
117
117
|
" myte query \"<text>\" [--with-diff] [--context \"...\"]",
|
|
118
118
|
" myte config [--json]",
|
|
119
119
|
" myte chat",
|
|
120
|
+
" myte create-prd <file.md> [--json]",
|
|
121
|
+
" myte add-prd <file.md> [--json]",
|
|
122
|
+
" cat file.md | myte create-prd --stdin [--title \"...\"]",
|
|
123
|
+
"",
|
|
124
|
+
"Run forms:",
|
|
125
|
+
" npm install myte then npx myte query \"...\" --with-diff",
|
|
126
|
+
" npm install myte then npm exec myte -- query \"...\" --with-diff",
|
|
127
|
+
" npm i -g myte then myte query \"...\" --with-diff",
|
|
128
|
+
" npx myte@latest query \"What changed in logging?\" --with-diff",
|
|
120
129
|
"",
|
|
121
130
|
"Auth:",
|
|
122
131
|
" - Set MYTE_API_KEY in a workspace .env (or env var)",
|
|
@@ -126,17 +135,49 @@ function printHelp() {
|
|
|
126
135
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
127
136
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
128
137
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
138
|
+
" --stdin Read PRD content from stdin instead of a file path",
|
|
139
|
+
" --title <text> Required only when stdin/raw markdown has no H1 title",
|
|
129
140
|
" --print-context Print JSON payload and exit (no query call)",
|
|
130
141
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
131
142
|
"",
|
|
132
143
|
"Examples:",
|
|
133
144
|
" myte query \"What changed in logging?\" --with-diff",
|
|
134
|
-
" myte
|
|
145
|
+
" myte create-prd ./drafts/auth-prd.md",
|
|
146
|
+
" cat ./drafts/auth-prd.md | myte create-prd --stdin",
|
|
135
147
|
" myte config",
|
|
136
148
|
].join("\n");
|
|
137
149
|
console.log(text);
|
|
138
150
|
}
|
|
139
151
|
|
|
152
|
+
function extractMarkdownTitle(text) {
|
|
153
|
+
const raw = String(text || "");
|
|
154
|
+
const lines = raw.split(/\r?\n/);
|
|
155
|
+
for (const rawLine of lines) {
|
|
156
|
+
const line = rawLine.trim();
|
|
157
|
+
if (line.startsWith("#")) {
|
|
158
|
+
const title = line.replace(/^#+\s*/, "").trim();
|
|
159
|
+
if (title) return title;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isMyteKanbanTicket(text) {
|
|
166
|
+
return /^\s*```myte-kanban\s*\{[\s\S]*?\}\s*```\s*/.test(String(text || ""));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readStdinText() {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
let data = "";
|
|
172
|
+
process.stdin.setEncoding("utf8");
|
|
173
|
+
process.stdin.on("data", (chunk) => {
|
|
174
|
+
data += chunk;
|
|
175
|
+
});
|
|
176
|
+
process.stdin.on("end", () => resolve(data));
|
|
177
|
+
process.stdin.on("error", reject);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
140
181
|
async function getFetch() {
|
|
141
182
|
if (typeof fetch !== "undefined") return fetch;
|
|
142
183
|
const mod = await import("node-fetch");
|
|
@@ -170,10 +211,47 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
213
|
|
|
173
|
-
function
|
|
214
|
+
function summarizeDiffDiagnosticsForContext(diagnostics) {
|
|
215
|
+
if (!diagnostics) return null;
|
|
216
|
+
const repos = Array.isArray(diagnostics.repo_summaries)
|
|
217
|
+
? diagnostics.repo_summaries
|
|
218
|
+
: [];
|
|
219
|
+
return {
|
|
220
|
+
project_id: diagnostics.project_id || null,
|
|
221
|
+
mode: diagnostics.mode,
|
|
222
|
+
requested_repos: diagnostics.requested_repo_names || [],
|
|
223
|
+
found_repos: diagnostics.found_repos || [],
|
|
224
|
+
missing_repos: diagnostics.missing_repos || [],
|
|
225
|
+
collected_any: Boolean(diagnostics.collected_any),
|
|
226
|
+
truncation: diagnostics.truncated ? "truncated" : "full",
|
|
227
|
+
repos: repos.map((repo) => ({
|
|
228
|
+
name: repo.name,
|
|
229
|
+
status: repo.status || "ok",
|
|
230
|
+
head_branch: repo.head_branch || null,
|
|
231
|
+
base_ref: repo.base_ref || null,
|
|
232
|
+
has_changes: Boolean(repo.has_changes),
|
|
233
|
+
changed_blocks: repo.changed_blocks || {},
|
|
234
|
+
untracked_file_count: repo.untracked_file_count || 0,
|
|
235
|
+
error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
|
|
236
|
+
})),
|
|
237
|
+
warnings: diagnostics.warnings || [],
|
|
238
|
+
errors: diagnostics.errors || [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatDiffDiagnosticText(diagnostics) {
|
|
243
|
+
const summary = summarizeDiffDiagnosticsForContext(diagnostics);
|
|
244
|
+
if (!summary) return "";
|
|
245
|
+
return `Git diff diagnostics:\n${JSON.stringify(summary, null, 2)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildContext(args, diffText, diffDiagnostics) {
|
|
174
249
|
const ctx = args.context ?? args.ctx ?? args.c;
|
|
175
250
|
const extra = ctx === undefined ? [] : Array.isArray(ctx) ? ctx.map(String) : [String(ctx)];
|
|
176
251
|
if (diffText) extra.push(`Current git diff snapshot (project-configured):\n${diffText}`);
|
|
252
|
+
if (diffDiagnostics) {
|
|
253
|
+
extra.push(formatDiffDiagnosticText(diffDiagnostics));
|
|
254
|
+
}
|
|
177
255
|
return extra;
|
|
178
256
|
}
|
|
179
257
|
|
|
@@ -203,7 +281,7 @@ function hasGitDir(repoPath) {
|
|
|
203
281
|
return fs.existsSync(path.join(repoPath, ".git"));
|
|
204
282
|
}
|
|
205
283
|
|
|
206
|
-
function
|
|
284
|
+
function runGitRaw(repoPath, args, opts = {}) {
|
|
207
285
|
const res = spawnSync("git", args, {
|
|
208
286
|
cwd: repoPath,
|
|
209
287
|
encoding: "utf8",
|
|
@@ -214,15 +292,40 @@ function runGitDiff(repoPath, args, opts = {}) {
|
|
|
214
292
|
if (res.error && res.error.code === "ENOENT") {
|
|
215
293
|
const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
|
|
216
294
|
err.code = "ENOENT";
|
|
217
|
-
|
|
295
|
+
return { ok: false, status: null, stdout: "", stderr: "", error: err };
|
|
218
296
|
}
|
|
219
297
|
if (res.status !== 0 && res.status !== 1) {
|
|
220
298
|
const msg = res.stderr || res.error?.message || "unknown git error";
|
|
221
299
|
const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
|
|
222
300
|
err.code = res.status;
|
|
223
|
-
|
|
301
|
+
return { ok: false, status: res.status, stdout: "", stderr: msg, error: err };
|
|
302
|
+
}
|
|
303
|
+
return { ok: true, status: res.status, stdout: (res.stdout || "").trim(), stderr: res.stderr || "" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function runGitDiff(repoPath, args, opts = {}) {
|
|
307
|
+
const result = runGitRaw(repoPath, args, opts);
|
|
308
|
+
if (!result.ok) {
|
|
309
|
+
throw result.error;
|
|
310
|
+
}
|
|
311
|
+
return result.stdout;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runGitDiffResult(repoPath, args, opts = {}) {
|
|
315
|
+
return runGitRaw(repoPath, args, opts);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toPatchWithPrefix(diffText, prefix) {
|
|
319
|
+
if (!diffText) return "";
|
|
320
|
+
return patchPaths(diffText, prefix);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function safeText(value) {
|
|
324
|
+
try {
|
|
325
|
+
return String(value || "");
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return "";
|
|
224
328
|
}
|
|
225
|
-
return (res.stdout || "").trim();
|
|
226
329
|
}
|
|
227
330
|
|
|
228
331
|
function runGitOk(repoPath, args, opts = {}) {
|
|
@@ -230,7 +333,7 @@ function runGitOk(repoPath, args, opts = {}) {
|
|
|
230
333
|
cwd: repoPath,
|
|
231
334
|
encoding: "utf8",
|
|
232
335
|
stdio: ["ignore", "pipe", "pipe"],
|
|
233
|
-
maxBuffer:
|
|
336
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
234
337
|
...opts,
|
|
235
338
|
});
|
|
236
339
|
return res.status === 0;
|
|
@@ -244,6 +347,19 @@ function runGitTry(repoPath, args) {
|
|
|
244
347
|
}
|
|
245
348
|
}
|
|
246
349
|
|
|
350
|
+
function runGitTryWithResult(repoPath, args, opts = {}) {
|
|
351
|
+
const result = runGitDiffResult(repoPath, args, opts);
|
|
352
|
+
if (!result.ok) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
output: "",
|
|
356
|
+
error: safeText(result.error?.message),
|
|
357
|
+
status: result.status,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { ok: true, output: result.stdout, error: "", status: result.status };
|
|
361
|
+
}
|
|
362
|
+
|
|
247
363
|
function runGitSafeDiff(repoPath, args, prefix) {
|
|
248
364
|
const patchText = runGitTry(repoPath, args);
|
|
249
365
|
if (!patchText) {
|
|
@@ -258,27 +374,27 @@ function patchPaths(diff, prefix = "") {
|
|
|
258
374
|
.replace(
|
|
259
375
|
/^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
|
|
260
376
|
(_, aPre, a, bPre, b) =>
|
|
261
|
-
`diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix
|
|
262
|
-
bPre === "/dev/null" ? "/dev/null" : prefix
|
|
377
|
+
`diff --git ${aPre === "/dev/null" ? "/dev/null" : `${aPre}${prefix}${a}`} ${
|
|
378
|
+
bPre === "/dev/null" ? "/dev/null" : `${bPre}${prefix}${b}`
|
|
263
379
|
}`
|
|
264
380
|
)
|
|
265
381
|
.replace(
|
|
266
382
|
/^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
267
|
-
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
383
|
+
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
268
384
|
)
|
|
269
385
|
.replace(
|
|
270
386
|
/^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
271
|
-
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
387
|
+
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
272
388
|
);
|
|
273
389
|
}
|
|
274
390
|
|
|
275
391
|
function filterDiff(diff) {
|
|
276
392
|
if (!diff) return "";
|
|
277
393
|
return diff
|
|
278
|
-
.split(
|
|
394
|
+
.split(/(?=^diff --git )/gm)
|
|
279
395
|
.filter((block) => {
|
|
280
396
|
if (!block.trim()) return false;
|
|
281
|
-
const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
397
|
+
const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
282
398
|
if (
|
|
283
399
|
m &&
|
|
284
400
|
(shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
|
|
@@ -287,7 +403,6 @@ function filterDiff(diff) {
|
|
|
287
403
|
}
|
|
288
404
|
return true;
|
|
289
405
|
})
|
|
290
|
-
.map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
|
|
291
406
|
.join("");
|
|
292
407
|
}
|
|
293
408
|
|
|
@@ -362,80 +477,202 @@ function resolveBaseRef(repoPath) {
|
|
|
362
477
|
return null;
|
|
363
478
|
}
|
|
364
479
|
|
|
365
|
-
function
|
|
480
|
+
function collectGitDiffWithDiagnostics({
|
|
481
|
+
projectId,
|
|
482
|
+
repoNames,
|
|
483
|
+
maxChars,
|
|
484
|
+
fetchRemote = true,
|
|
485
|
+
} = {}) {
|
|
486
|
+
const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
487
|
+
const diagnostics = {
|
|
488
|
+
project_id: projectId || null,
|
|
489
|
+
requested_repo_names: configuredRepos,
|
|
490
|
+
fetch_remote: Boolean(fetchRemote),
|
|
491
|
+
mode: "none",
|
|
492
|
+
search_root: null,
|
|
493
|
+
found_repos: [],
|
|
494
|
+
missing_repos: [],
|
|
495
|
+
repo_summaries: [],
|
|
496
|
+
collected_any: false,
|
|
497
|
+
collected_text_length: 0,
|
|
498
|
+
truncated: false,
|
|
499
|
+
warnings: [],
|
|
500
|
+
errors: [],
|
|
501
|
+
};
|
|
502
|
+
|
|
366
503
|
try {
|
|
367
|
-
const resolved = resolveConfiguredRepos(
|
|
504
|
+
const resolved = resolveConfiguredRepos(configuredRepos);
|
|
505
|
+
diagnostics.mode = resolved.mode || "none";
|
|
506
|
+
diagnostics.search_root = resolved.root || null;
|
|
507
|
+
diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
|
|
508
|
+
diagnostics.missing_repos = resolved.missing || [];
|
|
368
509
|
const repos = resolved.repos || [];
|
|
369
510
|
if (!repos.length) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
throw new Error(msg);
|
|
511
|
+
diagnostics.errors.push("No configured repos found locally.");
|
|
512
|
+
diagnostics.reason = "no_repos_found";
|
|
513
|
+
return {
|
|
514
|
+
text: "",
|
|
515
|
+
diagnostics,
|
|
516
|
+
};
|
|
377
517
|
}
|
|
378
518
|
|
|
379
|
-
|
|
380
|
-
if (projectId)
|
|
381
|
-
|
|
382
|
-
|
|
519
|
+
const sections = [];
|
|
520
|
+
if (projectId) sections.push(`# Project: ${projectId}`);
|
|
521
|
+
sections.push(`# Mode: ${resolved.mode}`);
|
|
522
|
+
sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
|
|
383
523
|
if (resolved.missing && resolved.missing.length) {
|
|
384
|
-
|
|
524
|
+
sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
|
|
385
525
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
for (const
|
|
389
|
-
|
|
526
|
+
sections.push("");
|
|
527
|
+
|
|
528
|
+
for (const repo of repos) {
|
|
529
|
+
const repoSummary = {
|
|
530
|
+
name: repo.name,
|
|
531
|
+
dir: repo.dir || ".",
|
|
532
|
+
root: repo.abs,
|
|
533
|
+
status: "ok",
|
|
534
|
+
head_branch: null,
|
|
535
|
+
base_ref: null,
|
|
536
|
+
has_changes: false,
|
|
537
|
+
changed_blocks: {
|
|
538
|
+
base_vs_head: false,
|
|
539
|
+
staged: false,
|
|
540
|
+
unstaged: false,
|
|
541
|
+
untracked_files: false,
|
|
542
|
+
},
|
|
543
|
+
untracked_file_count: 0,
|
|
544
|
+
errors: [],
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const { name, dir, abs, prefix } = repo;
|
|
548
|
+
const fetchDiag = { attempted: false, ok: false, message: "" };
|
|
549
|
+
if (fetchRemote) {
|
|
550
|
+
fetchDiag.attempted = true;
|
|
551
|
+
fetchDiag.ok = fetchBaseBranches(abs);
|
|
552
|
+
if (!fetchDiag.ok) {
|
|
553
|
+
fetchDiag.message = "failed to refresh origin main/master";
|
|
554
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
555
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
390
558
|
|
|
391
559
|
const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
392
560
|
const baseRef = resolveBaseRef(abs);
|
|
561
|
+
repoSummary.head_branch = headBranch;
|
|
562
|
+
repoSummary.base_ref = baseRef || "base-unresolved";
|
|
563
|
+
repoSummary.fetch = fetchDiag;
|
|
564
|
+
|
|
565
|
+
let baseDiff = "";
|
|
566
|
+
if (baseRef) {
|
|
567
|
+
const baseDiffRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`], {
|
|
568
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
569
|
+
});
|
|
570
|
+
baseDiff = filterDiff(toPatchWithPrefix(baseDiffRaw.output || "", prefix));
|
|
571
|
+
if (baseDiff) {
|
|
572
|
+
repoSummary.changed_blocks.base_vs_head = true;
|
|
573
|
+
repoSummary.has_changes = true;
|
|
574
|
+
} else if (baseDiffRaw.ok === false) {
|
|
575
|
+
repoSummary.errors.push(`base-diff: ${baseDiffRaw.error}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
393
578
|
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
579
|
+
const stagedRaw = runGitTryWithResult(abs, ["diff", "--cached", "--no-color", "-U2"]);
|
|
580
|
+
const staged = filterDiff(toPatchWithPrefix(stagedRaw.output || "", prefix));
|
|
581
|
+
if (staged) {
|
|
582
|
+
repoSummary.changed_blocks.staged = true;
|
|
583
|
+
repoSummary.has_changes = true;
|
|
584
|
+
} else if (stagedRaw.ok === false) {
|
|
585
|
+
repoSummary.errors.push(`staged-diff: ${stagedRaw.error}`);
|
|
586
|
+
}
|
|
399
587
|
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
588
|
+
const unstagedRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2"]);
|
|
589
|
+
const unstaged = filterDiff(toPatchWithPrefix(unstagedRaw.output || "", prefix));
|
|
590
|
+
if (unstaged) {
|
|
591
|
+
repoSummary.changed_blocks.unstaged = true;
|
|
592
|
+
repoSummary.has_changes = true;
|
|
593
|
+
} else if (unstagedRaw.ok === false) {
|
|
594
|
+
repoSummary.errors.push(`unstaged-diff: ${unstagedRaw.error}`);
|
|
595
|
+
}
|
|
403
596
|
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
.
|
|
407
|
-
|
|
597
|
+
const untrackedListResult = runGitTryWithResult(abs, ["ls-files", "--others", "--exclude-standard"]);
|
|
598
|
+
const untracked = untrackedListResult.ok
|
|
599
|
+
? untrackedListResult.output
|
|
600
|
+
.split("\n")
|
|
601
|
+
.map((f) => f.trim())
|
|
602
|
+
.filter(Boolean)
|
|
603
|
+
.filter((f) => !shouldIgnore(f))
|
|
604
|
+
: [];
|
|
605
|
+
if (!untrackedListResult.ok) {
|
|
606
|
+
repoSummary.errors.push(`untracked-list: ${untrackedListResult.error}`);
|
|
607
|
+
}
|
|
408
608
|
|
|
409
609
|
let untrackedDiffs = "";
|
|
410
610
|
for (const file of untracked) {
|
|
411
611
|
const absFile = path.join(abs, file);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
612
|
+
try {
|
|
613
|
+
if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
|
|
614
|
+
} catch (err) {
|
|
615
|
+
repoSummary.errors.push(`untracked-stat(${file}): ${safeText(err?.message || err)}`);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const patchResult = runGitTryWithResult(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
|
|
619
|
+
if (patchResult.ok && patchResult.output) {
|
|
620
|
+
untrackedDiffs += toPatchWithPrefix(patchResult.output, prefix) + "\n";
|
|
621
|
+
repoSummary.untracked_file_count += 1;
|
|
622
|
+
repoSummary.changed_blocks.untracked_files = true;
|
|
623
|
+
repoSummary.has_changes = true;
|
|
624
|
+
} else if (!patchResult.ok) {
|
|
625
|
+
repoSummary.errors.push(`untracked-diff(${file}): ${patchResult.error}`);
|
|
626
|
+
}
|
|
415
627
|
}
|
|
416
628
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
629
|
+
if (repoSummary.errors.length) {
|
|
630
|
+
repoSummary.status = "partial";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let section = `### ${name} (${dir || "."})\n\n`;
|
|
634
|
+
section += `# ?? Base vs HEAD (${repoSummary.base_ref}...HEAD | head=${headBranch})\n`;
|
|
635
|
+
if (baseDiff) section += `${baseDiff}\n\n`;
|
|
636
|
+
if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
|
|
637
|
+
if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
|
|
638
|
+
if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
|
|
423
639
|
|
|
424
|
-
if (!
|
|
425
|
-
|
|
640
|
+
if (!repoSummary.has_changes) {
|
|
641
|
+
section += "_No local changes - working tree clean_\n\n";
|
|
426
642
|
}
|
|
643
|
+
sections.push(section);
|
|
644
|
+
diagnostics.repo_summaries.push(repoSummary);
|
|
645
|
+
if (repoSummary.has_changes) diagnostics.collected_any = true;
|
|
427
646
|
}
|
|
428
647
|
|
|
648
|
+
let output = sections.join("\n");
|
|
649
|
+
diagnostics.collected_text_length = output.length;
|
|
650
|
+
if (!diagnostics.collected_any && !diagnostics.reason) {
|
|
651
|
+
diagnostics.reason = "no_changes_detected";
|
|
652
|
+
}
|
|
429
653
|
if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
|
|
430
|
-
|
|
654
|
+
diagnostics.truncated = true;
|
|
655
|
+
output = `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
|
|
431
656
|
}
|
|
432
|
-
|
|
657
|
+
output = output.trim();
|
|
658
|
+
return {
|
|
659
|
+
text: output,
|
|
660
|
+
diagnostics,
|
|
661
|
+
};
|
|
433
662
|
} catch (err) {
|
|
434
|
-
|
|
435
|
-
|
|
663
|
+
diagnostics.errors.push(safeText(err?.message || err));
|
|
664
|
+
diagnostics.reason = "collection_error";
|
|
665
|
+
return {
|
|
666
|
+
text: "",
|
|
667
|
+
diagnostics,
|
|
668
|
+
};
|
|
436
669
|
}
|
|
437
670
|
}
|
|
438
671
|
|
|
672
|
+
function collectGitDiff(options = {}) {
|
|
673
|
+
return collectGitDiffWithDiagnostics(options).text;
|
|
674
|
+
}
|
|
675
|
+
|
|
439
676
|
async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
440
677
|
const fetchFn = await getFetch();
|
|
441
678
|
const url = `${apiBase}/project-assistant/config`;
|
|
@@ -458,9 +695,9 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
458
695
|
return body.data || {};
|
|
459
696
|
}
|
|
460
697
|
|
|
461
|
-
async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
|
|
698
|
+
async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
462
699
|
const fetchFn = await getFetch();
|
|
463
|
-
const url = `${apiBase}
|
|
700
|
+
const url = `${apiBase}${endpoint}`;
|
|
464
701
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
465
702
|
fetchFn,
|
|
466
703
|
url,
|
|
@@ -484,6 +721,111 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
|
|
|
484
721
|
return body.data || {};
|
|
485
722
|
}
|
|
486
723
|
|
|
724
|
+
async function runCreatePrd(args) {
|
|
725
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
726
|
+
if (!key) {
|
|
727
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
|
|
731
|
+
|
|
732
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
733
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
734
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
735
|
+
|
|
736
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
737
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
738
|
+
|
|
739
|
+
const providedPath = args.file || args.path || (Array.isArray(args._) ? args._[0] : "");
|
|
740
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !providedPath));
|
|
741
|
+
|
|
742
|
+
let sourceText = "";
|
|
743
|
+
let filePath = "";
|
|
744
|
+
if (useStdin) {
|
|
745
|
+
sourceText = String(await readStdinText() || "");
|
|
746
|
+
} else {
|
|
747
|
+
filePath = String(providedPath || "").trim();
|
|
748
|
+
if (!filePath) {
|
|
749
|
+
console.error("Missing PRD file path.");
|
|
750
|
+
printHelp();
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
const absPath = path.resolve(filePath);
|
|
754
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
755
|
+
console.error(`PRD file not found: ${absPath}`);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
sourceText = fs.readFileSync(absPath, "utf8");
|
|
759
|
+
filePath = absPath;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const trimmedSource = String(sourceText || "").trim();
|
|
763
|
+
if (!trimmedSource) {
|
|
764
|
+
console.error("PRD content is empty.");
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const inferredTitle = String(args.title || extractMarkdownTitle(trimmedSource) || (!useStdin && filePath ? path.parse(filePath).name : "")).trim();
|
|
769
|
+
const payload = isMyteKanbanTicket(trimmedSource)
|
|
770
|
+
? {
|
|
771
|
+
ticket_markdown: trimmedSource,
|
|
772
|
+
}
|
|
773
|
+
: {
|
|
774
|
+
prd_markdown: trimmedSource,
|
|
775
|
+
title: inferredTitle,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
if (!payload.ticket_markdown && !payload.title) {
|
|
779
|
+
console.error("A title is required when uploading raw markdown without a myte-kanban metadata block. Use --title or add a top-level # heading.");
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (printContext) {
|
|
784
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
785
|
+
process.exit(0);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let data;
|
|
789
|
+
try {
|
|
790
|
+
data = await callAssistantQuery({
|
|
791
|
+
apiBase,
|
|
792
|
+
key,
|
|
793
|
+
payload,
|
|
794
|
+
timeoutMs,
|
|
795
|
+
endpoint: "/project-assistant/create-prd",
|
|
796
|
+
});
|
|
797
|
+
} catch (err) {
|
|
798
|
+
if (err?.name === "AbortError") {
|
|
799
|
+
console.error(`Request timed out after ${timeoutMs}ms`);
|
|
800
|
+
} else {
|
|
801
|
+
console.error("PRD upload failed:", err?.message || err);
|
|
802
|
+
}
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (args.json) {
|
|
807
|
+
console.log(
|
|
808
|
+
JSON.stringify(
|
|
809
|
+
{
|
|
810
|
+
feedback_id: data.feedback_id || null,
|
|
811
|
+
project_id: data.project_id || null,
|
|
812
|
+
title: data.title || null,
|
|
813
|
+
status: data.status || null,
|
|
814
|
+
deterministic: data.deterministic === true,
|
|
815
|
+
},
|
|
816
|
+
null,
|
|
817
|
+
2
|
|
818
|
+
)
|
|
819
|
+
);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (data.feedback_id) console.log(`Feedback ID: ${data.feedback_id}`);
|
|
824
|
+
if (data.project_id) console.log(`Project ID: ${data.project_id}`);
|
|
825
|
+
if (data.title) console.log(`Title: ${data.title}`);
|
|
826
|
+
if (data.status) console.log(`Status: ${data.status}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
487
829
|
async function runConfig(args) {
|
|
488
830
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
489
831
|
if (!key) {
|
|
@@ -563,6 +905,7 @@ async function runQuery(args) {
|
|
|
563
905
|
const apiBase = normalizeApiBase(baseRaw);
|
|
564
906
|
|
|
565
907
|
let diffText = "";
|
|
908
|
+
let diffDiagnostics = null;
|
|
566
909
|
if (includeDiff) {
|
|
567
910
|
let cfg = null;
|
|
568
911
|
try {
|
|
@@ -575,14 +918,20 @@ async function runQuery(args) {
|
|
|
575
918
|
|
|
576
919
|
if (cfg) {
|
|
577
920
|
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
578
|
-
|
|
921
|
+
const diffResult = collectGitDiffWithDiagnostics({
|
|
579
922
|
projectId: cfg.project_id,
|
|
580
923
|
repoNames,
|
|
581
924
|
maxChars: diffLimit,
|
|
582
925
|
fetchRemote,
|
|
583
926
|
});
|
|
927
|
+
diffText = diffResult.text;
|
|
928
|
+
diffDiagnostics = diffResult.diagnostics;
|
|
929
|
+
if (diffDiagnostics?.errors?.length) {
|
|
930
|
+
for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
|
|
931
|
+
}
|
|
584
932
|
} else {
|
|
585
933
|
diffText = "";
|
|
934
|
+
diffDiagnostics = null;
|
|
586
935
|
}
|
|
587
936
|
if (!diffText) {
|
|
588
937
|
console.error("Warning: no diff context collected. Continuing without --with-diff context.");
|
|
@@ -591,8 +940,9 @@ async function runQuery(args) {
|
|
|
591
940
|
|
|
592
941
|
const payload = {
|
|
593
942
|
query,
|
|
594
|
-
additional_context: buildContext(args, diffText),
|
|
943
|
+
additional_context: buildContext(args, diffText, diffDiagnostics),
|
|
595
944
|
};
|
|
945
|
+
if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
|
|
596
946
|
|
|
597
947
|
if (printContext) {
|
|
598
948
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -662,6 +1012,11 @@ async function main() {
|
|
|
662
1012
|
return;
|
|
663
1013
|
}
|
|
664
1014
|
|
|
1015
|
+
if (command === "create-prd" || command === "add-prd" || command === "prd") {
|
|
1016
|
+
await runCreatePrd(args);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
665
1020
|
// query/ask default
|
|
666
1021
|
await runQuery(args);
|
|
667
1022
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mytegroupinc/myte-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"README.md",
|
|
9
|
+
"cli.js",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
7
15
|
"license": "MIT",
|
|
8
16
|
"engines": {
|
|
9
17
|
"node": ">=18"
|