@mytegroupinc/myte-core 0.0.2 → 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 +449 -75
- package/package.json +9 -2
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",
|
|
@@ -211,13 +289,43 @@ function runGitDiff(repoPath, args, opts = {}) {
|
|
|
211
289
|
maxBuffer: 64 * 1024 * 1024,
|
|
212
290
|
...opts,
|
|
213
291
|
});
|
|
292
|
+
if (res.error && res.error.code === "ENOENT") {
|
|
293
|
+
const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
|
|
294
|
+
err.code = "ENOENT";
|
|
295
|
+
return { ok: false, status: null, stdout: "", stderr: "", error: err };
|
|
296
|
+
}
|
|
214
297
|
if (res.status !== 0 && res.status !== 1) {
|
|
215
298
|
const msg = res.stderr || res.error?.message || "unknown git error";
|
|
216
299
|
const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
|
|
217
300
|
err.code = res.status;
|
|
218
|
-
|
|
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 "";
|
|
219
328
|
}
|
|
220
|
-
return (res.stdout || "").trim();
|
|
221
329
|
}
|
|
222
330
|
|
|
223
331
|
function runGitOk(repoPath, args, opts = {}) {
|
|
@@ -225,7 +333,7 @@ function runGitOk(repoPath, args, opts = {}) {
|
|
|
225
333
|
cwd: repoPath,
|
|
226
334
|
encoding: "utf8",
|
|
227
335
|
stdio: ["ignore", "pipe", "pipe"],
|
|
228
|
-
maxBuffer:
|
|
336
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
229
337
|
...opts,
|
|
230
338
|
});
|
|
231
339
|
return res.status === 0;
|
|
@@ -239,33 +347,54 @@ function runGitTry(repoPath, args) {
|
|
|
239
347
|
}
|
|
240
348
|
}
|
|
241
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
|
+
|
|
363
|
+
function runGitSafeDiff(repoPath, args, prefix) {
|
|
364
|
+
const patchText = runGitTry(repoPath, args);
|
|
365
|
+
if (!patchText) {
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
return patchPaths(patchText, prefix);
|
|
369
|
+
}
|
|
370
|
+
|
|
242
371
|
function patchPaths(diff, prefix = "") {
|
|
243
372
|
if (!diff) return "";
|
|
244
373
|
return diff
|
|
245
374
|
.replace(
|
|
246
375
|
/^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
|
|
247
376
|
(_, aPre, a, bPre, b) =>
|
|
248
|
-
`diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix
|
|
249
|
-
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}`
|
|
250
379
|
}`
|
|
251
380
|
)
|
|
252
381
|
.replace(
|
|
253
382
|
/^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
254
|
-
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
383
|
+
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
255
384
|
)
|
|
256
385
|
.replace(
|
|
257
386
|
/^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
258
|
-
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
387
|
+
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
259
388
|
);
|
|
260
389
|
}
|
|
261
390
|
|
|
262
391
|
function filterDiff(diff) {
|
|
263
392
|
if (!diff) return "";
|
|
264
393
|
return diff
|
|
265
|
-
.split(
|
|
394
|
+
.split(/(?=^diff --git )/gm)
|
|
266
395
|
.filter((block) => {
|
|
267
396
|
if (!block.trim()) return false;
|
|
268
|
-
const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
397
|
+
const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
269
398
|
if (
|
|
270
399
|
m &&
|
|
271
400
|
(shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
|
|
@@ -274,7 +403,6 @@ function filterDiff(diff) {
|
|
|
274
403
|
}
|
|
275
404
|
return true;
|
|
276
405
|
})
|
|
277
|
-
.map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
|
|
278
406
|
.join("");
|
|
279
407
|
}
|
|
280
408
|
|
|
@@ -349,78 +477,202 @@ function resolveBaseRef(repoPath) {
|
|
|
349
477
|
return null;
|
|
350
478
|
}
|
|
351
479
|
|
|
352
|
-
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
|
+
|
|
353
503
|
try {
|
|
354
|
-
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 || [];
|
|
355
509
|
const repos = resolved.repos || [];
|
|
356
510
|
if (!repos.length) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
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
|
+
};
|
|
364
517
|
}
|
|
365
518
|
|
|
366
|
-
|
|
367
|
-
if (projectId)
|
|
368
|
-
|
|
369
|
-
|
|
519
|
+
const sections = [];
|
|
520
|
+
if (projectId) sections.push(`# Project: ${projectId}`);
|
|
521
|
+
sections.push(`# Mode: ${resolved.mode}`);
|
|
522
|
+
sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
|
|
370
523
|
if (resolved.missing && resolved.missing.length) {
|
|
371
|
-
|
|
524
|
+
sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
|
|
372
525
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for (const
|
|
376
|
-
|
|
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
|
+
}
|
|
377
558
|
|
|
378
559
|
const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
379
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
|
+
}
|
|
380
578
|
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
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
|
+
}
|
|
384
587
|
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
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
|
+
}
|
|
388
596
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
.
|
|
392
|
-
|
|
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
|
+
}
|
|
393
608
|
|
|
394
609
|
let untrackedDiffs = "";
|
|
395
610
|
for (const file of untracked) {
|
|
396
611
|
const absFile = path.join(abs, file);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (repoSummary.errors.length) {
|
|
630
|
+
repoSummary.status = "partial";
|
|
400
631
|
}
|
|
401
632
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (baseDiff)
|
|
405
|
-
if (staged)
|
|
406
|
-
if (unstaged)
|
|
407
|
-
if (untrackedDiffs)
|
|
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`;
|
|
408
639
|
|
|
409
|
-
if (!
|
|
410
|
-
|
|
640
|
+
if (!repoSummary.has_changes) {
|
|
641
|
+
section += "_No local changes - working tree clean_\n\n";
|
|
411
642
|
}
|
|
643
|
+
sections.push(section);
|
|
644
|
+
diagnostics.repo_summaries.push(repoSummary);
|
|
645
|
+
if (repoSummary.has_changes) diagnostics.collected_any = true;
|
|
412
646
|
}
|
|
413
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
|
+
}
|
|
414
653
|
if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
|
|
415
|
-
|
|
654
|
+
diagnostics.truncated = true;
|
|
655
|
+
output = `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
|
|
416
656
|
}
|
|
417
|
-
|
|
657
|
+
output = output.trim();
|
|
658
|
+
return {
|
|
659
|
+
text: output,
|
|
660
|
+
diagnostics,
|
|
661
|
+
};
|
|
418
662
|
} catch (err) {
|
|
419
|
-
|
|
420
|
-
|
|
663
|
+
diagnostics.errors.push(safeText(err?.message || err));
|
|
664
|
+
diagnostics.reason = "collection_error";
|
|
665
|
+
return {
|
|
666
|
+
text: "",
|
|
667
|
+
diagnostics,
|
|
668
|
+
};
|
|
421
669
|
}
|
|
422
670
|
}
|
|
423
671
|
|
|
672
|
+
function collectGitDiff(options = {}) {
|
|
673
|
+
return collectGitDiffWithDiagnostics(options).text;
|
|
674
|
+
}
|
|
675
|
+
|
|
424
676
|
async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
425
677
|
const fetchFn = await getFetch();
|
|
426
678
|
const url = `${apiBase}/project-assistant/config`;
|
|
@@ -443,9 +695,9 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
443
695
|
return body.data || {};
|
|
444
696
|
}
|
|
445
697
|
|
|
446
|
-
async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
|
|
698
|
+
async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
447
699
|
const fetchFn = await getFetch();
|
|
448
|
-
const url = `${apiBase}
|
|
700
|
+
const url = `${apiBase}${endpoint}`;
|
|
449
701
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
450
702
|
fetchFn,
|
|
451
703
|
url,
|
|
@@ -469,6 +721,111 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
|
|
|
469
721
|
return body.data || {};
|
|
470
722
|
}
|
|
471
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
|
+
|
|
472
829
|
async function runConfig(args) {
|
|
473
830
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
474
831
|
if (!key) {
|
|
@@ -548,31 +905,44 @@ async function runQuery(args) {
|
|
|
548
905
|
const apiBase = normalizeApiBase(baseRaw);
|
|
549
906
|
|
|
550
907
|
let diffText = "";
|
|
908
|
+
let diffDiagnostics = null;
|
|
551
909
|
if (includeDiff) {
|
|
552
|
-
let cfg;
|
|
910
|
+
let cfg = null;
|
|
553
911
|
try {
|
|
554
912
|
cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
|
|
555
913
|
} catch (err) {
|
|
556
|
-
console.
|
|
557
|
-
|
|
914
|
+
console.warn("Warning: project config unavailable for --with-diff. Continuing without diff context.");
|
|
915
|
+
console.warn(`Detail: ${err?.message || err}`);
|
|
916
|
+
cfg = null;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (cfg) {
|
|
920
|
+
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
921
|
+
const diffResult = collectGitDiffWithDiagnostics({
|
|
922
|
+
projectId: cfg.project_id,
|
|
923
|
+
repoNames,
|
|
924
|
+
maxChars: diffLimit,
|
|
925
|
+
fetchRemote,
|
|
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
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
diffText = "";
|
|
934
|
+
diffDiagnostics = null;
|
|
558
935
|
}
|
|
559
|
-
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
560
|
-
diffText = collectGitDiff({
|
|
561
|
-
projectId: cfg.project_id,
|
|
562
|
-
repoNames,
|
|
563
|
-
maxChars: diffLimit,
|
|
564
|
-
fetchRemote,
|
|
565
|
-
});
|
|
566
936
|
if (!diffText) {
|
|
567
|
-
console.error("
|
|
568
|
-
process.exit(1);
|
|
937
|
+
console.error("Warning: no diff context collected. Continuing without --with-diff context.");
|
|
569
938
|
}
|
|
570
939
|
}
|
|
571
940
|
|
|
572
941
|
const payload = {
|
|
573
942
|
query,
|
|
574
|
-
additional_context: buildContext(args, diffText),
|
|
943
|
+
additional_context: buildContext(args, diffText, diffDiagnostics),
|
|
575
944
|
};
|
|
945
|
+
if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
|
|
576
946
|
|
|
577
947
|
if (printContext) {
|
|
578
948
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -642,6 +1012,11 @@ async function main() {
|
|
|
642
1012
|
return;
|
|
643
1013
|
}
|
|
644
1014
|
|
|
1015
|
+
if (command === "create-prd" || command === "add-prd" || command === "prd") {
|
|
1016
|
+
await runCreatePrd(args);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
645
1020
|
// query/ask default
|
|
646
1021
|
await runQuery(args);
|
|
647
1022
|
}
|
|
@@ -650,4 +1025,3 @@ main().catch((err) => {
|
|
|
650
1025
|
console.error("Unexpected error:", err?.message || err);
|
|
651
1026
|
process.exit(1);
|
|
652
1027
|
});
|
|
653
|
-
|
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"
|
|
@@ -17,4 +25,3 @@
|
|
|
17
25
|
"node-fetch": "^3.3.2"
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
|
-
|