qfai 0.6.2 → 0.7.0

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.
@@ -870,8 +870,8 @@ import { readFile as readFile3 } from "fs/promises";
870
870
  import path6 from "path";
871
871
  import { fileURLToPath } from "url";
872
872
  async function resolveToolVersion() {
873
- if ("0.6.2".length > 0) {
874
- return "0.6.2";
873
+ if ("0.7.0".length > 0) {
874
+ return "0.7.0";
875
875
  }
876
876
  try {
877
877
  const packagePath = resolvePackageJsonPath();
@@ -3645,9 +3645,235 @@ async function writeValidationResult(root, outputPath, result) {
3645
3645
  `, "utf-8");
3646
3646
  }
3647
3647
 
3648
- // src/cli/commands/validate.ts
3649
- import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3648
+ // src/core/sync.ts
3649
+ import { access as access6, mkdir as mkdir4, readdir as readdir4, readFile as readFile13, copyFile as copyFile2 } from "fs/promises";
3650
3650
  import path19 from "path";
3651
+ import { createHash } from "crypto";
3652
+ import { fileURLToPath as fileURLToPath3 } from "url";
3653
+ async function exists6(target) {
3654
+ try {
3655
+ await access6(target);
3656
+ return true;
3657
+ } catch {
3658
+ return false;
3659
+ }
3660
+ }
3661
+ async function computeFileHash(filePath) {
3662
+ const content = await readFile13(filePath);
3663
+ return createHash("sha256").update(content).digest("hex");
3664
+ }
3665
+ async function collectFilesRecursive(dir, base) {
3666
+ const result = [];
3667
+ if (!await exists6(dir)) {
3668
+ return result;
3669
+ }
3670
+ const entries = await readdir4(dir, { withFileTypes: true });
3671
+ for (const entry of entries) {
3672
+ const fullPath = path19.join(dir, entry.name);
3673
+ if (entry.isDirectory()) {
3674
+ const nested = await collectFilesRecursive(fullPath, base);
3675
+ result.push(...nested);
3676
+ } else if (entry.isFile()) {
3677
+ const relative = path19.relative(base, fullPath);
3678
+ result.push(relative);
3679
+ }
3680
+ }
3681
+ return result;
3682
+ }
3683
+ function resolveAssetsPath() {
3684
+ const base = import.meta.url;
3685
+ const basePath = base.startsWith("file:") ? fileURLToPath3(base) : base;
3686
+ return path19.resolve(
3687
+ path19.dirname(basePath),
3688
+ "../../assets/init/.qfai/promptpack"
3689
+ );
3690
+ }
3691
+ async function copyDirRecursive(srcDir, destDir) {
3692
+ await mkdir4(destDir, { recursive: true });
3693
+ const entries = await readdir4(srcDir, { withFileTypes: true });
3694
+ for (const entry of entries) {
3695
+ const srcPath = path19.join(srcDir, entry.name);
3696
+ const destPath = path19.join(destDir, entry.name);
3697
+ if (entry.isDirectory()) {
3698
+ await copyDirRecursive(srcPath, destPath);
3699
+ } else if (entry.isFile()) {
3700
+ await copyFile2(srcPath, destPath);
3701
+ }
3702
+ }
3703
+ }
3704
+ async function createSyncData(options) {
3705
+ const root = path19.resolve(options.root);
3706
+ const version = await resolveToolVersion();
3707
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3708
+ const scope = "promptpack";
3709
+ const assetsPromptPackPath = resolveAssetsPath();
3710
+ const projectPromptPackPath = path19.join(root, ".qfai", "promptpack");
3711
+ const diffs = [];
3712
+ const assetsFiles = await collectFilesRecursive(
3713
+ assetsPromptPackPath,
3714
+ assetsPromptPackPath
3715
+ );
3716
+ const projectFiles = await collectFilesRecursive(
3717
+ projectPromptPackPath,
3718
+ projectPromptPackPath
3719
+ );
3720
+ const assetsSet = new Set(assetsFiles);
3721
+ const projectSet = new Set(projectFiles);
3722
+ for (const relativePath of assetsFiles) {
3723
+ const assetsFilePath = path19.join(assetsPromptPackPath, relativePath);
3724
+ const projectFilePath = path19.join(projectPromptPackPath, relativePath);
3725
+ if (!projectSet.has(relativePath)) {
3726
+ diffs.push({
3727
+ filePath: relativePath,
3728
+ status: "added",
3729
+ reason: "File exists in assets but not in project"
3730
+ });
3731
+ } else {
3732
+ const assetsHash = await computeFileHash(assetsFilePath);
3733
+ const projectHash = await computeFileHash(projectFilePath);
3734
+ if (assetsHash !== projectHash) {
3735
+ diffs.push({
3736
+ filePath: relativePath,
3737
+ status: "changed",
3738
+ reason: "Content differs between assets and project"
3739
+ });
3740
+ } else {
3741
+ diffs.push({
3742
+ filePath: relativePath,
3743
+ status: "unchanged"
3744
+ });
3745
+ }
3746
+ }
3747
+ }
3748
+ for (const relativePath of projectFiles) {
3749
+ if (!assetsSet.has(relativePath)) {
3750
+ diffs.push({
3751
+ filePath: relativePath,
3752
+ status: "removed",
3753
+ reason: "File exists in project but not in assets (local extension)"
3754
+ });
3755
+ }
3756
+ }
3757
+ diffs.sort((a, b) => a.filePath.localeCompare(b.filePath));
3758
+ const summary = {
3759
+ added: diffs.filter((d) => d.status === "added").length,
3760
+ removed: diffs.filter((d) => d.status === "removed").length,
3761
+ changed: diffs.filter((d) => d.status === "changed").length,
3762
+ unchanged: diffs.filter((d) => d.status === "unchanged").length
3763
+ };
3764
+ let exportPath;
3765
+ if (options.mode === "export") {
3766
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3767
+ const baseTimestamp = `${timestamp}-${Date.now()}`;
3768
+ const defaultOutDir = path19.join(root, ".qfai", ".sync");
3769
+ const outBase = options.outPath ? path19.isAbsolute(options.outPath) ? options.outPath : path19.resolve(root, options.outPath) : defaultOutDir;
3770
+ let exportDir;
3771
+ for (let attempt = 0; attempt < 100; attempt += 1) {
3772
+ const uniqueTimestamp = attempt === 0 ? baseTimestamp : `${baseTimestamp}-${attempt}`;
3773
+ const exportParent = path19.join(outBase, uniqueTimestamp);
3774
+ const candidate = path19.join(exportParent, "promptpack");
3775
+ await mkdir4(exportParent, { recursive: true });
3776
+ try {
3777
+ await mkdir4(candidate);
3778
+ exportDir = candidate;
3779
+ break;
3780
+ } catch (err) {
3781
+ const code = err?.code;
3782
+ if (code === "EEXIST") {
3783
+ continue;
3784
+ }
3785
+ throw err;
3786
+ }
3787
+ }
3788
+ if (!exportDir) {
3789
+ throw new Error("Failed to allocate unique export directory");
3790
+ }
3791
+ await copyDirRecursive(assetsPromptPackPath, exportDir);
3792
+ exportPath = toRelativePath(root, exportDir);
3793
+ }
3794
+ return {
3795
+ tool: "qfai",
3796
+ version,
3797
+ generatedAt,
3798
+ root: toRelativePath(process.cwd(), root),
3799
+ mode: options.mode,
3800
+ scope,
3801
+ summary,
3802
+ diffs,
3803
+ ...exportPath ? { exportPath } : {}
3804
+ };
3805
+ }
3806
+ function computeExitCode(data) {
3807
+ const hasDiff = data.summary.added > 0 || data.summary.removed > 0 || data.summary.changed > 0;
3808
+ return hasDiff ? 1 : 0;
3809
+ }
3810
+
3811
+ // src/cli/commands/sync.ts
3812
+ import path20 from "path";
3813
+ function formatSyncText(data) {
3814
+ const lines = [];
3815
+ lines.push(
3816
+ `qfai sync: root=${data.root} mode=${data.mode} scope=${data.scope}`
3817
+ );
3818
+ lines.push("");
3819
+ const diffs = data.diffs.filter((d) => d.status !== "unchanged");
3820
+ if (diffs.length === 0) {
3821
+ lines.push(
3822
+ "No differences found. Project promptpack is in sync with assets."
3823
+ );
3824
+ } else {
3825
+ lines.push("Differences:");
3826
+ for (const diff of diffs) {
3827
+ const statusMark = diff.status === "added" ? "[+]" : diff.status === "removed" ? "[-]" : "[~]";
3828
+ lines.push(` ${statusMark} ${diff.filePath}`);
3829
+ }
3830
+ }
3831
+ lines.push("");
3832
+ lines.push(
3833
+ `summary: added=${data.summary.added} removed=${data.summary.removed} changed=${data.summary.changed} unchanged=${data.summary.unchanged}`
3834
+ );
3835
+ if (data.exportPath) {
3836
+ lines.push("");
3837
+ lines.push(`exported to: ${data.exportPath}`);
3838
+ lines.push("");
3839
+ lines.push("Next steps:");
3840
+ const absRoot = path20.resolve(process.cwd(), data.root);
3841
+ const absExportPath = path20.resolve(absRoot, data.exportPath);
3842
+ lines.push(
3843
+ ` git diff --no-index ${path20.join(absRoot, ".qfai", "promptpack")} ${absExportPath}`
3844
+ );
3845
+ } else if (data.summary.added + data.summary.changed > 0) {
3846
+ lines.push("");
3847
+ lines.push("To export sync candidates:");
3848
+ lines.push(" qfai sync --mode export");
3849
+ }
3850
+ return lines.join("\n");
3851
+ }
3852
+ function formatSyncJson(data) {
3853
+ return JSON.stringify(data, null, 2);
3854
+ }
3855
+ async function runSync(options) {
3856
+ try {
3857
+ const data = await createSyncData({
3858
+ root: options.root,
3859
+ mode: options.mode,
3860
+ ...options.outPath !== void 0 ? { outPath: options.outPath } : {}
3861
+ });
3862
+ const output = options.format === "json" ? formatSyncJson(data) : formatSyncText(data);
3863
+ info(output);
3864
+ if (options.mode === "export") {
3865
+ return 0;
3866
+ }
3867
+ return computeExitCode(data);
3868
+ } catch (err) {
3869
+ error(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
3870
+ return 2;
3871
+ }
3872
+ }
3873
+
3874
+ // src/cli/commands/validate.ts
3875
+ import { mkdir as mkdir5, writeFile as writeFile3 } from "fs/promises";
3876
+ import path21 from "path";
3651
3877
 
3652
3878
  // src/cli/lib/failOn.ts
3653
3879
  function shouldFail(result, failOn) {
@@ -3662,7 +3888,7 @@ function shouldFail(result, failOn) {
3662
3888
 
3663
3889
  // src/cli/commands/validate.ts
3664
3890
  async function runValidate(options) {
3665
- const root = path19.resolve(options.root);
3891
+ const root = path21.resolve(options.root);
3666
3892
  const configResult = await loadConfig(root);
3667
3893
  const result = await validateProject(root, configResult);
3668
3894
  const normalized = normalizeValidationResult(root, result);
@@ -3779,12 +4005,12 @@ function issueKey(issue7) {
3779
4005
  }
3780
4006
  async function emitJson(result, root, jsonPath) {
3781
4007
  const abs = resolveJsonPath(root, jsonPath);
3782
- await mkdir4(path19.dirname(abs), { recursive: true });
4008
+ await mkdir5(path21.dirname(abs), { recursive: true });
3783
4009
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
3784
4010
  `, "utf-8");
3785
4011
  }
3786
4012
  function resolveJsonPath(root, jsonPath) {
3787
- return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
4013
+ return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
3788
4014
  }
3789
4015
  var GITHUB_ANNOTATION_LIMIT = 100;
3790
4016
 
@@ -3801,6 +4027,8 @@ function parseArgs(argv, cwd) {
3801
4027
  reportRunValidate: false,
3802
4028
  doctorFormat: "text",
3803
4029
  validateFormat: "text",
4030
+ syncFormat: "text",
4031
+ syncMode: "check",
3804
4032
  strict: false,
3805
4033
  help: false
3806
4034
  };
@@ -3854,6 +4082,8 @@ function parseArgs(argv, cwd) {
3854
4082
  if (next) {
3855
4083
  if (command === "doctor") {
3856
4084
  options.doctorOut = next;
4085
+ } else if (command === "sync") {
4086
+ options.syncOut = next;
3857
4087
  } else {
3858
4088
  options.reportOut = next;
3859
4089
  }
@@ -3861,6 +4091,28 @@ function parseArgs(argv, cwd) {
3861
4091
  }
3862
4092
  i += 1;
3863
4093
  break;
4094
+ case "--mode":
4095
+ {
4096
+ const next = args[i + 1];
4097
+ if (!next) {
4098
+ throw new Error(
4099
+ '--mode option requires a value of "check" or "export"'
4100
+ );
4101
+ }
4102
+ if (command !== "sync") {
4103
+ throw new Error(
4104
+ '--mode option is only supported for the "sync" command'
4105
+ );
4106
+ }
4107
+ if (next !== "check" && next !== "export") {
4108
+ throw new Error(
4109
+ `Invalid value for --mode: "${next}". Expected "check" or "export".`
4110
+ );
4111
+ }
4112
+ options.syncMode = next;
4113
+ }
4114
+ i += 1;
4115
+ break;
3864
4116
  case "--in":
3865
4117
  {
3866
4118
  const next = args[i + 1];
@@ -3905,6 +4157,12 @@ function applyFormatOption(command, value, options) {
3905
4157
  }
3906
4158
  return;
3907
4159
  }
4160
+ if (command === "sync") {
4161
+ if (value === "text" || value === "json") {
4162
+ options.syncFormat = value;
4163
+ }
4164
+ return;
4165
+ }
3908
4166
  if (value === "md" || value === "json") {
3909
4167
  options.reportFormat = value;
3910
4168
  }
@@ -3964,6 +4222,18 @@ async function run(argv, cwd) {
3964
4222
  process.exitCode = exitCode;
3965
4223
  }
3966
4224
  return;
4225
+ case "sync":
4226
+ {
4227
+ const resolvedRoot = await resolveRoot(options);
4228
+ const exitCode = await runSync({
4229
+ root: resolvedRoot,
4230
+ mode: options.syncMode,
4231
+ format: options.syncFormat,
4232
+ ...options.syncOut !== void 0 ? { outPath: options.syncOut } : {}
4233
+ });
4234
+ process.exitCode = exitCode;
4235
+ }
4236
+ return;
3967
4237
  default:
3968
4238
  error(`Unknown command: ${command}`);
3969
4239
  info(usage());
@@ -3978,6 +4248,7 @@ Commands:
3978
4248
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3979
4249
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3980
4250
  doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
4251
+ sync PromptPack \u306E\u5DEE\u5206\u691C\u77E5\u30FB\u540C\u671F\u5019\u88DC\u66F8\u304D\u51FA\u3057
3981
4252
 
3982
4253
  Options:
3983
4254
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3987,11 +4258,13 @@ Options:
3987
4258
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3988
4259
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3989
4260
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
3990
- --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
4261
+ --format <text|json> doctor/sync \u306E\u51FA\u529B\u5F62\u5F0F
3991
4262
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3992
4263
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3993
4264
  --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
4265
+ --mode <check|export> sync: \u52D5\u4F5C\u30E2\u30FC\u30C9\uFF08default: check\uFF09
3994
4266
  --out <path> report/doctor: \u51FA\u529B\u5148
4267
+ --out <dir> sync: export \u306E\u51FA\u529B\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\uFF08\u76F8\u5BFE/\u7D76\u5BFE\u3001export \u306E\u307F\uFF09
3995
4268
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3996
4269
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3997
4270
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A