qfai 0.6.3 → 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.
@@ -889,8 +889,8 @@ var import_promises6 = require("fs/promises");
889
889
  var import_node_path6 = __toESM(require("path"), 1);
890
890
  var import_node_url = require("url");
891
891
  async function resolveToolVersion() {
892
- if ("0.6.3".length > 0) {
893
- return "0.6.3";
892
+ if ("0.7.0".length > 0) {
893
+ return "0.7.0";
894
894
  }
895
895
  try {
896
896
  const packagePath = resolvePackageJsonPath();
@@ -3664,9 +3664,235 @@ async function writeValidationResult(root, outputPath, result) {
3664
3664
  `, "utf-8");
3665
3665
  }
3666
3666
 
3667
- // src/cli/commands/validate.ts
3667
+ // src/core/sync.ts
3668
3668
  var import_promises19 = require("fs/promises");
3669
3669
  var import_node_path19 = __toESM(require("path"), 1);
3670
+ var import_node_crypto2 = require("crypto");
3671
+ var import_node_url3 = require("url");
3672
+ async function exists6(target) {
3673
+ try {
3674
+ await (0, import_promises19.access)(target);
3675
+ return true;
3676
+ } catch {
3677
+ return false;
3678
+ }
3679
+ }
3680
+ async function computeFileHash(filePath) {
3681
+ const content = await (0, import_promises19.readFile)(filePath);
3682
+ return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex");
3683
+ }
3684
+ async function collectFilesRecursive(dir, base) {
3685
+ const result = [];
3686
+ if (!await exists6(dir)) {
3687
+ return result;
3688
+ }
3689
+ const entries = await (0, import_promises19.readdir)(dir, { withFileTypes: true });
3690
+ for (const entry of entries) {
3691
+ const fullPath = import_node_path19.default.join(dir, entry.name);
3692
+ if (entry.isDirectory()) {
3693
+ const nested = await collectFilesRecursive(fullPath, base);
3694
+ result.push(...nested);
3695
+ } else if (entry.isFile()) {
3696
+ const relative = import_node_path19.default.relative(base, fullPath);
3697
+ result.push(relative);
3698
+ }
3699
+ }
3700
+ return result;
3701
+ }
3702
+ function resolveAssetsPath() {
3703
+ const base = __filename;
3704
+ const basePath = base.startsWith("file:") ? (0, import_node_url3.fileURLToPath)(base) : base;
3705
+ return import_node_path19.default.resolve(
3706
+ import_node_path19.default.dirname(basePath),
3707
+ "../../assets/init/.qfai/promptpack"
3708
+ );
3709
+ }
3710
+ async function copyDirRecursive(srcDir, destDir) {
3711
+ await (0, import_promises19.mkdir)(destDir, { recursive: true });
3712
+ const entries = await (0, import_promises19.readdir)(srcDir, { withFileTypes: true });
3713
+ for (const entry of entries) {
3714
+ const srcPath = import_node_path19.default.join(srcDir, entry.name);
3715
+ const destPath = import_node_path19.default.join(destDir, entry.name);
3716
+ if (entry.isDirectory()) {
3717
+ await copyDirRecursive(srcPath, destPath);
3718
+ } else if (entry.isFile()) {
3719
+ await (0, import_promises19.copyFile)(srcPath, destPath);
3720
+ }
3721
+ }
3722
+ }
3723
+ async function createSyncData(options) {
3724
+ const root = import_node_path19.default.resolve(options.root);
3725
+ const version = await resolveToolVersion();
3726
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3727
+ const scope = "promptpack";
3728
+ const assetsPromptPackPath = resolveAssetsPath();
3729
+ const projectPromptPackPath = import_node_path19.default.join(root, ".qfai", "promptpack");
3730
+ const diffs = [];
3731
+ const assetsFiles = await collectFilesRecursive(
3732
+ assetsPromptPackPath,
3733
+ assetsPromptPackPath
3734
+ );
3735
+ const projectFiles = await collectFilesRecursive(
3736
+ projectPromptPackPath,
3737
+ projectPromptPackPath
3738
+ );
3739
+ const assetsSet = new Set(assetsFiles);
3740
+ const projectSet = new Set(projectFiles);
3741
+ for (const relativePath of assetsFiles) {
3742
+ const assetsFilePath = import_node_path19.default.join(assetsPromptPackPath, relativePath);
3743
+ const projectFilePath = import_node_path19.default.join(projectPromptPackPath, relativePath);
3744
+ if (!projectSet.has(relativePath)) {
3745
+ diffs.push({
3746
+ filePath: relativePath,
3747
+ status: "added",
3748
+ reason: "File exists in assets but not in project"
3749
+ });
3750
+ } else {
3751
+ const assetsHash = await computeFileHash(assetsFilePath);
3752
+ const projectHash = await computeFileHash(projectFilePath);
3753
+ if (assetsHash !== projectHash) {
3754
+ diffs.push({
3755
+ filePath: relativePath,
3756
+ status: "changed",
3757
+ reason: "Content differs between assets and project"
3758
+ });
3759
+ } else {
3760
+ diffs.push({
3761
+ filePath: relativePath,
3762
+ status: "unchanged"
3763
+ });
3764
+ }
3765
+ }
3766
+ }
3767
+ for (const relativePath of projectFiles) {
3768
+ if (!assetsSet.has(relativePath)) {
3769
+ diffs.push({
3770
+ filePath: relativePath,
3771
+ status: "removed",
3772
+ reason: "File exists in project but not in assets (local extension)"
3773
+ });
3774
+ }
3775
+ }
3776
+ diffs.sort((a, b) => a.filePath.localeCompare(b.filePath));
3777
+ const summary = {
3778
+ added: diffs.filter((d) => d.status === "added").length,
3779
+ removed: diffs.filter((d) => d.status === "removed").length,
3780
+ changed: diffs.filter((d) => d.status === "changed").length,
3781
+ unchanged: diffs.filter((d) => d.status === "unchanged").length
3782
+ };
3783
+ let exportPath;
3784
+ if (options.mode === "export") {
3785
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3786
+ const baseTimestamp = `${timestamp}-${Date.now()}`;
3787
+ const defaultOutDir = import_node_path19.default.join(root, ".qfai", ".sync");
3788
+ const outBase = options.outPath ? import_node_path19.default.isAbsolute(options.outPath) ? options.outPath : import_node_path19.default.resolve(root, options.outPath) : defaultOutDir;
3789
+ let exportDir;
3790
+ for (let attempt = 0; attempt < 100; attempt += 1) {
3791
+ const uniqueTimestamp = attempt === 0 ? baseTimestamp : `${baseTimestamp}-${attempt}`;
3792
+ const exportParent = import_node_path19.default.join(outBase, uniqueTimestamp);
3793
+ const candidate = import_node_path19.default.join(exportParent, "promptpack");
3794
+ await (0, import_promises19.mkdir)(exportParent, { recursive: true });
3795
+ try {
3796
+ await (0, import_promises19.mkdir)(candidate);
3797
+ exportDir = candidate;
3798
+ break;
3799
+ } catch (err) {
3800
+ const code = err?.code;
3801
+ if (code === "EEXIST") {
3802
+ continue;
3803
+ }
3804
+ throw err;
3805
+ }
3806
+ }
3807
+ if (!exportDir) {
3808
+ throw new Error("Failed to allocate unique export directory");
3809
+ }
3810
+ await copyDirRecursive(assetsPromptPackPath, exportDir);
3811
+ exportPath = toRelativePath(root, exportDir);
3812
+ }
3813
+ return {
3814
+ tool: "qfai",
3815
+ version,
3816
+ generatedAt,
3817
+ root: toRelativePath(process.cwd(), root),
3818
+ mode: options.mode,
3819
+ scope,
3820
+ summary,
3821
+ diffs,
3822
+ ...exportPath ? { exportPath } : {}
3823
+ };
3824
+ }
3825
+ function computeExitCode(data) {
3826
+ const hasDiff = data.summary.added > 0 || data.summary.removed > 0 || data.summary.changed > 0;
3827
+ return hasDiff ? 1 : 0;
3828
+ }
3829
+
3830
+ // src/cli/commands/sync.ts
3831
+ var import_node_path20 = __toESM(require("path"), 1);
3832
+ function formatSyncText(data) {
3833
+ const lines = [];
3834
+ lines.push(
3835
+ `qfai sync: root=${data.root} mode=${data.mode} scope=${data.scope}`
3836
+ );
3837
+ lines.push("");
3838
+ const diffs = data.diffs.filter((d) => d.status !== "unchanged");
3839
+ if (diffs.length === 0) {
3840
+ lines.push(
3841
+ "No differences found. Project promptpack is in sync with assets."
3842
+ );
3843
+ } else {
3844
+ lines.push("Differences:");
3845
+ for (const diff of diffs) {
3846
+ const statusMark = diff.status === "added" ? "[+]" : diff.status === "removed" ? "[-]" : "[~]";
3847
+ lines.push(` ${statusMark} ${diff.filePath}`);
3848
+ }
3849
+ }
3850
+ lines.push("");
3851
+ lines.push(
3852
+ `summary: added=${data.summary.added} removed=${data.summary.removed} changed=${data.summary.changed} unchanged=${data.summary.unchanged}`
3853
+ );
3854
+ if (data.exportPath) {
3855
+ lines.push("");
3856
+ lines.push(`exported to: ${data.exportPath}`);
3857
+ lines.push("");
3858
+ lines.push("Next steps:");
3859
+ const absRoot = import_node_path20.default.resolve(process.cwd(), data.root);
3860
+ const absExportPath = import_node_path20.default.resolve(absRoot, data.exportPath);
3861
+ lines.push(
3862
+ ` git diff --no-index ${import_node_path20.default.join(absRoot, ".qfai", "promptpack")} ${absExportPath}`
3863
+ );
3864
+ } else if (data.summary.added + data.summary.changed > 0) {
3865
+ lines.push("");
3866
+ lines.push("To export sync candidates:");
3867
+ lines.push(" qfai sync --mode export");
3868
+ }
3869
+ return lines.join("\n");
3870
+ }
3871
+ function formatSyncJson(data) {
3872
+ return JSON.stringify(data, null, 2);
3873
+ }
3874
+ async function runSync(options) {
3875
+ try {
3876
+ const data = await createSyncData({
3877
+ root: options.root,
3878
+ mode: options.mode,
3879
+ ...options.outPath !== void 0 ? { outPath: options.outPath } : {}
3880
+ });
3881
+ const output = options.format === "json" ? formatSyncJson(data) : formatSyncText(data);
3882
+ info(output);
3883
+ if (options.mode === "export") {
3884
+ return 0;
3885
+ }
3886
+ return computeExitCode(data);
3887
+ } catch (err) {
3888
+ error(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
3889
+ return 2;
3890
+ }
3891
+ }
3892
+
3893
+ // src/cli/commands/validate.ts
3894
+ var import_promises20 = require("fs/promises");
3895
+ var import_node_path21 = __toESM(require("path"), 1);
3670
3896
 
3671
3897
  // src/cli/lib/failOn.ts
3672
3898
  function shouldFail(result, failOn) {
@@ -3681,7 +3907,7 @@ function shouldFail(result, failOn) {
3681
3907
 
3682
3908
  // src/cli/commands/validate.ts
3683
3909
  async function runValidate(options) {
3684
- const root = import_node_path19.default.resolve(options.root);
3910
+ const root = import_node_path21.default.resolve(options.root);
3685
3911
  const configResult = await loadConfig(root);
3686
3912
  const result = await validateProject(root, configResult);
3687
3913
  const normalized = normalizeValidationResult(root, result);
@@ -3798,12 +4024,12 @@ function issueKey(issue7) {
3798
4024
  }
3799
4025
  async function emitJson(result, root, jsonPath) {
3800
4026
  const abs = resolveJsonPath(root, jsonPath);
3801
- await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
3802
- await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4027
+ await (0, import_promises20.mkdir)(import_node_path21.default.dirname(abs), { recursive: true });
4028
+ await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3803
4029
  `, "utf-8");
3804
4030
  }
3805
4031
  function resolveJsonPath(root, jsonPath) {
3806
- return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
4032
+ return import_node_path21.default.isAbsolute(jsonPath) ? jsonPath : import_node_path21.default.resolve(root, jsonPath);
3807
4033
  }
3808
4034
  var GITHUB_ANNOTATION_LIMIT = 100;
3809
4035
 
@@ -3820,6 +4046,8 @@ function parseArgs(argv, cwd) {
3820
4046
  reportRunValidate: false,
3821
4047
  doctorFormat: "text",
3822
4048
  validateFormat: "text",
4049
+ syncFormat: "text",
4050
+ syncMode: "check",
3823
4051
  strict: false,
3824
4052
  help: false
3825
4053
  };
@@ -3873,6 +4101,8 @@ function parseArgs(argv, cwd) {
3873
4101
  if (next) {
3874
4102
  if (command === "doctor") {
3875
4103
  options.doctorOut = next;
4104
+ } else if (command === "sync") {
4105
+ options.syncOut = next;
3876
4106
  } else {
3877
4107
  options.reportOut = next;
3878
4108
  }
@@ -3880,6 +4110,28 @@ function parseArgs(argv, cwd) {
3880
4110
  }
3881
4111
  i += 1;
3882
4112
  break;
4113
+ case "--mode":
4114
+ {
4115
+ const next = args[i + 1];
4116
+ if (!next) {
4117
+ throw new Error(
4118
+ '--mode option requires a value of "check" or "export"'
4119
+ );
4120
+ }
4121
+ if (command !== "sync") {
4122
+ throw new Error(
4123
+ '--mode option is only supported for the "sync" command'
4124
+ );
4125
+ }
4126
+ if (next !== "check" && next !== "export") {
4127
+ throw new Error(
4128
+ `Invalid value for --mode: "${next}". Expected "check" or "export".`
4129
+ );
4130
+ }
4131
+ options.syncMode = next;
4132
+ }
4133
+ i += 1;
4134
+ break;
3883
4135
  case "--in":
3884
4136
  {
3885
4137
  const next = args[i + 1];
@@ -3924,6 +4176,12 @@ function applyFormatOption(command, value, options) {
3924
4176
  }
3925
4177
  return;
3926
4178
  }
4179
+ if (command === "sync") {
4180
+ if (value === "text" || value === "json") {
4181
+ options.syncFormat = value;
4182
+ }
4183
+ return;
4184
+ }
3927
4185
  if (value === "md" || value === "json") {
3928
4186
  options.reportFormat = value;
3929
4187
  }
@@ -3983,6 +4241,18 @@ async function run(argv, cwd) {
3983
4241
  process.exitCode = exitCode;
3984
4242
  }
3985
4243
  return;
4244
+ case "sync":
4245
+ {
4246
+ const resolvedRoot = await resolveRoot(options);
4247
+ const exitCode = await runSync({
4248
+ root: resolvedRoot,
4249
+ mode: options.syncMode,
4250
+ format: options.syncFormat,
4251
+ ...options.syncOut !== void 0 ? { outPath: options.syncOut } : {}
4252
+ });
4253
+ process.exitCode = exitCode;
4254
+ }
4255
+ return;
3986
4256
  default:
3987
4257
  error(`Unknown command: ${command}`);
3988
4258
  info(usage());
@@ -3997,6 +4267,7 @@ Commands:
3997
4267
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3998
4268
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3999
4269
  doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
4270
+ sync PromptPack \u306E\u5DEE\u5206\u691C\u77E5\u30FB\u540C\u671F\u5019\u88DC\u66F8\u304D\u51FA\u3057
4000
4271
 
4001
4272
  Options:
4002
4273
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -4006,11 +4277,13 @@ Options:
4006
4277
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
4007
4278
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
4008
4279
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
4009
- --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
4280
+ --format <text|json> doctor/sync \u306E\u51FA\u529B\u5F62\u5F0F
4010
4281
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
4011
4282
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
4012
4283
  --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
4284
+ --mode <check|export> sync: \u52D5\u4F5C\u30E2\u30FC\u30C9\uFF08default: check\uFF09
4013
4285
  --out <path> report/doctor: \u51FA\u529B\u5148
4286
+ --out <dir> sync: export \u306E\u51FA\u529B\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\uFF08\u76F8\u5BFE/\u7D76\u5BFE\u3001export \u306E\u307F\uFF09
4014
4287
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
4015
4288
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
4016
4289
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A