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.
- package/README.md +206 -22
- package/dist/cli/index.cjs +281 -8
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +281 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
# QFAI Toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/qfai)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
|
|
7
|
+
品質重視型AI駆動運用モデル(SDD × ATDD × TDD)を単一パッケージで提供するツールキットです。
|
|
8
|
+
|
|
9
|
+
## 目次
|
|
10
|
+
|
|
11
|
+
- [インストール](#インストール)
|
|
12
|
+
- [Quick Start](#quick-start最短成功)
|
|
13
|
+
- [機能](#できること)
|
|
14
|
+
- [CLI リファレンス](#使い方cli)
|
|
15
|
+
- [設定](#設定)
|
|
16
|
+
- [契約](#契約contracts)
|
|
17
|
+
- [Monorepo 対応](#monorepo--サブディレクトリ)
|
|
18
|
+
- [CI 統合](#ci-と-hard-gate)
|
|
19
|
+
- [GitHub Actions](#github-actions-テンプレート)
|
|
20
|
+
- [開発](#開発)
|
|
21
|
+
- [ライセンス](#ライセンス)
|
|
22
|
+
|
|
23
|
+
## インストール
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install qfai
|
|
27
|
+
```
|
|
4
28
|
|
|
5
|
-
|
|
29
|
+
または
|
|
6
30
|
|
|
7
|
-
```
|
|
8
|
-
|
|
31
|
+
```sh
|
|
32
|
+
npx qfai init
|
|
9
33
|
```
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
**必要環境**: Node.js >= 18
|
|
12
36
|
|
|
13
|
-
|
|
37
|
+
## パッケージ
|
|
38
|
+
|
|
39
|
+
- `qfai`: CLI + コア + テンプレートを同梱
|
|
40
|
+
|
|
41
|
+
## Quick Start(最短成功)
|
|
42
|
+
|
|
43
|
+
```sh
|
|
14
44
|
npx qfai init
|
|
15
45
|
npx qfai validate --fail-on error --format github
|
|
16
46
|
npx qfai report
|
|
@@ -24,33 +54,76 @@ npx qfai report
|
|
|
24
54
|
- `npx qfai doctor` による設定/探索/パス/glob/validate.json の事前診断
|
|
25
55
|
- `npx qfai report` によるレポート出力
|
|
26
56
|
|
|
27
|
-
|
|
57
|
+
補足: v0.x は日本語テンプレ中心で提供します。将来は英語を正本、日本語を別ドキュメントに切り替える方針です。
|
|
58
|
+
|
|
59
|
+
## 使い方(CLI)
|
|
60
|
+
|
|
61
|
+
`validate` は `--fail-on` / `--strict` によって CI ゲート化できます。`validate` は常に `.qfai/out/validate.json`(`output.validateJsonPath`)へ JSON を出力します。`--format` は画面表示(text/github)のみを制御します。`--format github` はアノテーションの上限と重複排除を行い、先頭にサマリを出します(全量は `validate.json` か `--format text` を参照)。
|
|
62
|
+
`report` は `.qfai/out/validate.json` を既定入力とし、`--in` で上書きできます(優先順位: CLI > config)。`--run-validate` を指定すると validate を実行してから report を生成します。出力先は `--out` で変更できます(`--format json` の場合は `.qfai/out/report.json`)。
|
|
63
|
+
`doctor` は validate/report の前段で設定/探索/パス/glob/validate.json を診断します。`--format text|json`、`--out` をサポートし、診断のみ(修復はしません)。`--fail-on warning|error` を指定すると該当 severity 以上で exit 1(未指定は常に exit 0)になります。
|
|
64
|
+
`report.json` は非契約(experimental / internal)として扱います。外部 consumer は依存しないでください。フィールドは例であり固定ではありません。短い例:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"tool": "qfai",
|
|
69
|
+
"summary": {
|
|
70
|
+
"specs": 1,
|
|
71
|
+
"scenarios": 1,
|
|
72
|
+
"contracts": { "api": 0, "ui": 1, "db": 0 },
|
|
73
|
+
"counts": { "info": 0, "warning": 0, "error": 0 }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
doctor(text)の例:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
qfai doctor: root=. config=qfai.config.yaml (found)
|
|
82
|
+
[ok] config.search: qfai.config.yaml found
|
|
83
|
+
summary: ok=10 warning=2 error=0
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
doctor の JSON も非契約(内部形式。将来予告なく変更あり)です。フィールドは例であり固定ではありません。短い例:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"tool": "qfai",
|
|
91
|
+
"checks": [
|
|
92
|
+
{
|
|
93
|
+
"id": "config.search",
|
|
94
|
+
"severity": "ok",
|
|
95
|
+
"message": "qfai.config.yaml found"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
28
100
|
|
|
29
|
-
`validate` は `--fail-on` / `--strict` によって CI ゲート化できます。`validate` は常に `.qfai/out/validate.json`(`output.validateJsonPath`)へ JSON を出力し、`--format` は表示形式(text/github)のみを制御します。
|
|
30
|
-
`report` は `.qfai/out/validate.json` を読み、既定で `.qfai/out/report.md` を出力します(`--format json` の場合は `.qfai/out/report.json`)。出力先は `--out` で変更できます。入力パスは固定です。
|
|
31
|
-
`doctor` は validate/report の前段で設定/探索/パス/glob/validate.json を診断します。`--format text|json`、`--out` をサポートします。`--fail-on warning|error` を指定すると該当 severity 以上で exit 1(未指定は常に exit 0)になります。
|
|
32
101
|
`init --yes` は予約フラグです(現行の init は非対話のため挙動差はありません)。既存ファイルがある場合は `--force` が必要です。
|
|
33
|
-
|
|
102
|
+
|
|
103
|
+
## 設定
|
|
34
104
|
|
|
35
105
|
設定はリポジトリ直下の `qfai.config.yaml` で行います。
|
|
36
106
|
命名規約は `docs/rules/naming.md` を参照してください。
|
|
37
107
|
|
|
38
|
-
## Contracts
|
|
108
|
+
## 契約(Contracts)
|
|
39
109
|
|
|
40
|
-
Spec では `QFAI-CONTRACT-REF:` 行で参照する契約IDを宣言します(`none`
|
|
41
|
-
Scenario では `# QFAI-CONTRACT-REF:` のコメント行で契約参照を宣言します(`none`
|
|
110
|
+
Spec では `QFAI-CONTRACT-REF:` 行で参照する契約IDを宣言します(`none` 可)。Spec の先頭 H1 に `SPEC-xxxx` が必須です。
|
|
111
|
+
Scenario では `# QFAI-CONTRACT-REF:` のコメント行で契約参照を宣言します(`none` 可)。
|
|
42
112
|
契約ファイルは `QFAI-CONTRACT-ID: <ID>` を **1ファイル1ID** で宣言します。
|
|
43
|
-
|
|
44
|
-
宣言では大文字(`UI-0001`)、ファイル名は小文字(`ui-0001-...`)を使用します。
|
|
113
|
+
`validate.json` / `report` の file path は root 相対で出力します(absolute は出力しません)。
|
|
45
114
|
|
|
46
|
-
|
|
115
|
+
## Monorepo / サブディレクトリ
|
|
47
116
|
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
50
|
-
-
|
|
51
|
-
- `unknownContractIdSeverity` は Scenario 側(`QFAI-TRACE-008`)のみを制御
|
|
117
|
+
- `--root` 未指定時は cwd から親へ `qfai.config.yaml` を探索します(見つからない場合は defaultConfig + warning)。
|
|
118
|
+
- monorepo ではパッケージ単位に `qfai.config.yaml` を置くか、`--root` で明示します。
|
|
119
|
+
- `paths.outDir` はパッケージごとに分け、`out/` の衝突を避けてください。
|
|
52
120
|
|
|
53
|
-
|
|
121
|
+
例(pnpm workspace):
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
packages/<app-a>/qfai.config.yaml # paths.outDir: .qfai/out/<app-a>
|
|
125
|
+
packages/<app-b>/qfai.config.yaml # paths.outDir: .qfai/out/<app-b>
|
|
126
|
+
```
|
|
54
127
|
|
|
55
128
|
## CI と Hard Gate
|
|
56
129
|
|
|
@@ -68,3 +141,114 @@ SC→Test 検証は `validation.traceability.scMustHaveTest` と
|
|
|
68
141
|
- `validation.traceability.testFileExcludeGlobs`: 追加の除外 glob(配列、任意)
|
|
69
142
|
- `validation.traceability.scMustHaveTest`: SC→Test 検証の有効/無効を制御(`true` で有効、`false` で無効)
|
|
70
143
|
- `validation.traceability.scNoTestSeverity`: SC 未参照時の重要度を指定(`error` / `warning`)
|
|
144
|
+
|
|
145
|
+
## GitHub Actions テンプレート
|
|
146
|
+
|
|
147
|
+
`npx qfai init` で `.github/workflows/qfai.yml` を生成します。テンプレートは `validate` ジョブで `.qfai/out/validate.json` を生成し、`qfai-validation` として artifact をアップロードします。`report` はテンプレートには含まれないため、必要なら別ジョブまたはローカルで `qfai report` を実行してください。
|
|
148
|
+
|
|
149
|
+
テンプレートは npm 前提です。pnpm を使う場合は `cache` と install コマンドを置き換えてください。
|
|
150
|
+
各 Actions のバージョンは運用方針に合わせて指定してください。
|
|
151
|
+
|
|
152
|
+
追加で `report` を回す場合の最小例:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
jobs:
|
|
156
|
+
report:
|
|
157
|
+
needs: validate
|
|
158
|
+
runs-on: ubuntu-latest
|
|
159
|
+
steps:
|
|
160
|
+
- uses: actions/checkout@v4
|
|
161
|
+
- uses: actions/setup-node@v4
|
|
162
|
+
with:
|
|
163
|
+
node-version: lts/*
|
|
164
|
+
cache: npm
|
|
165
|
+
- run: npm ci
|
|
166
|
+
- uses: actions/download-artifact@v4
|
|
167
|
+
with:
|
|
168
|
+
name: qfai-validation
|
|
169
|
+
path: .qfai/out
|
|
170
|
+
- run: npx qfai report --out .qfai/out/report.md
|
|
171
|
+
- uses: actions/upload-artifact@v4
|
|
172
|
+
with:
|
|
173
|
+
name: qfai-report
|
|
174
|
+
path: .qfai/out/report.md
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
validate.json のスキーマと例は `docs/schema` / `docs/examples` を参照してください。
|
|
178
|
+
PromptPack は非契約(互換保証なし)です。編集する場合はラップ運用を推奨します。
|
|
179
|
+
|
|
180
|
+
## 生成される構成(例)
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
qfai.config.yaml
|
|
184
|
+
.qfai/
|
|
185
|
+
README.md
|
|
186
|
+
require/
|
|
187
|
+
README.md
|
|
188
|
+
specs/
|
|
189
|
+
README.md
|
|
190
|
+
spec-0001/
|
|
191
|
+
spec.md
|
|
192
|
+
delta.md
|
|
193
|
+
scenario.md
|
|
194
|
+
rules/
|
|
195
|
+
conventions.md
|
|
196
|
+
pnpm.md
|
|
197
|
+
promptpack/
|
|
198
|
+
constitution.md
|
|
199
|
+
steering/
|
|
200
|
+
compatibility-vs-change.md
|
|
201
|
+
traceability.md
|
|
202
|
+
naming.md
|
|
203
|
+
commands/
|
|
204
|
+
plan.md
|
|
205
|
+
implement.md
|
|
206
|
+
review.md
|
|
207
|
+
release.md
|
|
208
|
+
roles/
|
|
209
|
+
qa.md
|
|
210
|
+
spec.md
|
|
211
|
+
test.md
|
|
212
|
+
modes/
|
|
213
|
+
compatibility.md
|
|
214
|
+
change.md
|
|
215
|
+
prompts/
|
|
216
|
+
README.md
|
|
217
|
+
makeOverview.md
|
|
218
|
+
makeBusinessFlow.md
|
|
219
|
+
require-to-spec.md
|
|
220
|
+
qfai-generate-test-globs.md
|
|
221
|
+
qfai-maintain-traceability.md
|
|
222
|
+
qfai-maintain-contracts.md
|
|
223
|
+
qfai-classify-change.md
|
|
224
|
+
contracts/
|
|
225
|
+
README.md
|
|
226
|
+
api/
|
|
227
|
+
api-0001-sample.yaml
|
|
228
|
+
ui/
|
|
229
|
+
ui-0001-sample.yaml
|
|
230
|
+
db/
|
|
231
|
+
db-0001-sample.sql
|
|
232
|
+
out/
|
|
233
|
+
README.md
|
|
234
|
+
tests/
|
|
235
|
+
qfai-traceability.sample.test.ts
|
|
236
|
+
.github/
|
|
237
|
+
workflows/
|
|
238
|
+
qfai.yml
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 開発
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
pnpm install
|
|
245
|
+
pnpm build
|
|
246
|
+
pnpm format:check
|
|
247
|
+
pnpm lint
|
|
248
|
+
pnpm check-types
|
|
249
|
+
pnpm test:assets
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## ライセンス
|
|
253
|
+
|
|
254
|
+
[MIT](https://github.com/aganesy/QFAI/blob/main/LICENSE)
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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.
|
|
893
|
-
return "0.
|
|
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/
|
|
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 =
|
|
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,
|
|
3802
|
-
await (0,
|
|
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
|
|
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
|