@mytegroupinc/myte-core 0.0.3 → 0.0.5
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 +43 -2
- package/cli.js +678 -66
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -3,8 +3,49 @@
|
|
|
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 bootstrap`
|
|
7
|
+
- `npm install myte` then `npx myte query "..." --with-diff`
|
|
8
|
+
- `npm install myte` then `npm exec myte -- query "..." --with-diff`
|
|
9
|
+
- `npm i -g myte` then `myte bootstrap`
|
|
10
|
+
- `npm i -g myte` then `myte query "..." --with-diff`
|
|
11
|
+
- `npx myte@latest bootstrap`
|
|
7
12
|
- `npx myte@latest query "..." --with-diff`
|
|
13
|
+
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
|
|
14
|
+
- `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
Requirements:
|
|
17
|
+
- Node `18+`
|
|
18
|
+
- macOS, Linux, or Windows
|
|
19
|
+
- `git` in `PATH` for `--with-diff`
|
|
20
|
+
- `MYTE_API_KEY=<project_api_key>` in env or `.env`
|
|
21
|
+
- repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
|
|
10
22
|
|
|
23
|
+
Notes:
|
|
24
|
+
- `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
|
|
25
|
+
- `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
|
|
26
|
+
- `bootstrap` is a local file materialization path, not a hosted file download.
|
|
27
|
+
- `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
|
|
28
|
+
- `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
|
|
29
|
+
- `create-prd` is a deterministic PRD upload path, not an LLM generation command.
|
|
30
|
+
- `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
|
|
31
|
+
- `--with-diff` includes per-repo diagnostics in `print-context` payload:
|
|
32
|
+
- missing repo directories
|
|
33
|
+
- per-repo errors (for example fetch or command failures)
|
|
34
|
+
- clean/no-change repo summaries
|
|
35
|
+
- `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
|
|
36
|
+
|
|
37
|
+
Deterministic `create-prd` contract:
|
|
38
|
+
- Required: `MYTE_API_KEY`, a PRD markdown body, and a title.
|
|
39
|
+
- Title source: `myte-kanban.title`, the first markdown `# Heading`, or `--title`.
|
|
40
|
+
- Description source: `myte-kanban.description` or `--description`.
|
|
41
|
+
- The markdown body is stored verbatim as PRD content and is what the backend uses to build the PRD DOCX.
|
|
42
|
+
- Legacy `feedback_text` is still accepted for older payloads, but new callers should use `description`.
|
|
43
|
+
- Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
- `npx myte bootstrap`
|
|
47
|
+
- `npx myte bootstrap --dry-run --json`
|
|
48
|
+
- `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
|
|
49
|
+
- `npx myte create-prd ./drafts/auth-prd.md --print-context`
|
|
50
|
+
|
|
51
|
+
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", "bootstrap", "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", "description", "feedback-text", "output-dir"],
|
|
67
67
|
alias: {
|
|
68
68
|
q: "query",
|
|
69
69
|
d: "with-diff",
|
|
@@ -116,27 +116,84 @@ function printHelp() {
|
|
|
116
116
|
"Usage:",
|
|
117
117
|
" myte query \"<text>\" [--with-diff] [--context \"...\"]",
|
|
118
118
|
" myte config [--json]",
|
|
119
|
+
" myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
|
|
119
120
|
" myte chat",
|
|
121
|
+
" myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
|
|
122
|
+
" myte add-prd <file.md> [--json]",
|
|
123
|
+
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
124
|
+
"",
|
|
125
|
+
"Run forms:",
|
|
126
|
+
" npm install myte then npx myte query \"...\" --with-diff",
|
|
127
|
+
" npm install myte then npm exec myte -- query \"...\" --with-diff",
|
|
128
|
+
" npm i -g myte then myte query \"...\" --with-diff",
|
|
129
|
+
" npx myte@latest query \"What changed in logging?\" --with-diff",
|
|
120
130
|
"",
|
|
121
131
|
"Auth:",
|
|
122
132
|
" - Set MYTE_API_KEY in a workspace .env (or env var)",
|
|
123
133
|
"",
|
|
134
|
+
"bootstrap contract:",
|
|
135
|
+
" - Run from the wrapper root that contains the project's configured repo folders",
|
|
136
|
+
" - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
|
|
137
|
+
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
138
|
+
"",
|
|
139
|
+
"create-prd contract:",
|
|
140
|
+
" - Required: valid MYTE_API_KEY, PRD markdown body, title",
|
|
141
|
+
" - Title source: myte-kanban.title, first # heading, or --title",
|
|
142
|
+
" - Description source: myte-kanban.description or --description",
|
|
143
|
+
" - PRD DOCX content: the markdown body is stored verbatim",
|
|
144
|
+
"",
|
|
124
145
|
"Options:",
|
|
125
146
|
" --with-diff Include deterministic git diffs (project-scoped)",
|
|
126
147
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
127
148
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
128
149
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
150
|
+
" --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
|
|
151
|
+
" --stdin Read PRD content from stdin instead of a file path",
|
|
152
|
+
" --title <text> Override PRD title for raw markdown uploads",
|
|
153
|
+
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
129
154
|
" --print-context Print JSON payload and exit (no query call)",
|
|
130
155
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
131
156
|
"",
|
|
132
157
|
"Examples:",
|
|
133
158
|
" myte query \"What changed in logging?\" --with-diff",
|
|
134
|
-
" myte
|
|
159
|
+
" myte bootstrap",
|
|
160
|
+
" myte bootstrap --output-dir ./MyteCommandCenter",
|
|
161
|
+
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
|
|
162
|
+
" cat ./drafts/auth-prd.md | myte create-prd --stdin",
|
|
135
163
|
" myte config",
|
|
136
164
|
].join("\n");
|
|
137
165
|
console.log(text);
|
|
138
166
|
}
|
|
139
167
|
|
|
168
|
+
function extractMarkdownTitle(text) {
|
|
169
|
+
const raw = String(text || "");
|
|
170
|
+
const lines = raw.split(/\r?\n/);
|
|
171
|
+
for (const rawLine of lines) {
|
|
172
|
+
const line = rawLine.trim();
|
|
173
|
+
if (line.startsWith("#")) {
|
|
174
|
+
const title = line.replace(/^#+\s*/, "").trim();
|
|
175
|
+
if (title) return title;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isMyteKanbanTicket(text) {
|
|
182
|
+
return /^\s*```myte-kanban\s*\{[\s\S]*?\}\s*```\s*/.test(String(text || ""));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function readStdinText() {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
let data = "";
|
|
188
|
+
process.stdin.setEncoding("utf8");
|
|
189
|
+
process.stdin.on("data", (chunk) => {
|
|
190
|
+
data += chunk;
|
|
191
|
+
});
|
|
192
|
+
process.stdin.on("end", () => resolve(data));
|
|
193
|
+
process.stdin.on("error", reject);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
140
197
|
async function getFetch() {
|
|
141
198
|
if (typeof fetch !== "undefined") return fetch;
|
|
142
199
|
const mod = await import("node-fetch");
|
|
@@ -170,10 +227,47 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
|
|
|
170
227
|
}
|
|
171
228
|
}
|
|
172
229
|
|
|
173
|
-
function
|
|
230
|
+
function summarizeDiffDiagnosticsForContext(diagnostics) {
|
|
231
|
+
if (!diagnostics) return null;
|
|
232
|
+
const repos = Array.isArray(diagnostics.repo_summaries)
|
|
233
|
+
? diagnostics.repo_summaries
|
|
234
|
+
: [];
|
|
235
|
+
return {
|
|
236
|
+
project_id: diagnostics.project_id || null,
|
|
237
|
+
mode: diagnostics.mode,
|
|
238
|
+
requested_repos: diagnostics.requested_repo_names || [],
|
|
239
|
+
found_repos: diagnostics.found_repos || [],
|
|
240
|
+
missing_repos: diagnostics.missing_repos || [],
|
|
241
|
+
collected_any: Boolean(diagnostics.collected_any),
|
|
242
|
+
truncation: diagnostics.truncated ? "truncated" : "full",
|
|
243
|
+
repos: repos.map((repo) => ({
|
|
244
|
+
name: repo.name,
|
|
245
|
+
status: repo.status || "ok",
|
|
246
|
+
head_branch: repo.head_branch || null,
|
|
247
|
+
base_ref: repo.base_ref || null,
|
|
248
|
+
has_changes: Boolean(repo.has_changes),
|
|
249
|
+
changed_blocks: repo.changed_blocks || {},
|
|
250
|
+
untracked_file_count: repo.untracked_file_count || 0,
|
|
251
|
+
error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
|
|
252
|
+
})),
|
|
253
|
+
warnings: diagnostics.warnings || [],
|
|
254
|
+
errors: diagnostics.errors || [],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatDiffDiagnosticText(diagnostics) {
|
|
259
|
+
const summary = summarizeDiffDiagnosticsForContext(diagnostics);
|
|
260
|
+
if (!summary) return "";
|
|
261
|
+
return `Git diff diagnostics:\n${JSON.stringify(summary, null, 2)}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildContext(args, diffText, diffDiagnostics) {
|
|
174
265
|
const ctx = args.context ?? args.ctx ?? args.c;
|
|
175
266
|
const extra = ctx === undefined ? [] : Array.isArray(ctx) ? ctx.map(String) : [String(ctx)];
|
|
176
267
|
if (diffText) extra.push(`Current git diff snapshot (project-configured):\n${diffText}`);
|
|
268
|
+
if (diffDiagnostics) {
|
|
269
|
+
extra.push(formatDiffDiagnosticText(diffDiagnostics));
|
|
270
|
+
}
|
|
177
271
|
return extra;
|
|
178
272
|
}
|
|
179
273
|
|
|
@@ -203,7 +297,7 @@ function hasGitDir(repoPath) {
|
|
|
203
297
|
return fs.existsSync(path.join(repoPath, ".git"));
|
|
204
298
|
}
|
|
205
299
|
|
|
206
|
-
function
|
|
300
|
+
function runGitRaw(repoPath, args, opts = {}) {
|
|
207
301
|
const res = spawnSync("git", args, {
|
|
208
302
|
cwd: repoPath,
|
|
209
303
|
encoding: "utf8",
|
|
@@ -214,15 +308,40 @@ function runGitDiff(repoPath, args, opts = {}) {
|
|
|
214
308
|
if (res.error && res.error.code === "ENOENT") {
|
|
215
309
|
const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
|
|
216
310
|
err.code = "ENOENT";
|
|
217
|
-
|
|
311
|
+
return { ok: false, status: null, stdout: "", stderr: "", error: err };
|
|
218
312
|
}
|
|
219
313
|
if (res.status !== 0 && res.status !== 1) {
|
|
220
314
|
const msg = res.stderr || res.error?.message || "unknown git error";
|
|
221
315
|
const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
|
|
222
316
|
err.code = res.status;
|
|
223
|
-
|
|
317
|
+
return { ok: false, status: res.status, stdout: "", stderr: msg, error: err };
|
|
318
|
+
}
|
|
319
|
+
return { ok: true, status: res.status, stdout: (res.stdout || "").trim(), stderr: res.stderr || "" };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function runGitDiff(repoPath, args, opts = {}) {
|
|
323
|
+
const result = runGitRaw(repoPath, args, opts);
|
|
324
|
+
if (!result.ok) {
|
|
325
|
+
throw result.error;
|
|
326
|
+
}
|
|
327
|
+
return result.stdout;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function runGitDiffResult(repoPath, args, opts = {}) {
|
|
331
|
+
return runGitRaw(repoPath, args, opts);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function toPatchWithPrefix(diffText, prefix) {
|
|
335
|
+
if (!diffText) return "";
|
|
336
|
+
return patchPaths(diffText, prefix);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function safeText(value) {
|
|
340
|
+
try {
|
|
341
|
+
return String(value || "");
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return "";
|
|
224
344
|
}
|
|
225
|
-
return (res.stdout || "").trim();
|
|
226
345
|
}
|
|
227
346
|
|
|
228
347
|
function runGitOk(repoPath, args, opts = {}) {
|
|
@@ -230,7 +349,7 @@ function runGitOk(repoPath, args, opts = {}) {
|
|
|
230
349
|
cwd: repoPath,
|
|
231
350
|
encoding: "utf8",
|
|
232
351
|
stdio: ["ignore", "pipe", "pipe"],
|
|
233
|
-
maxBuffer:
|
|
352
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
234
353
|
...opts,
|
|
235
354
|
});
|
|
236
355
|
return res.status === 0;
|
|
@@ -244,6 +363,19 @@ function runGitTry(repoPath, args) {
|
|
|
244
363
|
}
|
|
245
364
|
}
|
|
246
365
|
|
|
366
|
+
function runGitTryWithResult(repoPath, args, opts = {}) {
|
|
367
|
+
const result = runGitDiffResult(repoPath, args, opts);
|
|
368
|
+
if (!result.ok) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
output: "",
|
|
372
|
+
error: safeText(result.error?.message),
|
|
373
|
+
status: result.status,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return { ok: true, output: result.stdout, error: "", status: result.status };
|
|
377
|
+
}
|
|
378
|
+
|
|
247
379
|
function runGitSafeDiff(repoPath, args, prefix) {
|
|
248
380
|
const patchText = runGitTry(repoPath, args);
|
|
249
381
|
if (!patchText) {
|
|
@@ -258,27 +390,27 @@ function patchPaths(diff, prefix = "") {
|
|
|
258
390
|
.replace(
|
|
259
391
|
/^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
|
|
260
392
|
(_, aPre, a, bPre, b) =>
|
|
261
|
-
`diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix
|
|
262
|
-
bPre === "/dev/null" ? "/dev/null" : prefix
|
|
393
|
+
`diff --git ${aPre === "/dev/null" ? "/dev/null" : `${aPre}${prefix}${a}`} ${
|
|
394
|
+
bPre === "/dev/null" ? "/dev/null" : `${bPre}${prefix}${b}`
|
|
263
395
|
}`
|
|
264
396
|
)
|
|
265
397
|
.replace(
|
|
266
398
|
/^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
267
|
-
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
399
|
+
(_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
268
400
|
)
|
|
269
401
|
.replace(
|
|
270
402
|
/^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
|
|
271
|
-
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix
|
|
403
|
+
(_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
|
|
272
404
|
);
|
|
273
405
|
}
|
|
274
406
|
|
|
275
407
|
function filterDiff(diff) {
|
|
276
408
|
if (!diff) return "";
|
|
277
409
|
return diff
|
|
278
|
-
.split(
|
|
410
|
+
.split(/(?=^diff --git )/gm)
|
|
279
411
|
.filter((block) => {
|
|
280
412
|
if (!block.trim()) return false;
|
|
281
|
-
const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
413
|
+
const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
|
|
282
414
|
if (
|
|
283
415
|
m &&
|
|
284
416
|
(shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
|
|
@@ -287,7 +419,6 @@ function filterDiff(diff) {
|
|
|
287
419
|
}
|
|
288
420
|
return true;
|
|
289
421
|
})
|
|
290
|
-
.map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
|
|
291
422
|
.join("");
|
|
292
423
|
}
|
|
293
424
|
|
|
@@ -362,80 +493,202 @@ function resolveBaseRef(repoPath) {
|
|
|
362
493
|
return null;
|
|
363
494
|
}
|
|
364
495
|
|
|
365
|
-
function
|
|
496
|
+
function collectGitDiffWithDiagnostics({
|
|
497
|
+
projectId,
|
|
498
|
+
repoNames,
|
|
499
|
+
maxChars,
|
|
500
|
+
fetchRemote = true,
|
|
501
|
+
} = {}) {
|
|
502
|
+
const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
|
|
503
|
+
const diagnostics = {
|
|
504
|
+
project_id: projectId || null,
|
|
505
|
+
requested_repo_names: configuredRepos,
|
|
506
|
+
fetch_remote: Boolean(fetchRemote),
|
|
507
|
+
mode: "none",
|
|
508
|
+
search_root: null,
|
|
509
|
+
found_repos: [],
|
|
510
|
+
missing_repos: [],
|
|
511
|
+
repo_summaries: [],
|
|
512
|
+
collected_any: false,
|
|
513
|
+
collected_text_length: 0,
|
|
514
|
+
truncated: false,
|
|
515
|
+
warnings: [],
|
|
516
|
+
errors: [],
|
|
517
|
+
};
|
|
518
|
+
|
|
366
519
|
try {
|
|
367
|
-
const resolved = resolveConfiguredRepos(
|
|
520
|
+
const resolved = resolveConfiguredRepos(configuredRepos);
|
|
521
|
+
diagnostics.mode = resolved.mode || "none";
|
|
522
|
+
diagnostics.search_root = resolved.root || null;
|
|
523
|
+
diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
|
|
524
|
+
diagnostics.missing_repos = resolved.missing || [];
|
|
368
525
|
const repos = resolved.repos || [];
|
|
369
526
|
if (!repos.length) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
throw new Error(msg);
|
|
527
|
+
diagnostics.errors.push("No configured repos found locally.");
|
|
528
|
+
diagnostics.reason = "no_repos_found";
|
|
529
|
+
return {
|
|
530
|
+
text: "",
|
|
531
|
+
diagnostics,
|
|
532
|
+
};
|
|
377
533
|
}
|
|
378
534
|
|
|
379
|
-
|
|
380
|
-
if (projectId)
|
|
381
|
-
|
|
382
|
-
|
|
535
|
+
const sections = [];
|
|
536
|
+
if (projectId) sections.push(`# Project: ${projectId}`);
|
|
537
|
+
sections.push(`# Mode: ${resolved.mode}`);
|
|
538
|
+
sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
|
|
383
539
|
if (resolved.missing && resolved.missing.length) {
|
|
384
|
-
|
|
540
|
+
sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
|
|
385
541
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
for (const
|
|
389
|
-
|
|
542
|
+
sections.push("");
|
|
543
|
+
|
|
544
|
+
for (const repo of repos) {
|
|
545
|
+
const repoSummary = {
|
|
546
|
+
name: repo.name,
|
|
547
|
+
dir: repo.dir || ".",
|
|
548
|
+
root: repo.abs,
|
|
549
|
+
status: "ok",
|
|
550
|
+
head_branch: null,
|
|
551
|
+
base_ref: null,
|
|
552
|
+
has_changes: false,
|
|
553
|
+
changed_blocks: {
|
|
554
|
+
base_vs_head: false,
|
|
555
|
+
staged: false,
|
|
556
|
+
unstaged: false,
|
|
557
|
+
untracked_files: false,
|
|
558
|
+
},
|
|
559
|
+
untracked_file_count: 0,
|
|
560
|
+
errors: [],
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const { name, dir, abs, prefix } = repo;
|
|
564
|
+
const fetchDiag = { attempted: false, ok: false, message: "" };
|
|
565
|
+
if (fetchRemote) {
|
|
566
|
+
fetchDiag.attempted = true;
|
|
567
|
+
fetchDiag.ok = fetchBaseBranches(abs);
|
|
568
|
+
if (!fetchDiag.ok) {
|
|
569
|
+
fetchDiag.message = "failed to refresh origin main/master";
|
|
570
|
+
repoSummary.errors.push(fetchDiag.message);
|
|
571
|
+
diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
390
574
|
|
|
391
575
|
const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
392
576
|
const baseRef = resolveBaseRef(abs);
|
|
577
|
+
repoSummary.head_branch = headBranch;
|
|
578
|
+
repoSummary.base_ref = baseRef || "base-unresolved";
|
|
579
|
+
repoSummary.fetch = fetchDiag;
|
|
580
|
+
|
|
581
|
+
let baseDiff = "";
|
|
582
|
+
if (baseRef) {
|
|
583
|
+
const baseDiffRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`], {
|
|
584
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
585
|
+
});
|
|
586
|
+
baseDiff = filterDiff(toPatchWithPrefix(baseDiffRaw.output || "", prefix));
|
|
587
|
+
if (baseDiff) {
|
|
588
|
+
repoSummary.changed_blocks.base_vs_head = true;
|
|
589
|
+
repoSummary.has_changes = true;
|
|
590
|
+
} else if (baseDiffRaw.ok === false) {
|
|
591
|
+
repoSummary.errors.push(`base-diff: ${baseDiffRaw.error}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
393
594
|
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
595
|
+
const stagedRaw = runGitTryWithResult(abs, ["diff", "--cached", "--no-color", "-U2"]);
|
|
596
|
+
const staged = filterDiff(toPatchWithPrefix(stagedRaw.output || "", prefix));
|
|
597
|
+
if (staged) {
|
|
598
|
+
repoSummary.changed_blocks.staged = true;
|
|
599
|
+
repoSummary.has_changes = true;
|
|
600
|
+
} else if (stagedRaw.ok === false) {
|
|
601
|
+
repoSummary.errors.push(`staged-diff: ${stagedRaw.error}`);
|
|
602
|
+
}
|
|
399
603
|
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
604
|
+
const unstagedRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2"]);
|
|
605
|
+
const unstaged = filterDiff(toPatchWithPrefix(unstagedRaw.output || "", prefix));
|
|
606
|
+
if (unstaged) {
|
|
607
|
+
repoSummary.changed_blocks.unstaged = true;
|
|
608
|
+
repoSummary.has_changes = true;
|
|
609
|
+
} else if (unstagedRaw.ok === false) {
|
|
610
|
+
repoSummary.errors.push(`unstaged-diff: ${unstagedRaw.error}`);
|
|
611
|
+
}
|
|
403
612
|
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
.
|
|
407
|
-
|
|
613
|
+
const untrackedListResult = runGitTryWithResult(abs, ["ls-files", "--others", "--exclude-standard"]);
|
|
614
|
+
const untracked = untrackedListResult.ok
|
|
615
|
+
? untrackedListResult.output
|
|
616
|
+
.split("\n")
|
|
617
|
+
.map((f) => f.trim())
|
|
618
|
+
.filter(Boolean)
|
|
619
|
+
.filter((f) => !shouldIgnore(f))
|
|
620
|
+
: [];
|
|
621
|
+
if (!untrackedListResult.ok) {
|
|
622
|
+
repoSummary.errors.push(`untracked-list: ${untrackedListResult.error}`);
|
|
623
|
+
}
|
|
408
624
|
|
|
409
625
|
let untrackedDiffs = "";
|
|
410
626
|
for (const file of untracked) {
|
|
411
627
|
const absFile = path.join(abs, file);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
628
|
+
try {
|
|
629
|
+
if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
|
|
630
|
+
} catch (err) {
|
|
631
|
+
repoSummary.errors.push(`untracked-stat(${file}): ${safeText(err?.message || err)}`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const patchResult = runGitTryWithResult(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
|
|
635
|
+
if (patchResult.ok && patchResult.output) {
|
|
636
|
+
untrackedDiffs += toPatchWithPrefix(patchResult.output, prefix) + "\n";
|
|
637
|
+
repoSummary.untracked_file_count += 1;
|
|
638
|
+
repoSummary.changed_blocks.untracked_files = true;
|
|
639
|
+
repoSummary.has_changes = true;
|
|
640
|
+
} else if (!patchResult.ok) {
|
|
641
|
+
repoSummary.errors.push(`untracked-diff(${file}): ${patchResult.error}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (repoSummary.errors.length) {
|
|
646
|
+
repoSummary.status = "partial";
|
|
415
647
|
}
|
|
416
648
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (baseDiff)
|
|
420
|
-
if (staged)
|
|
421
|
-
if (unstaged)
|
|
422
|
-
if (untrackedDiffs)
|
|
649
|
+
let section = `### ${name} (${dir || "."})\n\n`;
|
|
650
|
+
section += `# ?? Base vs HEAD (${repoSummary.base_ref}...HEAD | head=${headBranch})\n`;
|
|
651
|
+
if (baseDiff) section += `${baseDiff}\n\n`;
|
|
652
|
+
if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
|
|
653
|
+
if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
|
|
654
|
+
if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
|
|
423
655
|
|
|
424
|
-
if (!
|
|
425
|
-
|
|
656
|
+
if (!repoSummary.has_changes) {
|
|
657
|
+
section += "_No local changes - working tree clean_\n\n";
|
|
426
658
|
}
|
|
659
|
+
sections.push(section);
|
|
660
|
+
diagnostics.repo_summaries.push(repoSummary);
|
|
661
|
+
if (repoSummary.has_changes) diagnostics.collected_any = true;
|
|
427
662
|
}
|
|
428
663
|
|
|
664
|
+
let output = sections.join("\n");
|
|
665
|
+
diagnostics.collected_text_length = output.length;
|
|
666
|
+
if (!diagnostics.collected_any && !diagnostics.reason) {
|
|
667
|
+
diagnostics.reason = "no_changes_detected";
|
|
668
|
+
}
|
|
429
669
|
if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
|
|
430
|
-
|
|
670
|
+
diagnostics.truncated = true;
|
|
671
|
+
output = `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
|
|
431
672
|
}
|
|
432
|
-
|
|
673
|
+
output = output.trim();
|
|
674
|
+
return {
|
|
675
|
+
text: output,
|
|
676
|
+
diagnostics,
|
|
677
|
+
};
|
|
433
678
|
} catch (err) {
|
|
434
|
-
|
|
435
|
-
|
|
679
|
+
diagnostics.errors.push(safeText(err?.message || err));
|
|
680
|
+
diagnostics.reason = "collection_error";
|
|
681
|
+
return {
|
|
682
|
+
text: "",
|
|
683
|
+
diagnostics,
|
|
684
|
+
};
|
|
436
685
|
}
|
|
437
686
|
}
|
|
438
687
|
|
|
688
|
+
function collectGitDiff(options = {}) {
|
|
689
|
+
return collectGitDiffWithDiagnostics(options).text;
|
|
690
|
+
}
|
|
691
|
+
|
|
439
692
|
async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
440
693
|
const fetchFn = await getFetch();
|
|
441
694
|
const url = `${apiBase}/project-assistant/config`;
|
|
@@ -458,9 +711,31 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
458
711
|
return body.data || {};
|
|
459
712
|
}
|
|
460
713
|
|
|
461
|
-
async function
|
|
714
|
+
async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
|
|
462
715
|
const fetchFn = await getFetch();
|
|
463
|
-
const url = `${apiBase}/project-assistant/
|
|
716
|
+
const url = `${apiBase}/project-assistant/bootstrap`;
|
|
717
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
718
|
+
fetchFn,
|
|
719
|
+
url,
|
|
720
|
+
{
|
|
721
|
+
method: "GET",
|
|
722
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
723
|
+
},
|
|
724
|
+
timeoutMs
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
if (!resp.ok || body.status !== "success") {
|
|
728
|
+
const msg = body?.message || `Bootstrap request failed (${resp.status})`;
|
|
729
|
+
const err = new Error(msg);
|
|
730
|
+
err.status = resp.status;
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
733
|
+
return body.data || {};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
737
|
+
const fetchFn = await getFetch();
|
|
738
|
+
const url = `${apiBase}${endpoint}`;
|
|
464
739
|
const { resp, body } = await fetchJsonWithTimeout(
|
|
465
740
|
fetchFn,
|
|
466
741
|
url,
|
|
@@ -484,6 +759,231 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
|
|
|
484
759
|
return body.data || {};
|
|
485
760
|
}
|
|
486
761
|
|
|
762
|
+
function ensureDir(dirPath) {
|
|
763
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function clearYamlDirectory(dirPath) {
|
|
767
|
+
if (!fs.existsSync(dirPath)) {
|
|
768
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
772
|
+
if (!entry.isFile()) continue;
|
|
773
|
+
if (!entry.name.toLowerCase().endsWith(".yml")) continue;
|
|
774
|
+
fs.rmSync(path.join(dirPath, entry.name), { force: true });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function stableItemId(item, keys, fallback) {
|
|
779
|
+
for (const key of keys) {
|
|
780
|
+
const value = String(item?.[key] || "").trim();
|
|
781
|
+
if (value) return value;
|
|
782
|
+
}
|
|
783
|
+
return fallback;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function stringifyYaml(value) {
|
|
787
|
+
// eslint-disable-next-line global-require
|
|
788
|
+
const YAML = require("yaml");
|
|
789
|
+
return YAML.stringify(value, { lineWidth: 0 });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function writeYamlFile(filePath, value) {
|
|
793
|
+
ensureDir(path.dirname(filePath));
|
|
794
|
+
fs.writeFileSync(filePath, stringifyYaml(value), "utf8");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function writeJsonFile(filePath, value) {
|
|
798
|
+
ensureDir(path.dirname(filePath));
|
|
799
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function resolveBootstrapWorkspace(repoNames) {
|
|
803
|
+
const resolved = resolveConfiguredRepos(repoNames);
|
|
804
|
+
if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
|
|
805
|
+
const names = Array.isArray(repoNames) ? repoNames.join(", ") : "";
|
|
806
|
+
throw new Error(
|
|
807
|
+
`No configured project repos were found from the current workspace. Expected child folders matching: ${names || "(none)"}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return resolved;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
814
|
+
const targetRoot = outputDir
|
|
815
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
816
|
+
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
817
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
818
|
+
const phasesDir = path.join(dataRoot, "phases");
|
|
819
|
+
const epicsDir = path.join(dataRoot, "epics");
|
|
820
|
+
const storiesDir = path.join(dataRoot, "stories");
|
|
821
|
+
const missionsDir = path.join(dataRoot, "missions");
|
|
822
|
+
|
|
823
|
+
ensureDir(dataRoot);
|
|
824
|
+
clearYamlDirectory(phasesDir);
|
|
825
|
+
clearYamlDirectory(epicsDir);
|
|
826
|
+
clearYamlDirectory(storiesDir);
|
|
827
|
+
clearYamlDirectory(missionsDir);
|
|
828
|
+
|
|
829
|
+
const phases = Array.isArray(snapshot.phases) ? snapshot.phases : [];
|
|
830
|
+
const epics = Array.isArray(snapshot.epics) ? snapshot.epics : [];
|
|
831
|
+
const stories = Array.isArray(snapshot.stories) ? snapshot.stories : [];
|
|
832
|
+
const missions = Array.isArray(snapshot.missions) ? snapshot.missions : [];
|
|
833
|
+
|
|
834
|
+
phases.forEach((phase, index) => {
|
|
835
|
+
const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
|
|
836
|
+
writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
|
|
837
|
+
});
|
|
838
|
+
epics.forEach((epic, index) => {
|
|
839
|
+
const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
|
|
840
|
+
writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
|
|
841
|
+
});
|
|
842
|
+
stories.forEach((story, index) => {
|
|
843
|
+
const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
|
|
844
|
+
writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
|
|
845
|
+
});
|
|
846
|
+
missions.forEach((mission, index) => {
|
|
847
|
+
const missionId = stableItemId(mission, ["mission_id", "id", "_id"], `M${String(index + 1).padStart(3, "0")}`);
|
|
848
|
+
writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
852
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), snapshot.project);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const manifest = {
|
|
856
|
+
schema_version: snapshot.schema_version || 1,
|
|
857
|
+
generated_at: snapshot.generated_at || null,
|
|
858
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
859
|
+
project: snapshot.project || null,
|
|
860
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
861
|
+
counts: {
|
|
862
|
+
phases: phases.length,
|
|
863
|
+
epics: epics.length,
|
|
864
|
+
stories: stories.length,
|
|
865
|
+
missions: missions.length,
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
writeJsonFile(path.join(dataRoot, "bootstrap-manifest.json"), manifest);
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
targetRoot,
|
|
872
|
+
dataRoot,
|
|
873
|
+
manifest,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function runCreatePrd(args) {
|
|
878
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
879
|
+
if (!key) {
|
|
880
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
|
|
884
|
+
|
|
885
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
886
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
887
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
888
|
+
|
|
889
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
890
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
891
|
+
|
|
892
|
+
const providedPath = args.file || args.path || (Array.isArray(args._) ? args._[0] : "");
|
|
893
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !providedPath));
|
|
894
|
+
|
|
895
|
+
let sourceText = "";
|
|
896
|
+
let filePath = "";
|
|
897
|
+
if (useStdin) {
|
|
898
|
+
sourceText = String(await readStdinText() || "");
|
|
899
|
+
} else {
|
|
900
|
+
filePath = String(providedPath || "").trim();
|
|
901
|
+
if (!filePath) {
|
|
902
|
+
console.error("Missing PRD file path.");
|
|
903
|
+
printHelp();
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const absPath = path.resolve(filePath);
|
|
907
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
908
|
+
console.error(`PRD file not found: ${absPath}`);
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
sourceText = fs.readFileSync(absPath, "utf8");
|
|
912
|
+
filePath = absPath;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const trimmedSource = String(sourceText || "").trim();
|
|
916
|
+
if (!trimmedSource) {
|
|
917
|
+
console.error("PRD content is empty.");
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const inferredTitle = String(args.title || extractMarkdownTitle(trimmedSource) || (!useStdin && filePath ? path.parse(filePath).name : "")).trim();
|
|
922
|
+
const description = String(args.description || args["feedback-text"] || args.feedbackText || "").trim();
|
|
923
|
+
const payload = isMyteKanbanTicket(trimmedSource)
|
|
924
|
+
? {
|
|
925
|
+
ticket_markdown: trimmedSource,
|
|
926
|
+
}
|
|
927
|
+
: {
|
|
928
|
+
prd_markdown: trimmedSource,
|
|
929
|
+
title: inferredTitle,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
if (!payload.ticket_markdown && description) {
|
|
933
|
+
payload.description = description;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!payload.ticket_markdown && !payload.title) {
|
|
937
|
+
console.error("A title is required when uploading raw markdown without a myte-kanban metadata block. Use --title or add a top-level # heading.");
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (printContext) {
|
|
942
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
943
|
+
process.exit(0);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let data;
|
|
947
|
+
try {
|
|
948
|
+
data = await callAssistantQuery({
|
|
949
|
+
apiBase,
|
|
950
|
+
key,
|
|
951
|
+
payload,
|
|
952
|
+
timeoutMs,
|
|
953
|
+
endpoint: "/project-assistant/create-prd",
|
|
954
|
+
});
|
|
955
|
+
} catch (err) {
|
|
956
|
+
if (err?.name === "AbortError") {
|
|
957
|
+
console.error(`Request timed out after ${timeoutMs}ms`);
|
|
958
|
+
} else {
|
|
959
|
+
console.error("PRD upload failed:", err?.message || err);
|
|
960
|
+
}
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (args.json) {
|
|
965
|
+
console.log(
|
|
966
|
+
JSON.stringify(
|
|
967
|
+
{
|
|
968
|
+
feedback_id: data.feedback_id || null,
|
|
969
|
+
project_id: data.project_id || null,
|
|
970
|
+
title: data.title || null,
|
|
971
|
+
status: data.status || null,
|
|
972
|
+
deterministic: data.deterministic === true,
|
|
973
|
+
},
|
|
974
|
+
null,
|
|
975
|
+
2
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (data.feedback_id) console.log(`Feedback ID: ${data.feedback_id}`);
|
|
982
|
+
if (data.project_id) console.log(`Project ID: ${data.project_id}`);
|
|
983
|
+
if (data.title) console.log(`Title: ${data.title}`);
|
|
984
|
+
if (data.status) console.log(`Status: ${data.status}`);
|
|
985
|
+
}
|
|
986
|
+
|
|
487
987
|
async function runConfig(args) {
|
|
488
988
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
489
989
|
if (!key) {
|
|
@@ -532,6 +1032,100 @@ async function runConfig(args) {
|
|
|
532
1032
|
}
|
|
533
1033
|
}
|
|
534
1034
|
|
|
1035
|
+
async function runBootstrap(args) {
|
|
1036
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
1037
|
+
if (!key) {
|
|
1038
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
1043
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
1044
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
1045
|
+
|
|
1046
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
1047
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
1048
|
+
|
|
1049
|
+
let snapshot;
|
|
1050
|
+
try {
|
|
1051
|
+
snapshot = await fetchBootstrapSnapshot({ apiBase, key, timeoutMs });
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
console.error("Failed to fetch bootstrap snapshot:", err?.message || err);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (args["print-context"] || args.printContext) {
|
|
1058
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
let resolved;
|
|
1063
|
+
try {
|
|
1064
|
+
resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
console.error(err?.message || err);
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const wrapperRoot = resolved.root;
|
|
1071
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
1072
|
+
const dryRun = Boolean(args["dry-run"] || args.dryRun);
|
|
1073
|
+
const summary = {
|
|
1074
|
+
api_base: apiBase,
|
|
1075
|
+
project_id: snapshot?.project?.id || null,
|
|
1076
|
+
wrapper_root: wrapperRoot,
|
|
1077
|
+
output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
|
|
1078
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
1079
|
+
local: {
|
|
1080
|
+
mode: resolved.mode,
|
|
1081
|
+
found: (resolved.repos || []).map((repo) => repo.name),
|
|
1082
|
+
missing: resolved.missing || [],
|
|
1083
|
+
},
|
|
1084
|
+
counts: {
|
|
1085
|
+
phases: Array.isArray(snapshot.phases) ? snapshot.phases.length : 0,
|
|
1086
|
+
epics: Array.isArray(snapshot.epics) ? snapshot.epics.length : 0,
|
|
1087
|
+
stories: Array.isArray(snapshot.stories) ? snapshot.stories.length : 0,
|
|
1088
|
+
missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
|
|
1089
|
+
},
|
|
1090
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
1091
|
+
generated_at: snapshot.generated_at || null,
|
|
1092
|
+
dry_run: dryRun,
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
if (dryRun) {
|
|
1096
|
+
if (args.json) {
|
|
1097
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1098
|
+
} else {
|
|
1099
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1100
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1101
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1102
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1103
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1104
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1105
|
+
console.log(`Counts: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
1106
|
+
console.log("Dry run only - no files written.");
|
|
1107
|
+
}
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
1112
|
+
summary.data_root = writeResult.dataRoot;
|
|
1113
|
+
|
|
1114
|
+
if (args.json) {
|
|
1115
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1120
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1121
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1122
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1123
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1124
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1125
|
+
console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
1126
|
+
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
535
1129
|
async function runQuery(args) {
|
|
536
1130
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
537
1131
|
if (!key) {
|
|
@@ -563,6 +1157,7 @@ async function runQuery(args) {
|
|
|
563
1157
|
const apiBase = normalizeApiBase(baseRaw);
|
|
564
1158
|
|
|
565
1159
|
let diffText = "";
|
|
1160
|
+
let diffDiagnostics = null;
|
|
566
1161
|
if (includeDiff) {
|
|
567
1162
|
let cfg = null;
|
|
568
1163
|
try {
|
|
@@ -575,14 +1170,20 @@ async function runQuery(args) {
|
|
|
575
1170
|
|
|
576
1171
|
if (cfg) {
|
|
577
1172
|
const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
|
|
578
|
-
|
|
1173
|
+
const diffResult = collectGitDiffWithDiagnostics({
|
|
579
1174
|
projectId: cfg.project_id,
|
|
580
1175
|
repoNames,
|
|
581
1176
|
maxChars: diffLimit,
|
|
582
1177
|
fetchRemote,
|
|
583
1178
|
});
|
|
1179
|
+
diffText = diffResult.text;
|
|
1180
|
+
diffDiagnostics = diffResult.diagnostics;
|
|
1181
|
+
if (diffDiagnostics?.errors?.length) {
|
|
1182
|
+
for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
|
|
1183
|
+
}
|
|
584
1184
|
} else {
|
|
585
1185
|
diffText = "";
|
|
1186
|
+
diffDiagnostics = null;
|
|
586
1187
|
}
|
|
587
1188
|
if (!diffText) {
|
|
588
1189
|
console.error("Warning: no diff context collected. Continuing without --with-diff context.");
|
|
@@ -591,8 +1192,9 @@ async function runQuery(args) {
|
|
|
591
1192
|
|
|
592
1193
|
const payload = {
|
|
593
1194
|
query,
|
|
594
|
-
additional_context: buildContext(args, diffText),
|
|
1195
|
+
additional_context: buildContext(args, diffText, diffDiagnostics),
|
|
595
1196
|
};
|
|
1197
|
+
if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
|
|
596
1198
|
|
|
597
1199
|
if (printContext) {
|
|
598
1200
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -657,11 +1259,21 @@ async function main() {
|
|
|
657
1259
|
return;
|
|
658
1260
|
}
|
|
659
1261
|
|
|
1262
|
+
if (command === "bootstrap") {
|
|
1263
|
+
await runBootstrap(args);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
660
1267
|
if (command === "chat") {
|
|
661
1268
|
await runChat(args);
|
|
662
1269
|
return;
|
|
663
1270
|
}
|
|
664
1271
|
|
|
1272
|
+
if (command === "create-prd" || command === "add-prd" || command === "prd") {
|
|
1273
|
+
await runCreatePrd(args);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
665
1277
|
// query/ask default
|
|
666
1278
|
await runQuery(args);
|
|
667
1279
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mytegroupinc/myte-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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"
|
|
@@ -14,6 +22,7 @@
|
|
|
14
22
|
"dependencies": {
|
|
15
23
|
"dotenv": "^16.5.0",
|
|
16
24
|
"minimist": "^1.2.8",
|
|
17
|
-
"node-fetch": "^3.3.2"
|
|
25
|
+
"node-fetch": "^3.3.2",
|
|
26
|
+
"yaml": "^2.8.1"
|
|
18
27
|
}
|
|
19
28
|
}
|