@mytegroupinc/myte-core 0.0.4 → 0.0.6
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 +31 -0
- package/cli.js +489 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
Internal implementation package for the `myte` CLI.
|
|
4
4
|
|
|
5
5
|
Most users should install the unscoped wrapper instead:
|
|
6
|
+
- `npm install myte` then `npx myte bootstrap`
|
|
7
|
+
- `npm install myte` then `npx myte sync-qaqc`
|
|
6
8
|
- `npm install myte` then `npx myte query "..." --with-diff`
|
|
7
9
|
- `npm install myte` then `npm exec myte -- query "..." --with-diff`
|
|
10
|
+
- `npm i -g myte` then `myte bootstrap`
|
|
11
|
+
- `npm i -g myte` then `myte sync-qaqc`
|
|
8
12
|
- `npm i -g myte` then `myte query "..." --with-diff`
|
|
13
|
+
- `npx myte@latest bootstrap`
|
|
14
|
+
- `npx myte@latest sync-qaqc`
|
|
9
15
|
- `npx myte@latest query "..." --with-diff`
|
|
10
16
|
- `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
|
|
11
17
|
- `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
|
|
@@ -20,6 +26,15 @@ Requirements:
|
|
|
20
26
|
Notes:
|
|
21
27
|
- `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
|
|
22
28
|
- `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
|
|
29
|
+
- `bootstrap` is a local file materialization path, not a hosted file download.
|
|
30
|
+
- `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
|
|
31
|
+
- `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
|
|
32
|
+
- `bootstrap` materializes a public Command Center DTO, not raw backend documents.
|
|
33
|
+
- `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
|
|
34
|
+
- `sync-qaqc` works without `bootstrap`; it creates `MyteCommandCenter/data/qaqc` automatically if missing.
|
|
35
|
+
- `sync-qaqc` writes active mission QAQC cards to `MyteCommandCenter/data/qaqc/active-missions` and refreshes matching `MyteCommandCenter/data/missions` cards.
|
|
36
|
+
- `sync-qaqc` only exports active `Todo` / `In Progress` missions plus a public QAQC summary and sanitized latest batch metadata.
|
|
37
|
+
- `sync-qaqc` removes previously QAQC-managed mission files from `MyteCommandCenter/data/missions` once they leave the active set.
|
|
23
38
|
- `create-prd` is a deterministic PRD upload path, not an LLM generation command.
|
|
24
39
|
- `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
|
|
25
40
|
- `--with-diff` includes per-repo diagnostics in `print-context` payload:
|
|
@@ -28,4 +43,20 @@ Notes:
|
|
|
28
43
|
- clean/no-change repo summaries
|
|
29
44
|
- `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
|
|
30
45
|
|
|
46
|
+
Deterministic `create-prd` contract:
|
|
47
|
+
- Required: `MYTE_API_KEY`, a PRD markdown body, and a title.
|
|
48
|
+
- Title source: `myte-kanban.title`, the first markdown `# Heading`, or `--title`.
|
|
49
|
+
- Description source: `myte-kanban.description` or `--description`.
|
|
50
|
+
- The markdown body is stored verbatim as PRD content and is what the backend uses to build the PRD DOCX.
|
|
51
|
+
- Legacy `feedback_text` is still accepted for older payloads, but new callers should use `description`.
|
|
52
|
+
- Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
- `npx myte bootstrap`
|
|
56
|
+
- `npx myte sync-qaqc`
|
|
57
|
+
- `npx myte bootstrap --dry-run --json`
|
|
58
|
+
- `npx myte sync-qaqc --dry-run --json`
|
|
59
|
+
- `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
|
|
60
|
+
- `npx myte create-prd ./drafts/auth-prd.md --print-context`
|
|
61
|
+
|
|
31
62
|
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", "create-prd", "add-prd", "prd", "help", "--help", "-h"]);
|
|
52
|
+
const known = new Set(["query", "ask", "chat", "config", "bootstrap", "sync-qaqc", "qaqc-sync", "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;
|
|
@@ -63,7 +63,7 @@ function parseArgs(argv) {
|
|
|
63
63
|
// eslint-disable-next-line global-require
|
|
64
64
|
return require("minimist")(argv, {
|
|
65
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"],
|
|
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,10 +116,12 @@ 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]",
|
|
120
|
+
" myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
|
|
119
121
|
" myte chat",
|
|
120
|
-
" myte create-prd <file.md> [--json]",
|
|
122
|
+
" myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
|
|
121
123
|
" myte add-prd <file.md> [--json]",
|
|
122
|
-
" cat file.md | myte create-prd --stdin [--title \"...\"]",
|
|
124
|
+
" cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
|
|
123
125
|
"",
|
|
124
126
|
"Run forms:",
|
|
125
127
|
" npm install myte then npx myte query \"...\" --with-diff",
|
|
@@ -130,19 +132,41 @@ function printHelp() {
|
|
|
130
132
|
"Auth:",
|
|
131
133
|
" - Set MYTE_API_KEY in a workspace .env (or env var)",
|
|
132
134
|
"",
|
|
135
|
+
"bootstrap contract:",
|
|
136
|
+
" - Run from the wrapper root that contains the project's configured repo folders",
|
|
137
|
+
" - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
|
|
138
|
+
" - Uses the project-scoped bootstrap snapshot from the Myte API",
|
|
139
|
+
"",
|
|
140
|
+
"sync-qaqc contract:",
|
|
141
|
+
" - Run from the wrapper root that contains the project's configured repo folders",
|
|
142
|
+
" - Works even if bootstrap has not been run yet; it creates MyteCommandCenter/data/qaqc automatically",
|
|
143
|
+
" - Writes active mission QAQC context under MyteCommandCenter/data/qaqc/active-missions",
|
|
144
|
+
" - Refreshes matching MyteCommandCenter/data/missions cards for active missions only",
|
|
145
|
+
"",
|
|
146
|
+
"create-prd contract:",
|
|
147
|
+
" - Required: valid MYTE_API_KEY, PRD markdown body, title",
|
|
148
|
+
" - Title source: myte-kanban.title, first # heading, or --title",
|
|
149
|
+
" - Description source: myte-kanban.description or --description",
|
|
150
|
+
" - PRD DOCX content: the markdown body is stored verbatim",
|
|
151
|
+
"",
|
|
133
152
|
"Options:",
|
|
134
153
|
" --with-diff Include deterministic git diffs (project-scoped)",
|
|
135
154
|
" --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
|
|
136
155
|
" --timeout-ms <ms> Request timeout (default: 300000)",
|
|
137
156
|
" --base-url <url> API base (default: https://api.myte.dev)",
|
|
157
|
+
" --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
|
|
138
158
|
" --stdin Read PRD content from stdin instead of a file path",
|
|
139
|
-
" --title <text>
|
|
159
|
+
" --title <text> Override PRD title for raw markdown uploads",
|
|
160
|
+
" --description <text> Set feedback description/card summary for raw markdown uploads",
|
|
140
161
|
" --print-context Print JSON payload and exit (no query call)",
|
|
141
162
|
" --no-fetch Don't git fetch origin main/master before diff",
|
|
142
163
|
"",
|
|
143
164
|
"Examples:",
|
|
144
165
|
" myte query \"What changed in logging?\" --with-diff",
|
|
145
|
-
" myte
|
|
166
|
+
" myte bootstrap",
|
|
167
|
+
" myte bootstrap --output-dir ./MyteCommandCenter",
|
|
168
|
+
" myte sync-qaqc",
|
|
169
|
+
" myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
|
|
146
170
|
" cat ./drafts/auth-prd.md | myte create-prd --stdin",
|
|
147
171
|
" myte config",
|
|
148
172
|
].join("\n");
|
|
@@ -695,6 +719,50 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
|
|
|
695
719
|
return body.data || {};
|
|
696
720
|
}
|
|
697
721
|
|
|
722
|
+
async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
|
|
723
|
+
const fetchFn = await getFetch();
|
|
724
|
+
const url = `${apiBase}/project-assistant/bootstrap`;
|
|
725
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
726
|
+
fetchFn,
|
|
727
|
+
url,
|
|
728
|
+
{
|
|
729
|
+
method: "GET",
|
|
730
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
731
|
+
},
|
|
732
|
+
timeoutMs
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (!resp.ok || body.status !== "success") {
|
|
736
|
+
const msg = body?.message || `Bootstrap request failed (${resp.status})`;
|
|
737
|
+
const err = new Error(msg);
|
|
738
|
+
err.status = resp.status;
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
return body.data || {};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
|
|
745
|
+
const fetchFn = await getFetch();
|
|
746
|
+
const url = `${apiBase}/project-assistant/qaqc-sync`;
|
|
747
|
+
const { resp, body } = await fetchJsonWithTimeout(
|
|
748
|
+
fetchFn,
|
|
749
|
+
url,
|
|
750
|
+
{
|
|
751
|
+
method: "GET",
|
|
752
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
753
|
+
},
|
|
754
|
+
timeoutMs
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
if (!resp.ok || body.status !== "success") {
|
|
758
|
+
const msg = body?.message || `QAQC sync request failed (${resp.status})`;
|
|
759
|
+
const err = new Error(msg);
|
|
760
|
+
err.status = resp.status;
|
|
761
|
+
throw err;
|
|
762
|
+
}
|
|
763
|
+
return body.data || {};
|
|
764
|
+
}
|
|
765
|
+
|
|
698
766
|
async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
|
|
699
767
|
const fetchFn = await getFetch();
|
|
700
768
|
const url = `${apiBase}${endpoint}`;
|
|
@@ -721,6 +789,221 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
|
|
|
721
789
|
return body.data || {};
|
|
722
790
|
}
|
|
723
791
|
|
|
792
|
+
function ensureDir(dirPath) {
|
|
793
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function clearYamlDirectory(dirPath) {
|
|
797
|
+
if (!fs.existsSync(dirPath)) {
|
|
798
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
802
|
+
if (!entry.isFile()) continue;
|
|
803
|
+
if (!entry.name.toLowerCase().endsWith(".yml")) continue;
|
|
804
|
+
fs.rmSync(path.join(dirPath, entry.name), { force: true });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function stableItemId(item, keys, fallback) {
|
|
809
|
+
for (const key of keys) {
|
|
810
|
+
const value = String(item?.[key] || "").trim();
|
|
811
|
+
if (value) return value;
|
|
812
|
+
}
|
|
813
|
+
return fallback;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function stringifyYaml(value) {
|
|
817
|
+
// eslint-disable-next-line global-require
|
|
818
|
+
const YAML = require("yaml");
|
|
819
|
+
return YAML.stringify(value, { lineWidth: 0 });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function writeYamlFile(filePath, value) {
|
|
823
|
+
ensureDir(path.dirname(filePath));
|
|
824
|
+
fs.writeFileSync(filePath, stringifyYaml(value), "utf8");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function writeJsonFile(filePath, value) {
|
|
828
|
+
ensureDir(path.dirname(filePath));
|
|
829
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function readJsonFile(filePath) {
|
|
833
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
|
|
834
|
+
try {
|
|
835
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
836
|
+
} catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const BOOTSTRAP_FORBIDDEN_KEYS = new Set([
|
|
842
|
+
"_id",
|
|
843
|
+
"org_id",
|
|
844
|
+
"project_id",
|
|
845
|
+
"created_by",
|
|
846
|
+
"assigned_to",
|
|
847
|
+
"user_id",
|
|
848
|
+
"qa_qc_results",
|
|
849
|
+
"job_id",
|
|
850
|
+
"job_ids",
|
|
851
|
+
"celery_task_id",
|
|
852
|
+
"conversation_id",
|
|
853
|
+
"error",
|
|
854
|
+
]);
|
|
855
|
+
|
|
856
|
+
function scrubBootstrapValue(value) {
|
|
857
|
+
if (Array.isArray(value)) {
|
|
858
|
+
return value.map((item) => scrubBootstrapValue(item));
|
|
859
|
+
}
|
|
860
|
+
if (!value || typeof value !== "object") {
|
|
861
|
+
return value;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const cleaned = {};
|
|
865
|
+
for (const [key, child] of Object.entries(value)) {
|
|
866
|
+
if (BOOTSTRAP_FORBIDDEN_KEYS.has(key)) continue;
|
|
867
|
+
cleaned[key] = scrubBootstrapValue(child);
|
|
868
|
+
}
|
|
869
|
+
return cleaned;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function resolveBootstrapWorkspace(repoNames) {
|
|
873
|
+
const resolved = resolveConfiguredRepos(repoNames);
|
|
874
|
+
if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
|
|
875
|
+
const names = Array.isArray(repoNames) ? repoNames.join(", ") : "";
|
|
876
|
+
throw new Error(
|
|
877
|
+
`No configured project repos were found from the current workspace. Expected child folders matching: ${names || "(none)"}`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return resolved;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
884
|
+
const targetRoot = outputDir
|
|
885
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
886
|
+
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
887
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
888
|
+
const phasesDir = path.join(dataRoot, "phases");
|
|
889
|
+
const epicsDir = path.join(dataRoot, "epics");
|
|
890
|
+
const storiesDir = path.join(dataRoot, "stories");
|
|
891
|
+
const missionsDir = path.join(dataRoot, "missions");
|
|
892
|
+
|
|
893
|
+
ensureDir(dataRoot);
|
|
894
|
+
clearYamlDirectory(phasesDir);
|
|
895
|
+
clearYamlDirectory(epicsDir);
|
|
896
|
+
clearYamlDirectory(storiesDir);
|
|
897
|
+
clearYamlDirectory(missionsDir);
|
|
898
|
+
|
|
899
|
+
const phases = Array.isArray(snapshot.phases) ? snapshot.phases.map((item) => scrubBootstrapValue(item)) : [];
|
|
900
|
+
const epics = Array.isArray(snapshot.epics) ? snapshot.epics.map((item) => scrubBootstrapValue(item)) : [];
|
|
901
|
+
const stories = Array.isArray(snapshot.stories) ? snapshot.stories.map((item) => scrubBootstrapValue(item)) : [];
|
|
902
|
+
const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
|
|
903
|
+
|
|
904
|
+
phases.forEach((phase, index) => {
|
|
905
|
+
const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
|
|
906
|
+
writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
|
|
907
|
+
});
|
|
908
|
+
epics.forEach((epic, index) => {
|
|
909
|
+
const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
|
|
910
|
+
writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
|
|
911
|
+
});
|
|
912
|
+
stories.forEach((story, index) => {
|
|
913
|
+
const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
|
|
914
|
+
writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
|
|
915
|
+
});
|
|
916
|
+
missions.forEach((mission, index) => {
|
|
917
|
+
const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
|
|
918
|
+
writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
922
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const manifest = {
|
|
926
|
+
schema_version: snapshot.schema_version || 1,
|
|
927
|
+
generated_at: snapshot.generated_at || null,
|
|
928
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
929
|
+
project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
|
|
930
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
931
|
+
counts: {
|
|
932
|
+
phases: phases.length,
|
|
933
|
+
epics: epics.length,
|
|
934
|
+
stories: stories.length,
|
|
935
|
+
missions: missions.length,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
writeJsonFile(path.join(dataRoot, "bootstrap-manifest.json"), manifest);
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
targetRoot,
|
|
942
|
+
dataRoot,
|
|
943
|
+
manifest,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
|
|
948
|
+
const targetRoot = outputDir
|
|
949
|
+
? path.resolve(process.cwd(), String(outputDir))
|
|
950
|
+
: path.join(wrapperRoot, "MyteCommandCenter");
|
|
951
|
+
const dataRoot = path.join(targetRoot, "data");
|
|
952
|
+
const missionsDir = path.join(dataRoot, "missions");
|
|
953
|
+
const qaqcRoot = path.join(dataRoot, "qaqc");
|
|
954
|
+
const activeMissionsDir = path.join(qaqcRoot, "active-missions");
|
|
955
|
+
const manifestPath = path.join(qaqcRoot, "manifest.json");
|
|
956
|
+
|
|
957
|
+
ensureDir(dataRoot);
|
|
958
|
+
ensureDir(missionsDir);
|
|
959
|
+
ensureDir(qaqcRoot);
|
|
960
|
+
const previousManifest = readJsonFile(manifestPath);
|
|
961
|
+
const previousMissionIds = Array.isArray(previousManifest?.active_mission_ids)
|
|
962
|
+
? previousManifest.active_mission_ids.map((item) => String(item).trim()).filter(Boolean)
|
|
963
|
+
: [];
|
|
964
|
+
clearYamlDirectory(activeMissionsDir);
|
|
965
|
+
|
|
966
|
+
const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
|
|
967
|
+
const currentMissionIds = [];
|
|
968
|
+
|
|
969
|
+
if (snapshot.project && typeof snapshot.project === "object") {
|
|
970
|
+
writeYamlFile(path.join(dataRoot, "project.yml"), scrubBootstrapValue(snapshot.project));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
missions.forEach((mission, index) => {
|
|
974
|
+
const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
|
|
975
|
+
currentMissionIds.push(missionId);
|
|
976
|
+
writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
|
|
977
|
+
writeYamlFile(path.join(activeMissionsDir, `${missionId}.yml`), mission);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
const currentMissionIdSet = new Set(currentMissionIds);
|
|
981
|
+
previousMissionIds.forEach((missionId) => {
|
|
982
|
+
if (!missionId || currentMissionIdSet.has(missionId)) return;
|
|
983
|
+
fs.rmSync(path.join(missionsDir, `${missionId}.yml`), { force: true });
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const manifest = {
|
|
987
|
+
schema_version: snapshot.schema_version || 1,
|
|
988
|
+
generated_at: snapshot.generated_at || null,
|
|
989
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
990
|
+
project: snapshot.project ? scrubBootstrapValue(snapshot.project) : null,
|
|
991
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
992
|
+
active_mission_ids: currentMissionIds,
|
|
993
|
+
counts: snapshot.counts && typeof snapshot.counts === "object" ? scrubBootstrapValue(snapshot.counts) : {
|
|
994
|
+
active_missions: missions.length,
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
writeJsonFile(manifestPath, manifest);
|
|
998
|
+
writeJsonFile(path.join(qaqcRoot, "latest-batch.json"), snapshot.latest_batch ? scrubBootstrapValue(snapshot.latest_batch) : null);
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
targetRoot,
|
|
1002
|
+
dataRoot,
|
|
1003
|
+
manifest,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
724
1007
|
async function runCreatePrd(args) {
|
|
725
1008
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
726
1009
|
if (!key) {
|
|
@@ -766,6 +1049,7 @@ async function runCreatePrd(args) {
|
|
|
766
1049
|
}
|
|
767
1050
|
|
|
768
1051
|
const inferredTitle = String(args.title || extractMarkdownTitle(trimmedSource) || (!useStdin && filePath ? path.parse(filePath).name : "")).trim();
|
|
1052
|
+
const description = String(args.description || args["feedback-text"] || args.feedbackText || "").trim();
|
|
769
1053
|
const payload = isMyteKanbanTicket(trimmedSource)
|
|
770
1054
|
? {
|
|
771
1055
|
ticket_markdown: trimmedSource,
|
|
@@ -775,6 +1059,10 @@ async function runCreatePrd(args) {
|
|
|
775
1059
|
title: inferredTitle,
|
|
776
1060
|
};
|
|
777
1061
|
|
|
1062
|
+
if (!payload.ticket_markdown && description) {
|
|
1063
|
+
payload.description = description;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
778
1066
|
if (!payload.ticket_markdown && !payload.title) {
|
|
779
1067
|
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
1068
|
process.exit(1);
|
|
@@ -874,6 +1162,191 @@ async function runConfig(args) {
|
|
|
874
1162
|
}
|
|
875
1163
|
}
|
|
876
1164
|
|
|
1165
|
+
async function runBootstrap(args) {
|
|
1166
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
1167
|
+
if (!key) {
|
|
1168
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
1173
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
1174
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
1175
|
+
|
|
1176
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
1177
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
1178
|
+
|
|
1179
|
+
let snapshot;
|
|
1180
|
+
try {
|
|
1181
|
+
snapshot = await fetchBootstrapSnapshot({ apiBase, key, timeoutMs });
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
console.error("Failed to fetch bootstrap snapshot:", err?.message || err);
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (args["print-context"] || args.printContext) {
|
|
1188
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
let resolved;
|
|
1193
|
+
try {
|
|
1194
|
+
resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
console.error(err?.message || err);
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const wrapperRoot = resolved.root;
|
|
1201
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
1202
|
+
const dryRun = Boolean(args["dry-run"] || args.dryRun);
|
|
1203
|
+
const summary = {
|
|
1204
|
+
api_base: apiBase,
|
|
1205
|
+
project_id: snapshot?.project?.id || null,
|
|
1206
|
+
wrapper_root: wrapperRoot,
|
|
1207
|
+
output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
|
|
1208
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
1209
|
+
local: {
|
|
1210
|
+
mode: resolved.mode,
|
|
1211
|
+
found: (resolved.repos || []).map((repo) => repo.name),
|
|
1212
|
+
missing: resolved.missing || [],
|
|
1213
|
+
},
|
|
1214
|
+
counts: {
|
|
1215
|
+
phases: Array.isArray(snapshot.phases) ? snapshot.phases.length : 0,
|
|
1216
|
+
epics: Array.isArray(snapshot.epics) ? snapshot.epics.length : 0,
|
|
1217
|
+
stories: Array.isArray(snapshot.stories) ? snapshot.stories.length : 0,
|
|
1218
|
+
missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
|
|
1219
|
+
},
|
|
1220
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
1221
|
+
generated_at: snapshot.generated_at || null,
|
|
1222
|
+
dry_run: dryRun,
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
if (dryRun) {
|
|
1226
|
+
if (args.json) {
|
|
1227
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1230
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1231
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1232
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1233
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1234
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1235
|
+
console.log(`Counts: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
1236
|
+
console.log("Dry run only - no files written.");
|
|
1237
|
+
}
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
1242
|
+
summary.data_root = writeResult.dataRoot;
|
|
1243
|
+
|
|
1244
|
+
if (args.json) {
|
|
1245
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1250
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1251
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1252
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1253
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1254
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1255
|
+
console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
|
|
1256
|
+
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function runSyncQaqc(args) {
|
|
1260
|
+
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
1261
|
+
if (!key) {
|
|
1262
|
+
console.error("Missing MYTE_API_KEY (project key) in environment/.env");
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
|
|
1267
|
+
const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
|
|
1268
|
+
const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
|
|
1269
|
+
|
|
1270
|
+
const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
|
|
1271
|
+
const apiBase = normalizeApiBase(baseRaw);
|
|
1272
|
+
|
|
1273
|
+
let snapshot;
|
|
1274
|
+
try {
|
|
1275
|
+
snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
console.error("Failed to fetch QAQC sync snapshot:", err?.message || err);
|
|
1278
|
+
process.exit(1);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (args["print-context"] || args.printContext) {
|
|
1282
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
let resolved;
|
|
1287
|
+
try {
|
|
1288
|
+
resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
console.error(err?.message || err);
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const wrapperRoot = resolved.root;
|
|
1295
|
+
const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
|
|
1296
|
+
const dryRun = Boolean(args["dry-run"] || args.dryRun);
|
|
1297
|
+
const summary = {
|
|
1298
|
+
api_base: apiBase,
|
|
1299
|
+
project_id: snapshot?.project?.id || null,
|
|
1300
|
+
wrapper_root: wrapperRoot,
|
|
1301
|
+
output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
|
|
1302
|
+
repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
|
|
1303
|
+
local: {
|
|
1304
|
+
mode: resolved.mode,
|
|
1305
|
+
found: (resolved.repos || []).map((repo) => repo.name),
|
|
1306
|
+
missing: resolved.missing || [],
|
|
1307
|
+
},
|
|
1308
|
+
counts: snapshot.counts && typeof snapshot.counts === "object" ? snapshot.counts : {
|
|
1309
|
+
active_missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
|
|
1310
|
+
},
|
|
1311
|
+
snapshot_hash: snapshot.snapshot_hash || null,
|
|
1312
|
+
generated_at: snapshot.generated_at || null,
|
|
1313
|
+
dry_run: dryRun,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
if (dryRun) {
|
|
1317
|
+
if (args.json) {
|
|
1318
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1319
|
+
} else {
|
|
1320
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1321
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1322
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1323
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1324
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1325
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1326
|
+
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}`);
|
|
1327
|
+
console.log("Dry run only - no files written.");
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const writeResult = writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir });
|
|
1333
|
+
summary.data_root = writeResult.dataRoot;
|
|
1334
|
+
|
|
1335
|
+
if (args.json) {
|
|
1336
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
console.log(`Project: ${summary.project_id || "(unknown)"}`);
|
|
1341
|
+
console.log(`Wrapper root: ${summary.wrapper_root}`);
|
|
1342
|
+
console.log(`Output root: ${summary.output_root}`);
|
|
1343
|
+
console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
|
|
1344
|
+
console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
|
|
1345
|
+
if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
|
|
1346
|
+
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}`);
|
|
1347
|
+
console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
877
1350
|
async function runQuery(args) {
|
|
878
1351
|
const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
|
|
879
1352
|
if (!key) {
|
|
@@ -1007,6 +1480,16 @@ async function main() {
|
|
|
1007
1480
|
return;
|
|
1008
1481
|
}
|
|
1009
1482
|
|
|
1483
|
+
if (command === "bootstrap") {
|
|
1484
|
+
await runBootstrap(args);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (command === "sync-qaqc" || command === "qaqc-sync") {
|
|
1489
|
+
await runSyncQaqc(args);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1010
1493
|
if (command === "chat") {
|
|
1011
1494
|
await runChat(args);
|
|
1012
1495
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mytegroupinc/myte-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "cli.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"dotenv": "^16.5.0",
|
|
24
24
|
"minimist": "^1.2.8",
|
|
25
|
-
"node-fetch": "^3.3.2"
|
|
25
|
+
"node-fetch": "^3.3.2",
|
|
26
|
+
"yaml": "^2.8.1"
|
|
26
27
|
}
|
|
27
28
|
}
|