qfai 0.7.0 → 0.7.2

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.7.0".length > 0) {
874
- return "0.7.0";
873
+ if ("0.7.2".length > 0) {
874
+ return "0.7.2";
875
875
  }
876
876
  try {
877
877
  const packagePath = resolvePackageJsonPath();
@@ -902,7 +902,7 @@ function addCheck(checks, check) {
902
902
  checks.push(check);
903
903
  }
904
904
  function summarize(checks) {
905
- const summary = { ok: 0, warning: 0, error: 0 };
905
+ const summary = { ok: 0, info: 0, warning: 0, error: 0 };
906
906
  for (const check of checks) {
907
907
  summary[check.severity] += 1;
908
908
  }
@@ -974,6 +974,20 @@ async function createDoctorData(options) {
974
974
  message: ok ? `${key} exists` : `${key} is missing (did you run 'qfai init'?)`,
975
975
  details: { path: toRelativePath(root, resolved) }
976
976
  });
977
+ if (key === "promptsDir") {
978
+ const promptsLocalDir = path7.join(
979
+ path7.dirname(resolved),
980
+ `${path7.basename(resolved)}.local`
981
+ );
982
+ const found = await exists4(promptsLocalDir);
983
+ addCheck(checks, {
984
+ id: "paths.promptsLocalDir",
985
+ severity: "info",
986
+ title: "Prompts overlay (prompts.local)",
987
+ message: found ? "prompts.local exists (overlay can be used)" : "prompts.local is optional (create it to override prompts)",
988
+ details: { path: toRelativePath(root, promptsLocalDir) }
989
+ });
990
+ }
977
991
  }
978
992
  const specsRoot = resolvePath(root, config, "specsDir");
979
993
  const entries = await collectSpecEntries(specsRoot);
@@ -1172,7 +1186,7 @@ function formatDoctorText(data) {
1172
1186
  lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
1173
1187
  }
1174
1188
  lines.push(
1175
- `summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
1189
+ `summary: ok=${data.summary.ok} info=${data.summary.info} warning=${data.summary.warning} error=${data.summary.error}`
1176
1190
  );
1177
1191
  return lines.join("\n");
1178
1192
  }
@@ -1221,9 +1235,22 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1221
1235
  const copied = [];
1222
1236
  const skipped = [];
1223
1237
  const conflicts = [];
1238
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path9.sep);
1239
+ const isProtectedRelative = (relative) => {
1240
+ if (protectPrefixes.length === 0) {
1241
+ return false;
1242
+ }
1243
+ const normalized = relative.replace(/[\\/]+/g, path9.sep);
1244
+ return protectPrefixes.some(
1245
+ (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1246
+ );
1247
+ };
1224
1248
  if (!options.force) {
1225
1249
  for (const file of files) {
1226
1250
  const relative = path9.relative(sourceRoot, file);
1251
+ if (isProtectedRelative(relative)) {
1252
+ continue;
1253
+ }
1227
1254
  const dest = path9.join(destRoot, relative);
1228
1255
  if (!await shouldWrite(dest, options.force)) {
1229
1256
  conflicts.push(dest);
@@ -1236,7 +1263,8 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1236
1263
  for (const file of files) {
1237
1264
  const relative = path9.relative(sourceRoot, file);
1238
1265
  const dest = path9.join(destRoot, relative);
1239
- if (!await shouldWrite(dest, options.force)) {
1266
+ const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1267
+ if (!await shouldWrite(dest, forceForThisFile)) {
1240
1268
  skipped.push(dest);
1241
1269
  continue;
1242
1270
  }
@@ -1331,7 +1359,8 @@ async function runInit(options) {
1331
1359
  });
1332
1360
  const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1333
1361
  force: options.force,
1334
- dryRun: options.dryRun
1362
+ dryRun: options.dryRun,
1363
+ protect: ["prompts.local"]
1335
1364
  });
1336
1365
  report(
1337
1366
  [...rootResult.copied, ...qfaiResult.copied],
@@ -3645,235 +3674,9 @@ async function writeValidationResult(root, outputPath, result) {
3645
3674
  `, "utf-8");
3646
3675
  }
3647
3676
 
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
- 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
3677
  // src/cli/commands/validate.ts
3875
- import { mkdir as mkdir5, writeFile as writeFile3 } from "fs/promises";
3876
- import path21 from "path";
3678
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3679
+ import path19 from "path";
3877
3680
 
3878
3681
  // src/cli/lib/failOn.ts
3879
3682
  function shouldFail(result, failOn) {
@@ -3888,7 +3691,7 @@ function shouldFail(result, failOn) {
3888
3691
 
3889
3692
  // src/cli/commands/validate.ts
3890
3693
  async function runValidate(options) {
3891
- const root = path21.resolve(options.root);
3694
+ const root = path19.resolve(options.root);
3892
3695
  const configResult = await loadConfig(root);
3893
3696
  const result = await validateProject(root, configResult);
3894
3697
  const normalized = normalizeValidationResult(root, result);
@@ -4005,12 +3808,12 @@ function issueKey(issue7) {
4005
3808
  }
4006
3809
  async function emitJson(result, root, jsonPath) {
4007
3810
  const abs = resolveJsonPath(root, jsonPath);
4008
- await mkdir5(path21.dirname(abs), { recursive: true });
3811
+ await mkdir4(path19.dirname(abs), { recursive: true });
4009
3812
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4010
3813
  `, "utf-8");
4011
3814
  }
4012
3815
  function resolveJsonPath(root, jsonPath) {
4013
- return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
3816
+ return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
4014
3817
  }
4015
3818
  var GITHUB_ANNOTATION_LIMIT = 100;
4016
3819
 
@@ -4027,8 +3830,6 @@ function parseArgs(argv, cwd) {
4027
3830
  reportRunValidate: false,
4028
3831
  doctorFormat: "text",
4029
3832
  validateFormat: "text",
4030
- syncFormat: "text",
4031
- syncMode: "check",
4032
3833
  strict: false,
4033
3834
  help: false
4034
3835
  };
@@ -4082,8 +3883,6 @@ function parseArgs(argv, cwd) {
4082
3883
  if (next) {
4083
3884
  if (command === "doctor") {
4084
3885
  options.doctorOut = next;
4085
- } else if (command === "sync") {
4086
- options.syncOut = next;
4087
3886
  } else {
4088
3887
  options.reportOut = next;
4089
3888
  }
@@ -4091,28 +3890,6 @@ function parseArgs(argv, cwd) {
4091
3890
  }
4092
3891
  i += 1;
4093
3892
  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;
4116
3893
  case "--in":
4117
3894
  {
4118
3895
  const next = args[i + 1];
@@ -4157,12 +3934,6 @@ function applyFormatOption(command, value, options) {
4157
3934
  }
4158
3935
  return;
4159
3936
  }
4160
- if (command === "sync") {
4161
- if (value === "text" || value === "json") {
4162
- options.syncFormat = value;
4163
- }
4164
- return;
4165
- }
4166
3937
  if (value === "md" || value === "json") {
4167
3938
  options.reportFormat = value;
4168
3939
  }
@@ -4222,18 +3993,6 @@ async function run(argv, cwd) {
4222
3993
  process.exitCode = exitCode;
4223
3994
  }
4224
3995
  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;
4237
3996
  default:
4238
3997
  error(`Unknown command: ${command}`);
4239
3998
  info(usage());
@@ -4248,7 +4007,6 @@ Commands:
4248
4007
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
4249
4008
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
4250
4009
  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
4252
4010
 
4253
4011
  Options:
4254
4012
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -4258,13 +4016,11 @@ Options:
4258
4016
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
4259
4017
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
4260
4018
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
4261
- --format <text|json> doctor/sync \u306E\u51FA\u529B\u5F62\u5F0F
4019
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
4262
4020
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
4263
4021
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
4264
4022
  --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
4265
- --mode <check|export> sync: \u52D5\u4F5C\u30E2\u30FC\u30C9\uFF08default: check\uFF09
4266
4023
  --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
4268
4024
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
4269
4025
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
4270
4026
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A