@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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/cli.js +489 -6
  3. 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> Required only when stdin/raw markdown has no H1 title",
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 create-prd ./drafts/auth-prd.md",
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.4",
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
  }