@mytegroupinc/myte-core 0.0.5 → 0.0.7
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 -0
- package/cli.js +566 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,21 @@ Internal implementation package for the `myte` CLI.
|
|
|
4
4
|
|
|
5
5
|
Most users should install the unscoped wrapper instead:
|
|
6
6
|
- `npm install myte` then `npx myte bootstrap`
|
|
7
|
+
- `npm install myte` then `npx myte sync-qaqc`
|
|
7
8
|
- `npm install myte` then `npx myte query "..." --with-diff`
|
|
8
9
|
- `npm install myte` then `npm exec myte -- query "..." --with-diff`
|
|
10
|
+
- `npm install myte` then `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
11
|
+
- `npm install myte` then `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
9
12
|
- `npm i -g myte` then `myte bootstrap`
|
|
13
|
+
- `npm i -g myte` then `myte sync-qaqc`
|
|
10
14
|
- `npm i -g myte` then `myte query "..." --with-diff`
|
|
15
|
+
- `npm i -g myte` then `myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
16
|
+
- `npm i -g myte` then `myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
11
17
|
- `npx myte@latest bootstrap`
|
|
18
|
+
- `npx myte@latest sync-qaqc`
|
|
12
19
|
- `npx myte@latest query "..." --with-diff`
|
|
20
|
+
- `npx myte@latest update-team "Backend deploy completed; QAQC rerun queued."`
|
|
21
|
+
- `npx myte@latest update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
13
22
|
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
|
|
14
23
|
- `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
|
|
15
24
|
|
|
@@ -26,7 +35,17 @@ Notes:
|
|
|
26
35
|
- `bootstrap` is a local file materialization path, not a hosted file download.
|
|
27
36
|
- `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
|
|
28
37
|
- `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
|
|
38
|
+
- `bootstrap` materializes a public Command Center DTO, not raw backend documents.
|
|
39
|
+
- `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
|
|
40
|
+
- `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc` automatically if missing.
|
|
41
|
+
- `sync-qaqc` writes active mission QAQC cards to `MyteCommandCenter/data/qaqc/active-missions` and refreshes matching `MyteCommandCenter/data/missions` cards.
|
|
42
|
+
- `sync-qaqc` only exports active `Todo` / `In Progress` missions plus a public QAQC summary and sanitized latest batch metadata.
|
|
43
|
+
- `sync-qaqc` removes previously QAQC-managed mission files from `MyteCommandCenter/data/missions` once they leave the active set.
|
|
29
44
|
- `create-prd` is a deterministic PRD upload path, not an LLM generation command.
|
|
45
|
+
- `update-team` creates a project comment through `/api/project-assistant/project-comment`.
|
|
46
|
+
- `update-client` creates a client update draft through `/api/project-assistant/client-update-drafts`.
|
|
47
|
+
- `update-client` requires `--subject` plus body markdown from `--body-markdown`, `--body-file`, positional input, or `--stdin`.
|
|
48
|
+
- `update-client` accepts optional `--target-contact-id` repeats or `--target-contact-ids <id1,id2>`.
|
|
30
49
|
- `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
|
|
31
50
|
- `--with-diff` includes per-repo diagnostics in `print-context` payload:
|
|
32
51
|
- missing repo directories
|
|
@@ -44,8 +63,12 @@ Deterministic `create-prd` contract:
|
|
|
44
63
|
|
|
45
64
|
Examples:
|
|
46
65
|
- `npx myte bootstrap`
|
|
66
|
+
- `npx myte sync-qaqc`
|
|
47
67
|
- `npx myte bootstrap --dry-run --json`
|
|
68
|
+
- `npx myte sync-qaqc --dry-run --json`
|
|
48
69
|
- `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
|
|
49
70
|
- `npx myte create-prd ./drafts/auth-prd.md --print-context`
|
|
71
|
+
- `npx myte update-team "Backend deploy completed; QAQC rerun queued."`
|
|
72
|
+
- `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
|
|
50
73
|
|
|
51
74
|
This package is published under the org scope for governance; the public `myte` wrapper delegates here.
|
package/cli.js
CHANGED
|
@@ -49,7 +49,23 @@ function loadEnv() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function splitCommand(argv) {
|
|
52
|
-
const known = new Set([
|
|
52
|
+
const known = new Set([
|
|
53
|
+
"query",
|
|
54
|
+
"ask",
|
|
55
|
+
"chat",
|
|
56
|
+
"config",
|
|
57
|
+
"bootstrap",
|
|
58
|
+
"sync-qaqc",
|
|
59
|
+
"qaqc-sync",
|
|
60
|
+
"create-prd",
|
|
61
|
+
"add-prd",
|
|
62
|
+
"prd",
|
|
63
|
+
"update-team",
|
|
64
|
+
"update-client",
|
|
65
|
+
"help",
|
|
66
|
+
"--help",
|
|
67
|
+
"-h",
|
|
68
|
+
]);
|
|
53
69
|
const first = argv[0];
|
|
54
70
|
if (first && known.has(first)) {
|
|
55
71
|
const cmd = first === "--help" || first === "-h" ? "help" : first;
|
|
@@ -63,7 +79,25 @@ function parseArgs(argv) {
|
|
|
63
79
|
// eslint-disable-next-line global-require
|
|
64
80
|
return require("minimist")(argv, {
|
|
65
81
|
boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin"],
|
|
66
|
-
string: [
|
|
82
|
+
string: [
|
|
83
|
+
"query",
|
|
84
|
+
"q",
|
|
85
|
+
"context",
|
|
86
|
+
"ctx",
|
|
87
|
+
"base-url",
|
|
88
|
+
"timeout-ms",
|
|
89
|
+
"diff-limit",
|
|
90
|
+
"title",
|
|
91
|
+
"description",
|
|
92
|
+
"feedback-text",
|
|
93
|
+
"output-dir",
|
|
94
|
+
"content",
|
|
95
|
+
"subject",
|
|
96
|
+
"body-markdown",
|
|
97
|
+
"body-file",
|
|
98
|
+
"target-contact-id",
|
|
99
|
+
"target-contact-ids",
|
|
100
|
+
],
|
|
67
101
|
alias: {
|
|
68
102
|
q: "query",
|
|
69
103
|
d: "with-diff",
|
|
@@ -117,10 +151,14 @@ function printHelp() {
|
|
|
117
151
|
" myte query \"<text>\" [--with-diff] [--context \"...\"]",
|
|
118
152
|
" myte config [--json]",
|
|
119
153
|
" myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
|
|
154
|
+
" myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
|
|
155
|
+
" myte update-team \"<content>\" [--json]",
|
|
156
|
+
" myte update-client --subject \"<text>\" [--body-markdown \"...\"] [--body-file ./update.md] [--target-contact-ids <id1,id2>] [--json]",
|
|
120
157
|
" myte chat",
|
|
121
158
|
" myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
|
|
122
159
|
" myte add-prd <file.md> [--json]",
|
|
123
160
|
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
161
|
+
" cat update.md | myte update-client --stdin --subject \"Weekly client update\"",
|
|
124
162
|
"",
|
|
125
163
|
"Run forms:",
|
|
126
164
|
" npm install myte then npx myte query \"...\" --with-diff",
|
|
@@ -136,21 +174,42 @@ function printHelp() {
|
|
|
136
174
|
" - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
|
|
137
175
|
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
138
176
|
"",
|
|
177
|
+
"sync-qaqc contract:",
|
|
178
|
+
" - Run from the wrapper root that contains the project's configured repo folders",
|
|
179
|
+
" - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc automatically",
|
|
180
|
+
" - Writes active mission QAQC context under MyteCommandCenter/data/qaqc/active-missions",
|
|
181
|
+
" - Refreshes matching MyteCommandCenter/data/missions cards for active missions only",
|
|
182
|
+
"",
|
|
139
183
|
"create-prd contract:",
|
|
140
184
|
" - Required: valid MYTE_API_KEY, PRD markdown body, title",
|
|
141
185
|
" - Title source: myte-kanban.title, first # heading, or --title",
|
|
142
186
|
" - Description source: myte-kanban.description or --description",
|
|
143
187
|
" - PRD DOCX content: the markdown body is stored verbatim",
|
|
144
188
|
"",
|
|
189
|
+
"update-team contract:",
|
|
190
|
+
" - Creates a project team comment through /api/project-assistant/project-comment",
|
|
191
|
+
" - Required: content (inline, --content, or --stdin)",
|
|
192
|
+
"",
|
|
193
|
+
"update-client contract:",
|
|
194
|
+
" - Creates a client update draft through /api/project-assistant/client-update-drafts",
|
|
195
|
+
" - Required: --subject and body markdown (via --body-markdown, --body-file, positional file/text, or --stdin)",
|
|
196
|
+
" - Optional: --target-contact-id <id> (repeatable) or --target-contact-ids <id1,id2>",
|
|
197
|
+
"",
|
|
145
198
|
"Options:",
|
|
146
199
|
" --with-diff Include deterministic git diffs (project-scoped)",
|
|
147
200
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
148
201
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
149
202
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
150
203
|
" --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
|
|
151
|
-
" --stdin Read
|
|
204
|
+
" --stdin Read supported command content from stdin instead of inline text or a file path",
|
|
152
205
|
" --title <text> Override PRD title for raw markdown uploads",
|
|
153
206
|
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
207
|
+
" --content <text> Team update content for update-team",
|
|
208
|
+
" --subject <text> Client update subject for update-client",
|
|
209
|
+
" --body-markdown <md> Client update markdown body for update-client",
|
|
210
|
+
" --body-file <path> Read client update markdown body from a file",
|
|
211
|
+
" --target-contact-id Add one client contact ObjectId (repeatable)",
|
|
212
|
+
" --target-contact-ids Comma-separated client contact ObjectIds",
|
|
154
213
|
" --print-context Print JSON payload and exit (no query call)",
|
|
155
214
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
156
215
|
"",
|
|
@@ -158,6 +217,10 @@ function printHelp() {
|
|
|
158
217
|
" myte query \"What changed in logging?\" --with-diff",
|
|
159
218
|
" myte bootstrap",
|
|
160
219
|
" myte bootstrap --output-dir ./MyteCommandCenter",
|
|
220
|
+
" myte sync-qaqc",
|
|
221
|
+
" myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
|
|
222
|
+
" myte update-client --subject \"Weekly client update\" --body-file ./updates/week-12.md",
|
|
223
|
+
" myte update-client --subject \"Weekly client update\" --body-markdown \"## Progress\\n- Login complete\" --target-contact-ids 507f1f77bcf86cd799439011,507f1f77bcf86cd799439012",
|
|
161
224
|
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
|
|
162
225
|
" cat ./drafts/auth-prd.md | myte create-prd --stdin",
|
|
163
226
|
" myte config",
|
|
@@ -194,6 +257,139 @@ async function readStdinText() {
|
|
|
194
257
|
});
|
|
195
258
|
}
|
|
196
259
|
|
|
260
|
+
function firstNonEmptyString(...values) {
|
|
261
|
+
for (const value of values) {
|
|
262
|
+
if (value === undefined || value === null) continue;
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
const nested = firstNonEmptyString(...value);
|
|
265
|
+
if (nested) return nested;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const text = String(value).trim();
|
|
269
|
+
if (text) return text;
|
|
270
|
+
}
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toStringArray(value) {
|
|
275
|
+
if (value === undefined || value === null) return [];
|
|
276
|
+
return Array.isArray(value) ? value.flatMap((item) => toStringArray(item)) : [String(value)];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseCsvValues(...values) {
|
|
280
|
+
return values
|
|
281
|
+
.flatMap((value) => toStringArray(value))
|
|
282
|
+
.flatMap((value) => value.split(","))
|
|
283
|
+
.map((value) => value.trim())
|
|
284
|
+
.filter(Boolean);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function dedupeStrings(values) {
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
const unique = [];
|
|
290
|
+
for (const value of values) {
|
|
291
|
+
const normalized = String(value || "").trim();
|
|
292
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
293
|
+
seen.add(normalized);
|
|
294
|
+
unique.push(normalized);
|
|
295
|
+
}
|
|
296
|
+
return unique;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resolveTimeoutMs(args) {
|
|
300
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
301
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
302
|
+
return Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolveApiBase(args) {
|
|
306
|
+
const baseRaw =
|
|
307
|
+
args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
308
|
+
return normalizeApiBase(baseRaw);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getProjectApiKey() {
|
|
312
|
+
return (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resolveInputFile(candidatePath, label) {
|
|
316
|
+
const resolved = String(candidatePath || "").trim();
|
|
317
|
+
if (!resolved) return null;
|
|
318
|
+
const absPath = path.resolve(process.cwd(), resolved);
|
|
319
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
320
|
+
console.error(`${label} file not found: ${absPath}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
return absPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function resolveTeamUpdateContent(args) {
|
|
327
|
+
const inlineContent = firstNonEmptyString(args.content, Array.isArray(args._) ? args._.join(" ") : args._);
|
|
328
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !inlineContent));
|
|
329
|
+
if (useStdin) {
|
|
330
|
+
const stdinContent = String((await readStdinText()) || "").trim();
|
|
331
|
+
if (!stdinContent) {
|
|
332
|
+
console.error("Team update content is empty.");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
return stdinContent;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const content = String(inlineContent || "").trim();
|
|
339
|
+
if (!content) {
|
|
340
|
+
console.error("Missing team update content.");
|
|
341
|
+
printHelp();
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
return content;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function resolveClientUpdateBody(args) {
|
|
348
|
+
const inlineBody = firstNonEmptyString(
|
|
349
|
+
args["body-markdown"],
|
|
350
|
+
args.bodyMarkdown,
|
|
351
|
+
args.body_markdown
|
|
352
|
+
);
|
|
353
|
+
if (inlineBody) return inlineBody;
|
|
354
|
+
|
|
355
|
+
const explicitFile = firstNonEmptyString(args["body-file"], args.bodyFile, args.body_file);
|
|
356
|
+
const positionalInput = firstNonEmptyString(Array.isArray(args._) ? args._.join(" ") : args._);
|
|
357
|
+
const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !explicitFile && !positionalInput));
|
|
358
|
+
if (useStdin) {
|
|
359
|
+
const stdinBody = String((await readStdinText()) || "").trim();
|
|
360
|
+
if (!stdinBody) {
|
|
361
|
+
console.error("Client update body_markdown is empty.");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
return stdinBody;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const fileCandidate = explicitFile || (positionalInput && fs.existsSync(path.resolve(process.cwd(), positionalInput)) ? positionalInput : "");
|
|
368
|
+
if (fileCandidate) {
|
|
369
|
+
const absPath = resolveInputFile(fileCandidate, "Client update body");
|
|
370
|
+
return String(fs.readFileSync(absPath, "utf8") || "").trim();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (positionalInput) return positionalInput;
|
|
374
|
+
|
|
375
|
+
console.error("Missing client update body_markdown.");
|
|
376
|
+
printHelp();
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function resolveTargetContactIds(args) {
|
|
381
|
+
return dedupeStrings(
|
|
382
|
+
parseCsvValues(
|
|
383
|
+
args["target-contact-id"],
|
|
384
|
+
args.targetContactId,
|
|
385
|
+
args.target_contact_id,
|
|
386
|
+
args["target-contact-ids"],
|
|
387
|
+
args.targetContactIds,
|
|
388
|
+
args.target_contact_ids
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
197
393
|
async function getFetch() {
|
|
198
394
|
if (typeof fetch !== "undefined") return fetch;
|
|
199
395
|
const mod = await import("node-fetch");
|
|
@@ -733,6 +929,28 @@ async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
|
|
|
733
929
|
return body.data || {};
|
|
734
930
|
}
|
|
735
931
|
|
|
932
|
+
async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
|
|
933
|
+
const fetchFn = await getFetch();
|
|
934
|
+
const url = `${apiBase}/project-assistant/qaqc-sync`;
|
|
935
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
936
|
+
fetchFn,
|
|
937
|
+
url,
|
|
938
|
+
{
|
|
939
|
+
method: "GET",
|
|
940
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
941
|
+
},
|
|
942
|
+
timeoutMs
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (!resp.ok || body.status !== "success") {
|
|
946
|
+
const msg = body?.message || `QAQC sync request failed (${resp.status})`;
|
|
947
|
+
const err = new Error(msg);
|
|
948
|
+
err.status = resp.status;
|
|
949
|
+
throw err;
|
|
950
|
+
}
|
|
951
|
+
return body.data || {};
|
|
952
|
+
}
|
|
953
|
+
|
|
736
954
|
async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
737
955
|
const fetchFn = await getFetch();
|
|
738
956
|
const url = `${apiBase}${endpoint}`;
|
|
@@ -759,6 +977,138 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
|
|
|
759
977
|
return body.data || {};
|
|
760
978
|
}
|
|
761
979
|
|
|
980
|
+
function formatTargetContacts(contacts) {
|
|
981
|
+
const items = Array.isArray(contacts) ? contacts : [];
|
|
982
|
+
const formatted = items
|
|
983
|
+
.map((contact) => {
|
|
984
|
+
const name = String(contact?.name || "").trim();
|
|
985
|
+
const email = String(contact?.email || "").trim();
|
|
986
|
+
if (name && email) return `${name} <${email}>`;
|
|
987
|
+
return name || email || String(contact?.contact_id || "").trim();
|
|
988
|
+
})
|
|
989
|
+
.filter(Boolean);
|
|
990
|
+
return formatted.join(", ");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function runUpdateTeam(args) {
|
|
994
|
+
const key = getProjectApiKey();
|
|
995
|
+
if (!key) {
|
|
996
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const payload = {
|
|
1001
|
+
content: await resolveTeamUpdateContent(args),
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
|
|
1005
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
1010
|
+
const apiBase = resolveApiBase(args);
|
|
1011
|
+
|
|
1012
|
+
let data;
|
|
1013
|
+
try {
|
|
1014
|
+
data = await callAssistantQuery({
|
|
1015
|
+
apiBase,
|
|
1016
|
+
key,
|
|
1017
|
+
payload,
|
|
1018
|
+
timeoutMs,
|
|
1019
|
+
endpoint: "/project-assistant/project-comment",
|
|
1020
|
+
});
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
if (err?.name === "AbortError") {
|
|
1023
|
+
console.error(`Request timed out after ${timeoutMs}ms`);
|
|
1024
|
+
} else {
|
|
1025
|
+
console.error("Team update failed:", err?.message || err);
|
|
1026
|
+
}
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (args.json) {
|
|
1031
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (data.comment_id) console.log(`Comment ID: ${data.comment_id}`);
|
|
1036
|
+
if (data.project_id) console.log(`Project ID: ${data.project_id}`);
|
|
1037
|
+
if (data.user?.name) console.log(`Author: ${data.user.name}`);
|
|
1038
|
+
if (data.created_at) console.log(`Created At: ${data.created_at}`);
|
|
1039
|
+
if (data.content) {
|
|
1040
|
+
console.log("Content:");
|
|
1041
|
+
console.log(data.content);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function runUpdateClient(args) {
|
|
1046
|
+
const key = getProjectApiKey();
|
|
1047
|
+
if (!key) {
|
|
1048
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const subject = firstNonEmptyString(args.subject);
|
|
1053
|
+
if (!subject) {
|
|
1054
|
+
console.error("Missing --subject for client update.");
|
|
1055
|
+
printHelp();
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const payload = {
|
|
1060
|
+
subject,
|
|
1061
|
+
body_markdown: await resolveClientUpdateBody(args),
|
|
1062
|
+
target_contact_ids: resolveTargetContactIds(args),
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
if (args["print-context"] || args.printContext || args["dry-run"] || args.dryRun) {
|
|
1066
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
1071
|
+
const apiBase = resolveApiBase(args);
|
|
1072
|
+
|
|
1073
|
+
let data;
|
|
1074
|
+
try {
|
|
1075
|
+
data = await callAssistantQuery({
|
|
1076
|
+
apiBase,
|
|
1077
|
+
key,
|
|
1078
|
+
payload,
|
|
1079
|
+
timeoutMs,
|
|
1080
|
+
endpoint: "/project-assistant/client-update-drafts",
|
|
1081
|
+
});
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
if (err?.name === "AbortError") {
|
|
1084
|
+
console.error(`Request timed out after ${timeoutMs}ms`);
|
|
1085
|
+
} else {
|
|
1086
|
+
console.error("Client update draft failed:", err?.message || err);
|
|
1087
|
+
}
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (args.json) {
|
|
1092
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (data.draft_id) console.log(`Draft ID: ${data.draft_id}`);
|
|
1097
|
+
if (data.project_title) console.log(`Project: ${data.project_title}`);
|
|
1098
|
+
if (data.project_id) console.log(`Project ID: ${data.project_id}`);
|
|
1099
|
+
if (data.status) console.log(`Status: ${data.status}`);
|
|
1100
|
+
if (data.subject) console.log(`Subject: ${data.subject}`);
|
|
1101
|
+
if (data.created_by_name) console.log(`Author: ${data.created_by_name}`);
|
|
1102
|
+
const targets = formatTargetContacts(data.target_contacts);
|
|
1103
|
+
if (targets) console.log(`Targets: ${targets}`);
|
|
1104
|
+
if (data.created_at) console.log(`Created At: ${data.created_at}`);
|
|
1105
|
+
if (data.updated_at) console.log(`Updated At: ${data.updated_at}`);
|
|
1106
|
+
if (data.snippet) {
|
|
1107
|
+
console.log("Snippet:");
|
|
1108
|
+
console.log(data.snippet);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
762
1112
|
function ensureDir(dirPath) {
|
|
763
1113
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
764
1114
|
}
|
|
@@ -799,6 +1149,46 @@ function writeJsonFile(filePath, value) {
|
|
|
799
1149
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
800
1150
|
}
|
|
801
1151
|
|
|
1152
|
+
function readJsonFile(filePath) {
|
|
1153
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
|
|
1154
|
+
try {
|
|
1155
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1156
|
+
} catch {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const BOOTSTRAP_FORBIDDEN_KEYS = new Set([
|
|
1162
|
+
"_id",
|
|
1163
|
+
"org_id",
|
|
1164
|
+
"project_id",
|
|
1165
|
+
"created_by",
|
|
1166
|
+
"assigned_to",
|
|
1167
|
+
"user_id",
|
|
1168
|
+
"qa_qc_results",
|
|
1169
|
+
"job_id",
|
|
1170
|
+
"job_ids",
|
|
1171
|
+
"celery_task_id",
|
|
1172
|
+
"conversation_id",
|
|
1173
|
+
"error",
|
|
1174
|
+
]);
|
|
1175
|
+
|
|
1176
|
+
function scrubBootstrapValue(value) {
|
|
1177
|
+
if (Array.isArray(value)) {
|
|
1178
|
+
return value.map((item) => scrubBootstrapValue(item));
|
|
1179
|
+
}
|
|
1180
|
+
if (!value || typeof value !== "object") {
|
|
1181
|
+
return value;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const cleaned = {};
|
|
1185
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1186
|
+
if (BOOTSTRAP_FORBIDDEN_KEYS.has(key)) continue;
|
|
1187
|
+
cleaned[key] = scrubBootstrapValue(child);
|
|
1188
|
+
}
|
|
1189
|
+
return cleaned;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
802
1192
|
function resolveBootstrapWorkspace(repoNames) {
|
|
803
1193
|
const resolved = resolveConfiguredRepos(repoNames);
|
|
804
1194
|
if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
|
|
@@ -826,10 +1216,10 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
826
1216
|
clearYamlDirectory(storiesDir);
|
|
827
1217
|
clearYamlDirectory(missionsDir);
|
|
828
1218
|
|
|
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 : [];
|
|
1219
|
+
const phases = Array.isArray(snapshot.phases) ? snapshot.phases.map((item) => scrubBootstrapValue(item)) : [];
|
|
1220
|
+
const epics = Array.isArray(snapshot.epics) ? snapshot.epics.map((item) => scrubBootstrapValue(item)) : [];
|
|
1221
|
+
const stories = Array.isArray(snapshot.stories) ? snapshot.stories.map((item) => scrubBootstrapValue(item)) : [];
|
|
1222
|
+
const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
|
|
833
1223
|
|
|
834
1224
|
phases.forEach((phase, index) => {
|
|
835
1225
|
const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
|
|
@@ -844,19 +1234,19 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
844
1234
|
writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
|
|
845
1235
|
});
|
|
846
1236
|
missions.forEach((mission, index) => {
|
|
847
|
-
const missionId = stableItemId(mission, ["mission_id", "id"
|
|
1237
|
+
const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
|
|
848
1238
|
writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
|
|
849
1239
|
});
|
|
850
1240
|
|
|
851
1241
|
if (snapshot.project && typeof snapshot.project === "object") {
|
|
852
|
-
writeYamlFile(path.join(dataRoot, "project.yml"), snapshot.project);
|
|
1242
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
853
1243
|
}
|
|
854
1244
|
|
|
855
1245
|
const manifest = {
|
|
856
1246
|
schema_version: snapshot.schema_version || 1,
|
|
857
1247
|
generated_at: snapshot.generated_at || null,
|
|
858
1248
|
snapshot_hash: snapshot.snapshot_hash || null,
|
|
859
|
-
project: snapshot.project
|
|
1249
|
+
project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
|
|
860
1250
|
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
861
1251
|
counts: {
|
|
862
1252
|
phases: phases.length,
|
|
@@ -874,6 +1264,66 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
|
874
1264
|
};
|
|
875
1265
|
}
|
|
876
1266
|
|
|
1267
|
+
function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
1268
|
+
const targetRoot = outputDir
|
|
1269
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
1270
|
+
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
1271
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
1272
|
+
const missionsDir = path.join(dataRoot, "missions");
|
|
1273
|
+
const qaqcRoot = path.join(dataRoot, "qaqc");
|
|
1274
|
+
const activeMissionsDir = path.join(qaqcRoot, "active-missions");
|
|
1275
|
+
const manifestPath = path.join(qaqcRoot, "manifest.json");
|
|
1276
|
+
|
|
1277
|
+
ensureDir(dataRoot);
|
|
1278
|
+
ensureDir(missionsDir);
|
|
1279
|
+
ensureDir(qaqcRoot);
|
|
1280
|
+
const previousManifest = readJsonFile(manifestPath);
|
|
1281
|
+
const previousMissionIds = Array.isArray(previousManifest?.active_mission_ids)
|
|
1282
|
+
? previousManifest.active_mission_ids.map((item) => String(item).trim()).filter(Boolean)
|
|
1283
|
+
: [];
|
|
1284
|
+
clearYamlDirectory(activeMissionsDir);
|
|
1285
|
+
|
|
1286
|
+
const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
|
|
1287
|
+
const currentMissionIds = [];
|
|
1288
|
+
|
|
1289
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
1290
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
missions.forEach((mission, index) => {
|
|
1294
|
+
const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
|
|
1295
|
+
currentMissionIds.push(missionId);
|
|
1296
|
+
writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
|
|
1297
|
+
writeYamlFile(path.join(activeMissionsDir, `${missionId}.yml`), mission);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
const currentMissionIdSet = new Set(currentMissionIds);
|
|
1301
|
+
previousMissionIds.forEach((missionId) => {
|
|
1302
|
+
if (!missionId || currentMissionIdSet.has(missionId)) return;
|
|
1303
|
+
fs.rmSync(path.join(missionsDir, `${missionId}.yml`), { force: true });
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
const manifest = {
|
|
1307
|
+
schema_version: snapshot.schema_version || 1,
|
|
1308
|
+
generated_at: snapshot.generated_at || null,
|
|
1309
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
1310
|
+
project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
|
|
1311
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
1312
|
+
active_mission_ids: currentMissionIds,
|
|
1313
|
+
counts: snapshot.counts && typeof snapshot.counts === "object" ? scrubBootstrapValue(snapshot.counts) : {
|
|
1314
|
+
active_missions: missions.length,
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
1317
|
+
writeJsonFile(manifestPath, manifest);
|
|
1318
|
+
writeJsonFile(path.join(qaqcRoot, "latest-batch.json"), snapshot.latest_batch ? scrubBootstrapValue(snapshot.latest_batch) : null);
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
targetRoot,
|
|
1322
|
+
dataRoot,
|
|
1323
|
+
manifest,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
877
1327
|
async function runCreatePrd(args) {
|
|
878
1328
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
879
1329
|
if (!key) {
|
|
@@ -1126,6 +1576,97 @@ async function runBootstrap(args) {
|
|
|
1126
1576
|
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
1127
1577
|
}
|
|
1128
1578
|
|
|
1579
|
+
async function runSyncQaqc(args) {
|
|
1580
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
1581
|
+
if (!key) {
|
|
1582
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
1587
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
1588
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
1589
|
+
|
|
1590
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
1591
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
1592
|
+
|
|
1593
|
+
let snapshot;
|
|
1594
|
+
try {
|
|
1595
|
+
snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
console.error("Failed to fetch QAQC sync snapshot:", err?.message || err);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (args["print-context"] || args.printContext) {
|
|
1602
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
let resolved;
|
|
1607
|
+
try {
|
|
1608
|
+
resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
console.error(err?.message || err);
|
|
1611
|
+
process.exit(1);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const wrapperRoot = resolved.root;
|
|
1615
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
1616
|
+
const dryRun = Boolean(args["dry-run"] || args.dryRun);
|
|
1617
|
+
const summary = {
|
|
1618
|
+
api_base: apiBase,
|
|
1619
|
+
project_id: snapshot?.project?.id || null,
|
|
1620
|
+
wrapper_root: wrapperRoot,
|
|
1621
|
+
output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
|
|
1622
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
1623
|
+
local: {
|
|
1624
|
+
mode: resolved.mode,
|
|
1625
|
+
found: (resolved.repos || []).map((repo) => repo.name),
|
|
1626
|
+
missing: resolved.missing || [],
|
|
1627
|
+
},
|
|
1628
|
+
counts: snapshot.counts && typeof snapshot.counts === "object" ? snapshot.counts : {
|
|
1629
|
+
active_missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
|
|
1630
|
+
},
|
|
1631
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
1632
|
+
generated_at: snapshot.generated_at || null,
|
|
1633
|
+
dry_run: dryRun,
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
if (dryRun) {
|
|
1637
|
+
if (args.json) {
|
|
1638
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1639
|
+
} else {
|
|
1640
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1641
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1642
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1643
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1644
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1645
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1646
|
+
console.log(`Counts: active_missions=${summary.counts.active_missions || 0}, todo=${summary.counts.todo || 0}, in_progress=${summary.counts.in_progress || 0}, with_failures=${summary.counts.with_failures || 0}`);
|
|
1647
|
+
console.log("Dry run only - no files written.");
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const writeResult = writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
1653
|
+
summary.data_root = writeResult.dataRoot;
|
|
1654
|
+
|
|
1655
|
+
if (args.json) {
|
|
1656
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1661
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1662
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1663
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1664
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1665
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1666
|
+
console.log(`Wrote QAQC: active_missions=${summary.counts.active_missions || 0}, todo=${summary.counts.todo || 0}, in_progress=${summary.counts.in_progress || 0}, with_failures=${summary.counts.with_failures || 0}`);
|
|
1667
|
+
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1129
1670
|
async function runQuery(args) {
|
|
1130
1671
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
1131
1672
|
if (!key) {
|
|
@@ -1264,6 +1805,11 @@ async function main() {
|
|
|
1264
1805
|
return;
|
|
1265
1806
|
}
|
|
1266
1807
|
|
|
1808
|
+
if (command === "sync-qaqc" || command === "qaqc-sync") {
|
|
1809
|
+
await runSyncQaqc(args);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1267
1813
|
if (command === "chat") {
|
|
1268
1814
|
await runChat(args);
|
|
1269
1815
|
return;
|
|
@@ -1274,6 +1820,16 @@ async function main() {
|
|
|
1274
1820
|
return;
|
|
1275
1821
|
}
|
|
1276
1822
|
|
|
1823
|
+
if (command === "update-team") {
|
|
1824
|
+
await runUpdateTeam(args);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (command === "update-client") {
|
|
1829
|
+
await runUpdateClient(args);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1277
1833
|
// query/ask default
|
|
1278
1834
|
await runQuery(args);
|
|
1279
1835
|
}
|