@metasession.co/devaudit-cli 0.3.10 → 0.3.12

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/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command, Option } from 'commander';
3
3
  import { createConsola } from 'consola';
4
4
  import { execa } from 'execa';
5
5
  import { join, resolve, basename, dirname, relative } from 'path';
6
- import { promises } from 'fs';
6
+ import { promises, openAsBlob } from 'fs';
7
7
  import envPaths from 'env-paths';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
9
  import { validateManifest } from '@metasession.co/devaudit-plugin-sdk';
@@ -56,7 +56,7 @@ function emitJsonResult(payload) {
56
56
 
57
57
  // package.json
58
58
  var package_default = {
59
- version: "0.3.10"};
59
+ version: "0.3.12"};
60
60
 
61
61
  // src/lib/version.ts
62
62
  var CLI_VERSION = package_default.version;
@@ -602,6 +602,7 @@ var INITIAL_BACKOFF_MS = 1e3;
602
602
  var DEFAULT_PRESIGNED_THRESHOLD_BYTES = 26214400;
603
603
  var DEFAULT_PRESIGNED_MAX_ATTEMPTS = 3;
604
604
  var DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS = 300;
605
+ var STUB_PREFIX_BYTES = 4096;
605
606
  function maxAttempts() {
606
607
  const raw = Number.parseInt(process.env["UPLOAD_MAX_ATTEMPTS"] ?? "", 10);
607
608
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_ATTEMPTS;
@@ -629,20 +630,71 @@ async function collectFiles(filePath) {
629
630
  throw new Error(`${filePath} is neither a file nor a directory.`);
630
631
  }
631
632
  var STUB_BANNER = /STARTER TEMPLATE.+REPLACE BEFORE/;
632
- function isUneditedStub(buf) {
633
- return STUB_BANNER.test(buf.toString("utf-8"));
634
- }
633
+ var TEXT_STUB_EXTENSIONS = /* @__PURE__ */ new Set([
634
+ ".md",
635
+ ".markdown",
636
+ ".txt",
637
+ ".json",
638
+ ".yml",
639
+ ".yaml",
640
+ ".csv",
641
+ ".html",
642
+ ".xml",
643
+ ".log"
644
+ ]);
635
645
  function delay(ms) {
636
646
  return new Promise((res) => setTimeout(res, ms));
637
647
  }
638
- function buildUploadForm(file, buf, opts) {
648
+ function extensionOf(file) {
649
+ const dot = file.lastIndexOf(".");
650
+ return dot === -1 ? "" : file.slice(dot).toLowerCase();
651
+ }
652
+ function isTextLikeEvidenceFile(file) {
653
+ return TEXT_STUB_EXTENSIONS.has(extensionOf(file));
654
+ }
655
+ async function readFilePrefix(file, bytes) {
656
+ const handle = await promises.open(file, "r");
657
+ try {
658
+ const buf = Buffer.alloc(bytes);
659
+ const { bytesRead } = await handle.read(buf, 0, bytes, 0);
660
+ return buf.subarray(0, bytesRead).toString("utf-8");
661
+ } finally {
662
+ await handle.close();
663
+ }
664
+ }
665
+ async function isUneditedStub(file) {
666
+ if (!isTextLikeEvidenceFile(file)) return false;
667
+ const prefix = await readFilePrefix(file, STUB_PREFIX_BYTES);
668
+ return STUB_BANNER.test(prefix);
669
+ }
670
+ async function createUploadSource(file) {
671
+ const mimeType = deriveMimeType(file);
672
+ const { size } = await promises.stat(file);
673
+ try {
674
+ return {
675
+ blob: await openAsBlob(file, { type: mimeType }),
676
+ size,
677
+ mimeType
678
+ };
679
+ } catch {
680
+ return {
681
+ blob: new Blob([await promises.readFile(file)], { type: mimeType }),
682
+ size,
683
+ mimeType
684
+ };
685
+ }
686
+ }
687
+ function buildUploadForm(file, source, opts) {
688
+ const metadata = {
689
+ ...opts.metadata ?? {},
690
+ ...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
691
+ };
639
692
  const form = new FormData();
640
- const blob = new Blob([new Uint8Array(buf)]);
641
- form.set("file", blob, basename(file));
693
+ form.set("file", source.blob, basename(file));
642
694
  form.set("projectSlug", opts.projectSlug);
643
695
  form.set("requirementId", opts.requirementId);
644
696
  form.set("evidenceType", opts.evidenceType);
645
- form.set("metadata", JSON.stringify(opts.metadata ?? {}));
697
+ form.set("metadata", JSON.stringify(metadata));
646
698
  if (opts.releaseVersion) form.set("releaseVersion", opts.releaseVersion);
647
699
  if (opts.createReleaseIfMissing) form.set("createReleaseIfMissing", "true");
648
700
  if (opts.environment) form.set("environment", opts.environment);
@@ -654,6 +706,8 @@ function buildUploadForm(file, buf, opts) {
654
706
  if (opts.gateStatus) form.set("gateStatus", opts.gateStatus);
655
707
  if (opts.sdlcStage) form.set("sdlcStage", opts.sdlcStage);
656
708
  if (opts.testCycleId) form.set("testCycleId", opts.testCycleId);
709
+ if (opts.sentinelContent) form.set("sentinelContent", opts.sentinelContent);
710
+ if (opts.commitTimestamp) form.set("commitTimestamp", opts.commitTimestamp);
657
711
  return form;
658
712
  }
659
713
  function uploadFailureMessage(err, timeoutSeconds) {
@@ -684,7 +738,7 @@ async function probeBaseUrlDrift(baseUrl) {
684
738
  return null;
685
739
  }
686
740
  }
687
- async function uploadOne(file, buf, opts) {
741
+ async function uploadOne(file, source, opts) {
688
742
  const attempts = maxAttempts();
689
743
  const timeoutSeconds = uploadMaxTimeSeconds();
690
744
  const url = `${opts.baseUrl.replace(/\/$/, "")}/api/evidence/upload`;
@@ -698,7 +752,7 @@ async function uploadOne(file, buf, opts) {
698
752
  res = await fetch(url, {
699
753
  method: "POST",
700
754
  headers: { authorization: `Bearer ${opts.apiKey}` },
701
- body: buildUploadForm(file, buf, opts),
755
+ body: buildUploadForm(file, source, opts),
702
756
  signal: controller.signal
703
757
  });
704
758
  } catch (err) {
@@ -731,15 +785,17 @@ async function uploadOne(file, buf, opts) {
731
785
  return { file, ok: false, status: 0, error: "max retries exhausted" };
732
786
  }
733
787
  var PRESIGNED_FALLBACK = /* @__PURE__ */ Symbol("presigned-fallback");
734
- async function uploadPresigned(file, buf, opts) {
788
+ async function uploadPresigned(file, source, opts) {
735
789
  const baseUrl = opts.baseUrl.replace(/\/$/, "");
736
790
  const presignUrl = `${baseUrl}/api/evidence/upload-url`;
737
791
  const completeUrl = `${baseUrl}/api/evidence/upload-complete`;
738
792
  const maxAttemptsPresigned = DEFAULT_PRESIGNED_MAX_ATTEMPTS;
739
793
  const timeoutSeconds = uploadMaxTimeSeconds();
740
794
  const presignedTimeoutSeconds = DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS;
741
- const mimeType = deriveMimeType(file);
742
- const metadata = opts.metadata ?? {};
795
+ const metadata = {
796
+ ...opts.metadata ?? {},
797
+ ...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
798
+ };
743
799
  let uploadUrl = "";
744
800
  let evidenceId = "";
745
801
  let attempt = 1;
@@ -760,8 +816,8 @@ async function uploadPresigned(file, buf, opts) {
760
816
  requirementId: opts.requirementId,
761
817
  evidenceType: opts.evidenceType,
762
818
  fileName: basename(file),
763
- fileSizeBytes: buf.byteLength,
764
- mimeType,
819
+ fileSizeBytes: source.size,
820
+ mimeType: source.mimeType,
765
821
  metadata,
766
822
  ...opts.releaseVersion ? { releaseVersion: opts.releaseVersion } : {},
767
823
  ...opts.createReleaseIfMissing ? { createReleaseIfMissing: true } : {},
@@ -770,8 +826,11 @@ async function uploadPresigned(file, buf, opts) {
770
826
  ...opts.evidenceCategory ? { evidenceCategory: opts.evidenceCategory } : {},
771
827
  ...opts.releaseTitle ? { releaseTitle: opts.releaseTitle } : {},
772
828
  ...opts.releaseSummary ? { releaseSummary: opts.releaseSummary } : {},
829
+ ...opts.changeType ? { changeType: opts.changeType } : {},
773
830
  ...opts.sdlcStage ? { sdlcStage: opts.sdlcStage } : {},
774
- ...opts.testCycleId ? { testCycleId: opts.testCycleId } : {}
831
+ ...opts.testCycleId ? { testCycleId: opts.testCycleId } : {},
832
+ ...opts.sentinelContent ? { sentinelContent: opts.sentinelContent } : {},
833
+ ...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
775
834
  }),
776
835
  signal: controller.signal
777
836
  });
@@ -816,8 +875,8 @@ async function uploadPresigned(file, buf, opts) {
816
875
  try {
817
876
  res = await fetch(uploadUrl, {
818
877
  method: "PUT",
819
- headers: { "content-type": mimeType },
820
- body: new Uint8Array(buf),
878
+ headers: { "content-type": source.mimeType },
879
+ body: source.blob,
821
880
  signal: controller.signal
822
881
  });
823
882
  } catch (err) {
@@ -898,25 +957,26 @@ async function uploadEvidence(opts) {
898
957
  }
899
958
  const results = [];
900
959
  for (const file of files) {
901
- const buf = await promises.readFile(file);
902
- if (isUneditedStub(buf)) {
960
+ if (await isUneditedStub(file)) {
903
961
  results.push({ file, ok: true, status: 0, skipped: true });
904
962
  continue;
905
963
  }
906
- if (buf.byteLength >= DEFAULT_PRESIGNED_THRESHOLD_BYTES) {
907
- const presignedResult = await uploadPresigned(file, buf, opts);
964
+ const source = await createUploadSource(file);
965
+ if (source.size >= DEFAULT_PRESIGNED_THRESHOLD_BYTES) {
966
+ const presignedResult = await uploadPresigned(file, source, opts);
908
967
  if (presignedResult !== PRESIGNED_FALLBACK) {
909
968
  results.push(presignedResult);
910
969
  continue;
911
970
  }
912
971
  }
913
- results.push(await uploadOne(file, buf, opts));
972
+ results.push(await uploadOne(file, source, opts));
914
973
  }
915
974
  return results;
916
975
  }
917
976
 
918
977
  // src/commands/push.ts
919
978
  var DEFAULT_BASE_URL3 = "https://devaudit.metasession.co";
979
+ var TRACKED_CHANGE_TYPES = /* @__PURE__ */ new Set(["feat", "fix", "refactor", "perf", "compliance", "revert"]);
920
980
  function buildMetadata(options) {
921
981
  const metadata = {};
922
982
  if (options.gitSha) metadata["gitSha"] = options.gitSha;
@@ -928,6 +988,28 @@ function buildMetadata(options) {
928
988
  }
929
989
  return metadata;
930
990
  }
991
+ async function resolveSentinelContext(options) {
992
+ if (!options.changeType || !TRACKED_CHANGE_TYPES.has(options.changeType)) {
993
+ return {};
994
+ }
995
+ const sentinelPath = join(process.cwd(), ".sdlc-implementer-invoked");
996
+ let sentinelContent;
997
+ try {
998
+ const raw = await promises.readFile(sentinelPath, "utf8");
999
+ if (raw.trim()) sentinelContent = raw;
1000
+ } catch {
1001
+ sentinelContent = void 0;
1002
+ }
1003
+ let commitTimestamp;
1004
+ try {
1005
+ const target = options.gitSha?.trim() ? options.gitSha.trim() : "HEAD";
1006
+ const { stdout } = await execa("git", ["show", "-s", "--format=%cI", target], { reject: false });
1007
+ if (stdout.trim()) commitTimestamp = stdout.trim();
1008
+ } catch {
1009
+ commitTimestamp = void 0;
1010
+ }
1011
+ return { sentinelContent, commitTimestamp };
1012
+ }
931
1013
  function validateOptions(options) {
932
1014
  if (options.environment && !options.release) {
933
1015
  return "--environment requires --release (evidence without a release is orphaned)";
@@ -998,6 +1080,8 @@ async function runPush(options) {
998
1080
  await runHook(plugins, "beforePush", ctx);
999
1081
  }
1000
1082
  const metadata = buildMetadata(options);
1083
+ const { sentinelContent, commitTimestamp } = await resolveSentinelContext(options);
1084
+ if (commitTimestamp) metadata["commitTimestamp"] = commitTimestamp;
1001
1085
  const results = await uploadEvidence({
1002
1086
  projectSlug: options.projectSlug,
1003
1087
  requirementId: options.requirementId,
@@ -1016,6 +1100,8 @@ async function runPush(options) {
1016
1100
  ...options.gateStatus !== void 0 ? { gateStatus: options.gateStatus } : {},
1017
1101
  ...options.sdlcStage !== void 0 ? { sdlcStage: options.sdlcStage } : {},
1018
1102
  ...options.testCycleId !== void 0 ? { testCycleId: options.testCycleId } : {},
1103
+ ...sentinelContent !== void 0 ? { sentinelContent } : {},
1104
+ ...commitTimestamp !== void 0 ? { commitTimestamp } : {},
1019
1105
  metadata
1020
1106
  });
1021
1107
  let okCount = 0;