swallowkit 1.0.0-beta.54 → 1.0.0-beta.56
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/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +154 -157
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +178 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +7 -2
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +111 -123
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +127 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/auth-operations.d.ts +57 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +269 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +23 -13
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/file-session.d.ts +53 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +195 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +5 -0
- package/dist/core/operations/runtime.d.ts.map +1 -1
- package/dist/core/operations/runtime.js +23 -0
- package/dist/core/operations/runtime.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js +1 -13
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +195 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +255 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +108 -52
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.js +9 -9
- package/dist/core/scaffold/nextjs-generator.js +6 -6
- package/dist/core/scaffold/ui-generator.js +19 -19
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +62 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +343 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +8 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +4 -1
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +9 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +350 -16
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +202 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
- package/src/__tests__/agent-loop-phase34.test.ts +449 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/init.test.ts +50 -0
- package/src/__tests__/mcp.test.ts +113 -5
- package/src/__tests__/multi-auth.test.ts +1 -1
- package/src/__tests__/plan-apply.test.ts +294 -0
- package/src/cli/commands/add-auth.ts +164 -164
- package/src/cli/commands/init.ts +186 -24
- package/src/cli/commands/scaffold.ts +128 -129
- package/src/cli/index.ts +148 -2
- package/src/core/config.ts +5 -4
- package/src/core/operations/auth-operations.ts +353 -0
- package/src/core/operations/create-model.ts +25 -13
- package/src/core/operations/file-session.ts +210 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +23 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +2 -16
- package/src/core/operations/scaffold-plan.ts +224 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/workflows.ts +249 -0
- package/src/core/scaffold/auth-generator.ts +119 -60
- package/src/core/scaffold/functions-generator.ts +9 -9
- package/src/core/scaffold/nextjs-generator.ts +6 -6
- package/src/core/scaffold/ui-generator.ts +19 -19
- package/src/core/verify/index.ts +392 -0
- package/src/machine/contracts.ts +16 -0
- package/src/machine/errors.ts +12 -1
- package/src/machine/index.ts +444 -15
- package/src/mcp/index.ts +198 -2
- package/src/types/index.ts +17 -0
|
@@ -71,7 +71,7 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
71
71
|
const refModelCamel = toCamelCase(refModel);
|
|
72
72
|
return ` fetch('/api/${refModelCamel}')
|
|
73
73
|
.then(res => res.json())
|
|
74
|
-
.then((data:
|
|
74
|
+
.then((data: Array<{ id: string; name?: string; title?: string }>) => {
|
|
75
75
|
const map: Record<string, string> = {};
|
|
76
76
|
data.forEach(item => {
|
|
77
77
|
// name または title フィールドを表示用文字列として使用
|
|
@@ -138,8 +138,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
138
138
|
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
139
139
|
|
|
140
140
|
set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
|
|
141
|
-
} catch (err
|
|
142
|
-
alert(\`Error: \${err.message}\`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
|
|
@@ -216,7 +216,7 @@ ${displayFields.map(f => {
|
|
|
216
216
|
const displayField = f.nestedDisplayField || 'name';
|
|
217
217
|
if (f.isArray) {
|
|
218
218
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
219
|
-
{Array.isArray(item.${f.name}) ? item.${f.name}.map((ref:
|
|
219
|
+
{Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}
|
|
220
220
|
</td>`;
|
|
221
221
|
} else {
|
|
222
222
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
@@ -317,7 +317,7 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
|
|
|
317
317
|
const refModelCamel = toCamelCase(refModel);
|
|
318
318
|
return ` fetch('/api/${refModelCamel}')
|
|
319
319
|
.then(res => res.json())
|
|
320
|
-
.then((data:
|
|
320
|
+
.then((data: Array<{ id: string; name?: string; title?: string }>) => {
|
|
321
321
|
const map: Record<string, string> = {};
|
|
322
322
|
data.forEach(item => {
|
|
323
323
|
map[item.id] = item.name || item.title || item.id;
|
|
@@ -389,8 +389,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
389
389
|
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
390
390
|
|
|
391
391
|
router.push('/${modelKebab}');
|
|
392
|
-
} catch (err
|
|
393
|
-
alert(\`Error: \${err.message}\`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
394
394
|
}
|
|
395
395
|
};
|
|
396
396
|
|
|
@@ -455,7 +455,7 @@ ${model.fields.map(f => {
|
|
|
455
455
|
if (f.isArray) {
|
|
456
456
|
return ` <div>
|
|
457
457
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
|
|
458
|
-
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref:
|
|
458
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}</dd>
|
|
459
459
|
</div>`;
|
|
460
460
|
} else {
|
|
461
461
|
return ` <div>
|
|
@@ -547,7 +547,7 @@ export function generateFormComponent(model: ModelInfo, sharedPackageName: strin
|
|
|
547
547
|
return ` // ${refModelPascal} の一覧を取得
|
|
548
548
|
fetch('/api/${refModelCamel}')
|
|
549
549
|
.then(res => res.json())
|
|
550
|
-
.then(data => set${refModelPascal}Options(data.map((item:
|
|
550
|
+
.then(data => set${refModelPascal}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
|
|
551
551
|
.catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
|
|
552
552
|
}).join('\n');
|
|
553
553
|
|
|
@@ -581,7 +581,7 @@ ${formFields.map(f => {
|
|
|
581
581
|
// ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
|
|
582
582
|
if (f.isNestedSchema) {
|
|
583
583
|
if (f.isArray) {
|
|
584
|
-
return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item:
|
|
584
|
+
return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item: { id: string }) => item.id) : []) : [] as string[],`;
|
|
585
585
|
} else {
|
|
586
586
|
return ` ${f.name}Id: initialData?.${f.name}?.id ?? '',`;
|
|
587
587
|
}
|
|
@@ -615,7 +615,7 @@ ${needsUseEffect ? `
|
|
|
615
615
|
${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
|
|
616
616
|
fetch('/api/${toKebabCase(f.referencedModel!)}')
|
|
617
617
|
.then(res => res.json())
|
|
618
|
-
.then(data => set${f.referencedModel}Options(data.map((item:
|
|
618
|
+
.then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
|
|
619
619
|
.catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
|
|
620
620
|
${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
621
621
|
}, []);
|
|
@@ -627,7 +627,7 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
|
627
627
|
|
|
628
628
|
try {
|
|
629
629
|
// Array フィールドをカンマ区切りから配列に変換
|
|
630
|
-
const submitData:
|
|
630
|
+
const submitData: Record<string, unknown> = { ...formData };
|
|
631
631
|
${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
|
|
632
632
|
submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
|
|
633
633
|
}`).join('\n')}
|
|
@@ -651,7 +651,7 @@ ${nestedSchemaFields.map(f => {
|
|
|
651
651
|
if (f.isArray) {
|
|
652
652
|
return ` // ${refModelPascal} 配列参照の変換
|
|
653
653
|
if (submitData.${f.name}Ids) {
|
|
654
|
-
submitData.${f.name} = submitData.${f.name}Ids
|
|
654
|
+
submitData.${f.name} = (submitData.${f.name}Ids as string[])
|
|
655
655
|
.map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
|
|
656
656
|
.filter(Boolean);
|
|
657
657
|
delete submitData.${f.name}Ids;
|
|
@@ -688,17 +688,17 @@ ${nestedSchemaFields.map(f => {
|
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
router.push('/${toKebabCase(modelName)}');
|
|
691
|
-
} catch (err
|
|
692
|
-
if (err.
|
|
691
|
+
} catch (err) {
|
|
692
|
+
if (err instanceof z.ZodError) {
|
|
693
693
|
// Zod validation errors
|
|
694
694
|
const fieldErrors: Record<string, string> = {};
|
|
695
|
-
err.issues.forEach((
|
|
696
|
-
const field =
|
|
697
|
-
fieldErrors[field] =
|
|
695
|
+
err.issues.forEach((issue) => {
|
|
696
|
+
const field = String(issue.path[0]);
|
|
697
|
+
fieldErrors[field] = issue.message;
|
|
698
698
|
});
|
|
699
699
|
setErrors(fieldErrors);
|
|
700
700
|
} else {
|
|
701
|
-
alert(\`Error: \${err.message}\`);
|
|
701
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
702
702
|
}
|
|
703
703
|
setLoading(false);
|
|
704
704
|
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify — 生成後の検証チェックを実行し、機械可読な結果を返す。
|
|
3
|
+
*
|
|
4
|
+
* 提供チェック:
|
|
5
|
+
* - structure: プロジェクト構造規約 (validateProject)
|
|
6
|
+
* - drift: 生成物と現在状態の乖離 (detectDrift)
|
|
7
|
+
* - typecheck: TypeScript 型チェック (tsc --noEmit / typecheck script)
|
|
8
|
+
* - build/lint/test: package.json scripts に同名 script がある場合のみ実行 (なければ skip)
|
|
9
|
+
* - custom: swallowkit.config の verify.checks で定義されたプロジェクト固有チェック
|
|
10
|
+
*
|
|
11
|
+
* 既定実行セットは structure/drift/typecheck + custom。build/lint/test は --checks で明示指定。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import { loadConfig } from "../config";
|
|
18
|
+
import { detectDrift, DriftFinding } from "../project/drift";
|
|
19
|
+
import { saveLastVerifyState } from "../project/state";
|
|
20
|
+
import { ProjectViolation, validateProject } from "../project/validation";
|
|
21
|
+
import { CustomVerifyCheck } from "../../types";
|
|
22
|
+
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
23
|
+
|
|
24
|
+
export type VerifyCheckStatus = "pass" | "fail" | "skip" | "error";
|
|
25
|
+
|
|
26
|
+
export interface VerifyCheckEvidence {
|
|
27
|
+
command?: string;
|
|
28
|
+
exitCode?: number;
|
|
29
|
+
violations?: ProjectViolation[];
|
|
30
|
+
findings?: DriftFinding[];
|
|
31
|
+
logTail?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface VerifyCheckResult {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
status: VerifyCheckStatus;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
fixable: boolean;
|
|
40
|
+
evidence: VerifyCheckEvidence;
|
|
41
|
+
suggestedActions: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VerifySummary {
|
|
45
|
+
passed: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
skipped: number;
|
|
48
|
+
errors: number;
|
|
49
|
+
/** true = すべてのチェックが pass または skip */
|
|
50
|
+
done: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface VerifyResult {
|
|
54
|
+
checks: VerifyCheckResult[];
|
|
55
|
+
summary: VerifySummary;
|
|
56
|
+
verifiedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "build", "lint", "test"] as const;
|
|
60
|
+
export type VerifyCheckId = (typeof AVAILABLE_VERIFY_CHECKS)[number];
|
|
61
|
+
|
|
62
|
+
/** 既定で実行される built-in チェック。build/lint/test は明示指定時のみ。 */
|
|
63
|
+
export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck"] as const;
|
|
64
|
+
|
|
65
|
+
async function runStructureCheck(projectRoot: string): Promise<VerifyCheckResult> {
|
|
66
|
+
const startedAt = Date.now();
|
|
67
|
+
try {
|
|
68
|
+
const result = await validateProject(projectRoot);
|
|
69
|
+
const errors = result.violations.filter((violation) => violation.severity === "error");
|
|
70
|
+
return {
|
|
71
|
+
id: "structure",
|
|
72
|
+
title: "Project structure conventions",
|
|
73
|
+
status: errors.length === 0 ? "pass" : "fail",
|
|
74
|
+
durationMs: Date.now() - startedAt,
|
|
75
|
+
fixable: true,
|
|
76
|
+
evidence: { violations: result.violations },
|
|
77
|
+
suggestedActions:
|
|
78
|
+
errors.length === 0
|
|
79
|
+
? []
|
|
80
|
+
: errors.map((violation) => violation.suggestedFix ?? violation.message),
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
id: "structure",
|
|
85
|
+
title: "Project structure conventions",
|
|
86
|
+
status: "error",
|
|
87
|
+
durationMs: Date.now() - startedAt,
|
|
88
|
+
fixable: false,
|
|
89
|
+
evidence: { logTail: [error instanceof Error ? error.message : String(error)] },
|
|
90
|
+
suggestedActions: ["Run this command inside a SwallowKit project root."],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runDriftCheck(projectRoot: string): Promise<VerifyCheckResult> {
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
try {
|
|
98
|
+
const result = await detectDrift(projectRoot);
|
|
99
|
+
const errors = result.findings.filter((finding) => finding.severity === "error");
|
|
100
|
+
return {
|
|
101
|
+
id: "drift",
|
|
102
|
+
title: "Generated artifact drift",
|
|
103
|
+
status: errors.length === 0 ? "pass" : "fail",
|
|
104
|
+
durationMs: Date.now() - startedAt,
|
|
105
|
+
fixable: true,
|
|
106
|
+
evidence: { findings: result.findings },
|
|
107
|
+
suggestedActions: errors.map((finding) => finding.repairAction),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
id: "drift",
|
|
112
|
+
title: "Generated artifact drift",
|
|
113
|
+
status: "error",
|
|
114
|
+
durationMs: Date.now() - startedAt,
|
|
115
|
+
fixable: false,
|
|
116
|
+
evidence: { logTail: [error instanceof Error ? error.message : String(error)] },
|
|
117
|
+
suggestedActions: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function tailLines(text: string, count: number): string[] {
|
|
123
|
+
return text
|
|
124
|
+
.split(/\r?\n/)
|
|
125
|
+
.filter((line) => line.trim().length > 0)
|
|
126
|
+
.slice(-count);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface CommandCheckSpec {
|
|
130
|
+
id: string;
|
|
131
|
+
title: string;
|
|
132
|
+
command: string;
|
|
133
|
+
args: string[];
|
|
134
|
+
timeoutMs?: number;
|
|
135
|
+
failAction: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyCheckResult {
|
|
139
|
+
const startedAt = Date.now();
|
|
140
|
+
const commandLabel = [spec.command, ...spec.args].join(" ").trim();
|
|
141
|
+
try {
|
|
142
|
+
const result = spawnSync(spec.command, spec.args, {
|
|
143
|
+
cwd: projectRoot,
|
|
144
|
+
shell: true,
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
timeout: spec.timeoutMs ?? 5 * 60 * 1000,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
150
|
+
const exitCode = typeof result.status === "number" ? result.status : -1;
|
|
151
|
+
|
|
152
|
+
if (result.error) {
|
|
153
|
+
return {
|
|
154
|
+
id: spec.id,
|
|
155
|
+
title: spec.title,
|
|
156
|
+
status: "error",
|
|
157
|
+
durationMs: Date.now() - startedAt,
|
|
158
|
+
fixable: false,
|
|
159
|
+
evidence: { command: commandLabel, logTail: [result.error.message] },
|
|
160
|
+
suggestedActions: [`Ensure "${commandLabel}" can run in this project.`],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
id: spec.id,
|
|
166
|
+
title: spec.title,
|
|
167
|
+
status: exitCode === 0 ? "pass" : "fail",
|
|
168
|
+
durationMs: Date.now() - startedAt,
|
|
169
|
+
fixable: true,
|
|
170
|
+
evidence: {
|
|
171
|
+
command: commandLabel,
|
|
172
|
+
exitCode,
|
|
173
|
+
logTail: tailLines(output, 50),
|
|
174
|
+
},
|
|
175
|
+
suggestedActions: exitCode === 0 ? [] : [spec.failAction],
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
id: spec.id,
|
|
180
|
+
title: spec.title,
|
|
181
|
+
status: "error",
|
|
182
|
+
durationMs: Date.now() - startedAt,
|
|
183
|
+
fixable: false,
|
|
184
|
+
evidence: { command: commandLabel, logTail: [error instanceof Error ? error.message : String(error)] },
|
|
185
|
+
suggestedActions: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readPackageScripts(projectRoot: string): Record<string, string> {
|
|
191
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
192
|
+
if (!fs.existsSync(packageJsonPath)) return {};
|
|
193
|
+
try {
|
|
194
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { scripts?: Record<string, string> };
|
|
195
|
+
return pkg.scripts ?? {};
|
|
196
|
+
} catch {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
|
|
202
|
+
const startedAt = Date.now();
|
|
203
|
+
|
|
204
|
+
if (!fs.existsSync(path.join(projectRoot, "tsconfig.json"))) {
|
|
205
|
+
return {
|
|
206
|
+
id: "typecheck",
|
|
207
|
+
title: "TypeScript type check",
|
|
208
|
+
status: "skip",
|
|
209
|
+
durationMs: Date.now() - startedAt,
|
|
210
|
+
fixable: false,
|
|
211
|
+
evidence: { logTail: ["tsconfig.json not found; skipping typecheck."] },
|
|
212
|
+
suggestedActions: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const scripts = readPackageScripts(projectRoot);
|
|
217
|
+
let command: string;
|
|
218
|
+
let args: string[];
|
|
219
|
+
if (typeof scripts.typecheck === "string") {
|
|
220
|
+
const pm = detectFromProject(projectRoot);
|
|
221
|
+
command = getCommands(pm).name;
|
|
222
|
+
args = ["run", "typecheck"];
|
|
223
|
+
} else {
|
|
224
|
+
command = "npx";
|
|
225
|
+
args = ["tsc", "--noEmit"];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return runCommandCheck(projectRoot, {
|
|
229
|
+
id: "typecheck",
|
|
230
|
+
title: "TypeScript type check",
|
|
231
|
+
command,
|
|
232
|
+
args,
|
|
233
|
+
failAction: "Fix the TypeScript errors listed in evidence.logTail, then re-run verify.",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const SCRIPT_CHECK_TITLES: Record<string, string> = {
|
|
238
|
+
build: "Project build",
|
|
239
|
+
lint: "Lint",
|
|
240
|
+
test: "Test suite",
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function runScriptCheck(projectRoot: string, scriptName: "build" | "lint" | "test"): VerifyCheckResult {
|
|
244
|
+
const startedAt = Date.now();
|
|
245
|
+
const scripts = readPackageScripts(projectRoot);
|
|
246
|
+
const title = SCRIPT_CHECK_TITLES[scriptName];
|
|
247
|
+
|
|
248
|
+
if (typeof scripts[scriptName] !== "string") {
|
|
249
|
+
return {
|
|
250
|
+
id: scriptName,
|
|
251
|
+
title,
|
|
252
|
+
status: "skip",
|
|
253
|
+
durationMs: Date.now() - startedAt,
|
|
254
|
+
fixable: false,
|
|
255
|
+
evidence: { logTail: [`No "${scriptName}" script in package.json; skipping.`] },
|
|
256
|
+
suggestedActions: [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const pm = detectFromProject(projectRoot);
|
|
261
|
+
return runCommandCheck(projectRoot, {
|
|
262
|
+
id: scriptName,
|
|
263
|
+
title,
|
|
264
|
+
command: getCommands(pm).name,
|
|
265
|
+
args: ["run", scriptName],
|
|
266
|
+
timeoutMs: 10 * 60 * 1000,
|
|
267
|
+
failAction: `Fix the ${scriptName} failures listed in evidence.logTail, then re-run verify.`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const CUSTOM_CHECK_ID = /^[a-z][a-z0-9-]*$/;
|
|
272
|
+
|
|
273
|
+
/** swallowkit.config の verify.checks を projectRoot 基準で静かに読み込む。 */
|
|
274
|
+
export function loadCustomVerifyChecks(projectRoot: string): CustomVerifyCheck[] {
|
|
275
|
+
const configNames = ["swallowkit.config.json", "swallowkit.config.js", ".swallowkitrc.json"];
|
|
276
|
+
for (const name of configNames) {
|
|
277
|
+
const configPath = path.join(projectRoot, name);
|
|
278
|
+
if (!fs.existsSync(configPath)) continue;
|
|
279
|
+
try {
|
|
280
|
+
const checks = loadConfig(configPath, false, true).verify?.checks ?? [];
|
|
281
|
+
return checks.filter(
|
|
282
|
+
(check) =>
|
|
283
|
+
typeof check?.id === "string" &&
|
|
284
|
+
CUSTOM_CHECK_ID.test(check.id) &&
|
|
285
|
+
!(AVAILABLE_VERIFY_CHECKS as readonly string[]).includes(check.id) &&
|
|
286
|
+
typeof check.command === "string" &&
|
|
287
|
+
check.command.trim().length > 0
|
|
288
|
+
);
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function runCustomCheck(projectRoot: string, check: CustomVerifyCheck): VerifyCheckResult {
|
|
297
|
+
return runCommandCheck(projectRoot, {
|
|
298
|
+
id: check.id,
|
|
299
|
+
title: check.title ?? `Custom check: ${check.id}`,
|
|
300
|
+
command: check.command,
|
|
301
|
+
args: [],
|
|
302
|
+
timeoutMs: check.timeoutMs,
|
|
303
|
+
failAction: `Fix the failures reported by "${check.command}", then re-run verify.`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function runVerify(
|
|
308
|
+
checkIds?: string[],
|
|
309
|
+
projectRoot: string = process.cwd()
|
|
310
|
+
): Promise<VerifyResult> {
|
|
311
|
+
const customChecks = loadCustomVerifyChecks(projectRoot);
|
|
312
|
+
const customIds = customChecks.map((check) => check.id);
|
|
313
|
+
const availableIds = [...AVAILABLE_VERIFY_CHECKS, ...customIds];
|
|
314
|
+
|
|
315
|
+
const requested = (checkIds && checkIds.length > 0
|
|
316
|
+
? checkIds
|
|
317
|
+
: [...DEFAULT_VERIFY_CHECKS, ...customIds]
|
|
318
|
+
).map((id) => id.trim()).filter(Boolean);
|
|
319
|
+
|
|
320
|
+
const unknown = requested.filter((id) => !availableIds.includes(id));
|
|
321
|
+
if (unknown.length > 0) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Unknown verify check(s): ${unknown.join(", ")}. Available checks: ${availableIds.join(", ")}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const checks: VerifyCheckResult[] = [];
|
|
328
|
+
for (const id of requested) {
|
|
329
|
+
if (id === "structure") {
|
|
330
|
+
checks.push(await runStructureCheck(projectRoot));
|
|
331
|
+
} else if (id === "drift") {
|
|
332
|
+
checks.push(await runDriftCheck(projectRoot));
|
|
333
|
+
} else if (id === "typecheck") {
|
|
334
|
+
checks.push(runTypecheckCheck(projectRoot));
|
|
335
|
+
} else if (id === "build" || id === "lint" || id === "test") {
|
|
336
|
+
checks.push(runScriptCheck(projectRoot, id));
|
|
337
|
+
} else {
|
|
338
|
+
const custom = customChecks.find((check) => check.id === id);
|
|
339
|
+
if (custom) checks.push(runCustomCheck(projectRoot, custom));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const summary: VerifySummary = {
|
|
344
|
+
passed: checks.filter((check) => check.status === "pass").length,
|
|
345
|
+
failed: checks.filter((check) => check.status === "fail").length,
|
|
346
|
+
skipped: checks.filter((check) => check.status === "skip").length,
|
|
347
|
+
errors: checks.filter((check) => check.status === "error").length,
|
|
348
|
+
done: checks.every((check) => check.status === "pass" || check.status === "skip"),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const result: VerifyResult = {
|
|
352
|
+
checks,
|
|
353
|
+
summary,
|
|
354
|
+
verifiedAt: new Date().toISOString(),
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
saveLastVerifyState(result, projectRoot);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface FailureExplanation {
|
|
362
|
+
checkId: string;
|
|
363
|
+
title: string;
|
|
364
|
+
status: VerifyCheckStatus;
|
|
365
|
+
verifiedAt: string;
|
|
366
|
+
evidence: VerifyCheckEvidence;
|
|
367
|
+
suggestedActions: string[];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function explainVerifyFailure(
|
|
371
|
+
lastVerify: VerifyResult,
|
|
372
|
+
checkId?: string
|
|
373
|
+
): FailureExplanation[] {
|
|
374
|
+
const targets = checkId
|
|
375
|
+
? lastVerify.checks.filter((check) => check.id === checkId)
|
|
376
|
+
: lastVerify.checks.filter((check) => check.status === "fail" || check.status === "error");
|
|
377
|
+
|
|
378
|
+
if (checkId && targets.length === 0) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Unknown check "${checkId}" in the last verify result. Available: ${lastVerify.checks.map((check) => check.id).join(", ")}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return targets.map((check) => ({
|
|
385
|
+
checkId: check.id,
|
|
386
|
+
title: check.title,
|
|
387
|
+
status: check.status,
|
|
388
|
+
verifiedAt: lastVerify.verifiedAt,
|
|
389
|
+
evidence: check.evidence,
|
|
390
|
+
suggestedActions: check.suggestedActions,
|
|
391
|
+
}));
|
|
392
|
+
}
|
package/src/machine/contracts.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
export type MachineOperationStatus =
|
|
2
|
+
| "complete"
|
|
3
|
+
| "in-progress"
|
|
4
|
+
| "blocked"
|
|
5
|
+
| "requires-human"
|
|
6
|
+
| "unsafe"
|
|
7
|
+
| "failed";
|
|
8
|
+
|
|
9
|
+
export interface MachineNextAction {
|
|
10
|
+
command: string;
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
export interface MachineSuccessResponse<TData> {
|
|
2
15
|
ok: true;
|
|
3
16
|
command: string;
|
|
17
|
+
status?: MachineOperationStatus;
|
|
18
|
+
nextActions?: MachineNextAction[];
|
|
4
19
|
data: TData;
|
|
5
20
|
}
|
|
6
21
|
|
|
7
22
|
export interface MachineErrorResponse {
|
|
8
23
|
ok: false;
|
|
9
24
|
command: string;
|
|
25
|
+
status?: MachineOperationStatus;
|
|
10
26
|
error: {
|
|
11
27
|
code: string;
|
|
12
28
|
message: string;
|
package/src/machine/errors.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import type { MachineOperationStatus } from "./contracts";
|
|
2
|
+
|
|
1
3
|
export class MachineCommandError extends Error {
|
|
2
4
|
readonly code: string;
|
|
3
5
|
readonly details?: unknown;
|
|
6
|
+
readonly status?: MachineOperationStatus;
|
|
4
7
|
|
|
5
|
-
constructor(code: string, message: string, details?: unknown) {
|
|
8
|
+
constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus) {
|
|
6
9
|
super(message);
|
|
7
10
|
this.name = "MachineCommandError";
|
|
8
11
|
this.code = code;
|
|
9
12
|
this.details = details;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveMachineErrorStatus(error: unknown): MachineOperationStatus {
|
|
18
|
+
if (error instanceof MachineCommandError && error.status) {
|
|
19
|
+
return error.status;
|
|
10
20
|
}
|
|
21
|
+
return "failed";
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
export function toMachineError(error: unknown): { code: string; message: string; details?: unknown } {
|