ai-spec-dev 0.31.0 → 0.33.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/.claude/settings.local.json +5 -1
- package/RELEASE_LOG.md +155 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +153 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +277 -0
- package/cli/utils.ts +83 -0
- package/core/dsl-feedback.ts +255 -0
- package/core/run-trend.ts +241 -0
- package/core/self-evaluator.ts +106 -2
- package/dist/cli/index.js +972 -449
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +972 -449
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +6 -3
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- package/tests/task-generator.test.ts +230 -0
package/dist/cli/index.js
CHANGED
|
@@ -488,9 +488,9 @@ var init_workspace_loader = __esm({
|
|
|
488
488
|
|
|
489
489
|
// cli/index.ts
|
|
490
490
|
var import_commander = require("commander");
|
|
491
|
-
var
|
|
492
|
-
var
|
|
493
|
-
var
|
|
491
|
+
var path23 = __toESM(require("path"));
|
|
492
|
+
var fs24 = __toESM(require("fs-extra"));
|
|
493
|
+
var import_chalk21 = __toESM(require("chalk"));
|
|
494
494
|
var dotenv = __toESM(require("dotenv"));
|
|
495
495
|
var import_prompts3 = require("@inquirer/prompts");
|
|
496
496
|
|
|
@@ -4870,221 +4870,221 @@ function validateDsl(raw) {
|
|
|
4870
4870
|
}
|
|
4871
4871
|
return { valid: true, dsl: raw };
|
|
4872
4872
|
}
|
|
4873
|
-
function validateFeature(raw,
|
|
4873
|
+
function validateFeature(raw, path24, errors) {
|
|
4874
4874
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4875
|
-
errors.push({ path:
|
|
4875
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4876
4876
|
return;
|
|
4877
4877
|
}
|
|
4878
4878
|
const f = raw;
|
|
4879
|
-
requireNonEmptyString(f["id"], `${
|
|
4880
|
-
requireNonEmptyString(f["title"], `${
|
|
4881
|
-
requireNonEmptyString(f["description"], `${
|
|
4879
|
+
requireNonEmptyString(f["id"], `${path24}.id`, errors);
|
|
4880
|
+
requireNonEmptyString(f["title"], `${path24}.title`, errors);
|
|
4881
|
+
requireNonEmptyString(f["description"], `${path24}.description`, errors);
|
|
4882
4882
|
}
|
|
4883
|
-
function validateModel(raw,
|
|
4883
|
+
function validateModel(raw, path24, errors) {
|
|
4884
4884
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4885
|
-
errors.push({ path:
|
|
4885
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4886
4886
|
return;
|
|
4887
4887
|
}
|
|
4888
4888
|
const m = raw;
|
|
4889
|
-
requireNonEmptyString(m["name"], `${
|
|
4889
|
+
requireNonEmptyString(m["name"], `${path24}.name`, errors);
|
|
4890
4890
|
if (!Array.isArray(m["fields"])) {
|
|
4891
|
-
errors.push({ path: `${
|
|
4891
|
+
errors.push({ path: `${path24}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4892
4892
|
} else {
|
|
4893
4893
|
const fields = m["fields"];
|
|
4894
4894
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4895
|
-
errors.push({ path: `${
|
|
4895
|
+
errors.push({ path: `${path24}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4896
4896
|
}
|
|
4897
4897
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4898
|
-
validateModelField(fields[j2], `${
|
|
4898
|
+
validateModelField(fields[j2], `${path24}.fields[${j2}]`, errors);
|
|
4899
4899
|
}
|
|
4900
4900
|
}
|
|
4901
4901
|
if (m["relations"] !== void 0) {
|
|
4902
4902
|
if (!Array.isArray(m["relations"])) {
|
|
4903
|
-
errors.push({ path: `${
|
|
4903
|
+
errors.push({ path: `${path24}.relations`, message: "Must be an array of strings if present" });
|
|
4904
4904
|
} else {
|
|
4905
4905
|
const rels = m["relations"];
|
|
4906
4906
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4907
4907
|
if (typeof rels[j2] !== "string") {
|
|
4908
|
-
errors.push({ path: `${
|
|
4908
|
+
errors.push({ path: `${path24}.relations[${j2}]`, message: "Must be a string" });
|
|
4909
4909
|
}
|
|
4910
4910
|
}
|
|
4911
4911
|
}
|
|
4912
4912
|
}
|
|
4913
4913
|
}
|
|
4914
|
-
function validateModelField(raw,
|
|
4914
|
+
function validateModelField(raw, path24, errors) {
|
|
4915
4915
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4916
|
-
errors.push({ path:
|
|
4916
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4917
4917
|
return;
|
|
4918
4918
|
}
|
|
4919
4919
|
const f = raw;
|
|
4920
|
-
requireNonEmptyString(f["name"], `${
|
|
4921
|
-
requireNonEmptyString(f["type"], `${
|
|
4920
|
+
requireNonEmptyString(f["name"], `${path24}.name`, errors);
|
|
4921
|
+
requireNonEmptyString(f["type"], `${path24}.type`, errors);
|
|
4922
4922
|
if (typeof f["required"] !== "boolean") {
|
|
4923
|
-
errors.push({ path: `${
|
|
4923
|
+
errors.push({ path: `${path24}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4924
4924
|
}
|
|
4925
4925
|
}
|
|
4926
|
-
function validateEndpoint(raw,
|
|
4926
|
+
function validateEndpoint(raw, path24, errors) {
|
|
4927
4927
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4928
|
-
errors.push({ path:
|
|
4928
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4929
4929
|
return;
|
|
4930
4930
|
}
|
|
4931
4931
|
const e = raw;
|
|
4932
|
-
requireNonEmptyString(e["id"], `${
|
|
4933
|
-
requireNonEmptyString(e["description"], `${
|
|
4932
|
+
requireNonEmptyString(e["id"], `${path24}.id`, errors);
|
|
4933
|
+
requireNonEmptyString(e["description"], `${path24}.description`, errors);
|
|
4934
4934
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4935
4935
|
errors.push({
|
|
4936
|
-
path: `${
|
|
4936
|
+
path: `${path24}.method`,
|
|
4937
4937
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4938
4938
|
});
|
|
4939
4939
|
}
|
|
4940
4940
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4941
4941
|
errors.push({
|
|
4942
|
-
path: `${
|
|
4942
|
+
path: `${path24}.path`,
|
|
4943
4943
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4944
4944
|
});
|
|
4945
4945
|
}
|
|
4946
4946
|
if (typeof e["auth"] !== "boolean") {
|
|
4947
|
-
errors.push({ path: `${
|
|
4947
|
+
errors.push({ path: `${path24}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4948
4948
|
}
|
|
4949
4949
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4950
4950
|
errors.push({
|
|
4951
|
-
path: `${
|
|
4951
|
+
path: `${path24}.successStatus`,
|
|
4952
4952
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4953
4953
|
});
|
|
4954
4954
|
}
|
|
4955
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4955
|
+
requireNonEmptyString(e["successDescription"], `${path24}.successDescription`, errors);
|
|
4956
4956
|
if (e["request"] !== void 0) {
|
|
4957
|
-
validateRequestSchema(e["request"], `${
|
|
4957
|
+
validateRequestSchema(e["request"], `${path24}.request`, errors);
|
|
4958
4958
|
}
|
|
4959
4959
|
if (e["errors"] !== void 0) {
|
|
4960
4960
|
if (!Array.isArray(e["errors"])) {
|
|
4961
|
-
errors.push({ path: `${
|
|
4961
|
+
errors.push({ path: `${path24}.errors`, message: "Must be an array if present" });
|
|
4962
4962
|
} else {
|
|
4963
4963
|
const errs = e["errors"];
|
|
4964
4964
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4965
|
-
errors.push({ path: `${
|
|
4965
|
+
errors.push({ path: `${path24}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4966
4966
|
}
|
|
4967
4967
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4968
|
-
validateResponseError(errs[j2], `${
|
|
4968
|
+
validateResponseError(errs[j2], `${path24}.errors[${j2}]`, errors);
|
|
4969
4969
|
}
|
|
4970
4970
|
}
|
|
4971
4971
|
}
|
|
4972
4972
|
}
|
|
4973
|
-
function validateRequestSchema(raw,
|
|
4973
|
+
function validateRequestSchema(raw, path24, errors) {
|
|
4974
4974
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4975
|
-
errors.push({ path:
|
|
4975
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4976
4976
|
return;
|
|
4977
4977
|
}
|
|
4978
4978
|
const r = raw;
|
|
4979
4979
|
for (const key of ["body", "query", "params"]) {
|
|
4980
4980
|
if (r[key] !== void 0) {
|
|
4981
|
-
validateFieldMap(r[key], `${
|
|
4981
|
+
validateFieldMap(r[key], `${path24}.${key}`, errors);
|
|
4982
4982
|
}
|
|
4983
4983
|
}
|
|
4984
4984
|
}
|
|
4985
|
-
function validateFieldMap(raw,
|
|
4985
|
+
function validateFieldMap(raw, path24, errors) {
|
|
4986
4986
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4987
|
-
errors.push({ path:
|
|
4987
|
+
errors.push({ path: path24, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4988
4988
|
return;
|
|
4989
4989
|
}
|
|
4990
4990
|
const map = raw;
|
|
4991
4991
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4992
4992
|
if (typeof v2 !== "string") {
|
|
4993
|
-
errors.push({ path: `${
|
|
4993
|
+
errors.push({ path: `${path24}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4994
4994
|
}
|
|
4995
4995
|
}
|
|
4996
4996
|
}
|
|
4997
|
-
function validateResponseError(raw,
|
|
4997
|
+
function validateResponseError(raw, path24, errors) {
|
|
4998
4998
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4999
|
-
errors.push({ path:
|
|
4999
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5000
5000
|
return;
|
|
5001
5001
|
}
|
|
5002
5002
|
const e = raw;
|
|
5003
5003
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5004
|
-
errors.push({ path: `${
|
|
5004
|
+
errors.push({ path: `${path24}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5005
5005
|
}
|
|
5006
|
-
requireNonEmptyString(e["code"], `${
|
|
5007
|
-
requireNonEmptyString(e["description"], `${
|
|
5006
|
+
requireNonEmptyString(e["code"], `${path24}.code`, errors);
|
|
5007
|
+
requireNonEmptyString(e["description"], `${path24}.description`, errors);
|
|
5008
5008
|
}
|
|
5009
|
-
function validateBehavior(raw,
|
|
5009
|
+
function validateBehavior(raw, path24, errors) {
|
|
5010
5010
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5011
|
-
errors.push({ path:
|
|
5011
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5012
5012
|
return;
|
|
5013
5013
|
}
|
|
5014
5014
|
const b = raw;
|
|
5015
|
-
requireNonEmptyString(b["id"], `${
|
|
5016
|
-
requireNonEmptyString(b["description"], `${
|
|
5015
|
+
requireNonEmptyString(b["id"], `${path24}.id`, errors);
|
|
5016
|
+
requireNonEmptyString(b["description"], `${path24}.description`, errors);
|
|
5017
5017
|
if (b["constraints"] !== void 0) {
|
|
5018
5018
|
if (!Array.isArray(b["constraints"])) {
|
|
5019
|
-
errors.push({ path: `${
|
|
5019
|
+
errors.push({ path: `${path24}.constraints`, message: "Must be an array of strings if present" });
|
|
5020
5020
|
} else {
|
|
5021
5021
|
const cs2 = b["constraints"];
|
|
5022
5022
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5023
5023
|
if (typeof cs2[j2] !== "string") {
|
|
5024
|
-
errors.push({ path: `${
|
|
5024
|
+
errors.push({ path: `${path24}.constraints[${j2}]`, message: "Must be a string" });
|
|
5025
5025
|
}
|
|
5026
5026
|
}
|
|
5027
5027
|
}
|
|
5028
5028
|
}
|
|
5029
5029
|
}
|
|
5030
|
-
function validateComponent(raw,
|
|
5030
|
+
function validateComponent(raw, path24, errors) {
|
|
5031
5031
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5032
|
-
errors.push({ path:
|
|
5032
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5033
5033
|
return;
|
|
5034
5034
|
}
|
|
5035
5035
|
const c = raw;
|
|
5036
|
-
requireNonEmptyString(c["id"], `${
|
|
5037
|
-
requireNonEmptyString(c["name"], `${
|
|
5038
|
-
requireNonEmptyString(c["description"], `${
|
|
5036
|
+
requireNonEmptyString(c["id"], `${path24}.id`, errors);
|
|
5037
|
+
requireNonEmptyString(c["name"], `${path24}.name`, errors);
|
|
5038
|
+
requireNonEmptyString(c["description"], `${path24}.description`, errors);
|
|
5039
5039
|
if (c["props"] !== void 0) {
|
|
5040
5040
|
if (!Array.isArray(c["props"])) {
|
|
5041
|
-
errors.push({ path: `${
|
|
5041
|
+
errors.push({ path: `${path24}.props`, message: "Must be an array if present" });
|
|
5042
5042
|
} else {
|
|
5043
5043
|
const props = c["props"];
|
|
5044
5044
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5045
5045
|
const p = props[j2];
|
|
5046
5046
|
if (typeof p !== "object" || p === null) {
|
|
5047
|
-
errors.push({ path: `${
|
|
5047
|
+
errors.push({ path: `${path24}.props[${j2}]`, message: "Must be an object" });
|
|
5048
5048
|
continue;
|
|
5049
5049
|
}
|
|
5050
|
-
requireNonEmptyString(p["name"], `${
|
|
5051
|
-
requireNonEmptyString(p["type"], `${
|
|
5050
|
+
requireNonEmptyString(p["name"], `${path24}.props[${j2}].name`, errors);
|
|
5051
|
+
requireNonEmptyString(p["type"], `${path24}.props[${j2}].type`, errors);
|
|
5052
5052
|
if (typeof p["required"] !== "boolean") {
|
|
5053
|
-
errors.push({ path: `${
|
|
5053
|
+
errors.push({ path: `${path24}.props[${j2}].required`, message: "Must be boolean" });
|
|
5054
5054
|
}
|
|
5055
5055
|
}
|
|
5056
5056
|
}
|
|
5057
5057
|
}
|
|
5058
5058
|
if (c["events"] !== void 0) {
|
|
5059
5059
|
if (!Array.isArray(c["events"])) {
|
|
5060
|
-
errors.push({ path: `${
|
|
5060
|
+
errors.push({ path: `${path24}.events`, message: "Must be an array if present" });
|
|
5061
5061
|
} else {
|
|
5062
5062
|
const events = c["events"];
|
|
5063
5063
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5064
5064
|
const e = events[j2];
|
|
5065
5065
|
if (typeof e !== "object" || e === null) {
|
|
5066
|
-
errors.push({ path: `${
|
|
5066
|
+
errors.push({ path: `${path24}.events[${j2}]`, message: "Must be an object" });
|
|
5067
5067
|
continue;
|
|
5068
5068
|
}
|
|
5069
|
-
requireNonEmptyString(e["name"], `${
|
|
5069
|
+
requireNonEmptyString(e["name"], `${path24}.events[${j2}].name`, errors);
|
|
5070
5070
|
}
|
|
5071
5071
|
}
|
|
5072
5072
|
}
|
|
5073
5073
|
if (c["state"] !== void 0) {
|
|
5074
5074
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5075
|
-
errors.push({ path: `${
|
|
5075
|
+
errors.push({ path: `${path24}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5076
5076
|
}
|
|
5077
5077
|
}
|
|
5078
5078
|
if (c["apiCalls"] !== void 0) {
|
|
5079
5079
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5080
|
-
errors.push({ path: `${
|
|
5080
|
+
errors.push({ path: `${path24}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5081
5081
|
}
|
|
5082
5082
|
}
|
|
5083
5083
|
}
|
|
5084
|
-
function requireNonEmptyString(v2,
|
|
5084
|
+
function requireNonEmptyString(v2, path24, errors) {
|
|
5085
5085
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5086
5086
|
errors.push({
|
|
5087
|
-
path:
|
|
5087
|
+
path: path24,
|
|
5088
5088
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5089
5089
|
});
|
|
5090
5090
|
}
|
|
@@ -6046,8 +6046,8 @@ var RunSnapshot = class {
|
|
|
6046
6046
|
const fullPath = path7.isAbsolute(filePath) ? filePath : path7.join(this.workingDir, filePath);
|
|
6047
6047
|
if (!await fs8.pathExists(fullPath)) return;
|
|
6048
6048
|
if (this.snapshotted.has(fullPath)) return;
|
|
6049
|
-
const
|
|
6050
|
-
const dest = path7.join(this.backupRoot,
|
|
6049
|
+
const relative8 = path7.relative(this.workingDir, fullPath);
|
|
6050
|
+
const dest = path7.join(this.backupRoot, relative8);
|
|
6051
6051
|
await fs8.ensureDir(path7.dirname(dest));
|
|
6052
6052
|
await fs8.copy(fullPath, dest);
|
|
6053
6053
|
this.snapshotted.add(fullPath);
|
|
@@ -6062,11 +6062,11 @@ var RunSnapshot = class {
|
|
|
6062
6062
|
if (entry.isDirectory()) {
|
|
6063
6063
|
await walk(full);
|
|
6064
6064
|
} else {
|
|
6065
|
-
const
|
|
6066
|
-
const dest = path7.join(this.workingDir,
|
|
6065
|
+
const relative8 = path7.relative(this.backupRoot, full);
|
|
6066
|
+
const dest = path7.join(this.workingDir, relative8);
|
|
6067
6067
|
await fs8.ensureDir(path7.dirname(dest));
|
|
6068
6068
|
await fs8.copy(full, dest, { overwrite: true });
|
|
6069
|
-
restored.push(
|
|
6069
|
+
restored.push(relative8);
|
|
6070
6070
|
}
|
|
6071
6071
|
}
|
|
6072
6072
|
};
|
|
@@ -9959,6 +9959,17 @@ function extractReviewScore(reviewText) {
|
|
|
9959
9959
|
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
9960
9960
|
return match ? parseFloat(match[1]) : null;
|
|
9961
9961
|
}
|
|
9962
|
+
function modelNameTokens(name) {
|
|
9963
|
+
const lower = name.toLowerCase();
|
|
9964
|
+
const parts = name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").split("-").filter(Boolean);
|
|
9965
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
9966
|
+
tokens.add(lower);
|
|
9967
|
+
if (parts.length > 1) {
|
|
9968
|
+
tokens.add(parts.join("-"));
|
|
9969
|
+
tokens.add(parts.join("_"));
|
|
9970
|
+
}
|
|
9971
|
+
return [...tokens];
|
|
9972
|
+
}
|
|
9962
9973
|
function runSelfEval(opts) {
|
|
9963
9974
|
const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
|
|
9964
9975
|
const endpointsTotal = dsl?.endpoints?.length ?? 0;
|
|
@@ -9966,16 +9977,39 @@ function runSelfEval(opts) {
|
|
|
9966
9977
|
const endpointLayerCovered = generatedFiles.some(
|
|
9967
9978
|
(f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9968
9979
|
);
|
|
9980
|
+
const endpointLayerFiles = generatedFiles.filter(
|
|
9981
|
+
(f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9982
|
+
).length;
|
|
9969
9983
|
const modelLayerCovered = generatedFiles.some(
|
|
9970
9984
|
(f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9971
9985
|
);
|
|
9986
|
+
let modelNameMatched = 0;
|
|
9987
|
+
if (modelsTotal > 0 && dsl?.models) {
|
|
9988
|
+
for (const model of dsl.models) {
|
|
9989
|
+
const tokens = modelNameTokens(model.name);
|
|
9990
|
+
const found = generatedFiles.some((f) => {
|
|
9991
|
+
const lf = f.toLowerCase();
|
|
9992
|
+
return tokens.some((t) => lf.includes(t));
|
|
9993
|
+
});
|
|
9994
|
+
if (found) modelNameMatched++;
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9997
|
+
const modelNameCoverage = modelsTotal > 0 ? modelNameMatched / modelsTotal : 1;
|
|
9972
9998
|
let dslCoverageScore = 10;
|
|
9973
9999
|
if (generatedFiles.length === 0) {
|
|
9974
10000
|
dslCoverageScore = 0;
|
|
9975
10001
|
} else {
|
|
9976
10002
|
if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
|
|
9977
10003
|
if (modelsTotal > 0 && !modelLayerCovered) dslCoverageScore -= 3;
|
|
10004
|
+
if (modelsTotal > 0 && modelLayerCovered) {
|
|
10005
|
+
if (modelNameCoverage < 0.5) dslCoverageScore -= 2;
|
|
10006
|
+
else if (modelNameCoverage < 0.8) dslCoverageScore -= 1;
|
|
10007
|
+
}
|
|
10008
|
+
if (endpointsTotal >= 5 && endpointLayerCovered && endpointLayerFiles < 2) {
|
|
10009
|
+
dslCoverageScore -= 1;
|
|
10010
|
+
}
|
|
9978
10011
|
}
|
|
10012
|
+
dslCoverageScore = Math.max(0, Math.min(10, dslCoverageScore));
|
|
9979
10013
|
const compileScore = compilePassed ? 10 : 5;
|
|
9980
10014
|
const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
|
|
9981
10015
|
const harnessScore = reviewScore !== null ? Math.round((dslCoverageScore * 0.4 + compileScore * 0.3 + reviewScore * 0.3) * 10) / 10 : Math.round((dslCoverageScore * 0.55 + compileScore * 0.45) * 10) / 10;
|
|
@@ -9988,8 +10022,11 @@ function runSelfEval(opts) {
|
|
|
9988
10022
|
detail: {
|
|
9989
10023
|
endpointsTotal,
|
|
9990
10024
|
endpointLayerCovered,
|
|
10025
|
+
endpointLayerFiles,
|
|
9991
10026
|
modelsTotal,
|
|
9992
10027
|
modelLayerCovered,
|
|
10028
|
+
modelNameCoverage: Math.round(modelNameCoverage * 100) / 100,
|
|
10029
|
+
modelNameMatched,
|
|
9993
10030
|
filesWritten: generatedFiles.length
|
|
9994
10031
|
}
|
|
9995
10032
|
};
|
|
@@ -9999,32 +10036,324 @@ function runSelfEval(opts) {
|
|
|
9999
10036
|
dslCoverageScore,
|
|
10000
10037
|
compileScore,
|
|
10001
10038
|
reviewScore: reviewScore ?? void 0,
|
|
10002
|
-
promptHash
|
|
10039
|
+
promptHash,
|
|
10040
|
+
modelNameCoverage: result.detail.modelNameCoverage,
|
|
10041
|
+
modelNameMatched: result.detail.modelNameMatched,
|
|
10042
|
+
endpointLayerFiles: result.detail.endpointLayerFiles
|
|
10003
10043
|
});
|
|
10004
10044
|
return result;
|
|
10005
10045
|
}
|
|
10006
10046
|
function printSelfEval(result) {
|
|
10007
|
-
const
|
|
10047
|
+
const scoreColor2 = result.harnessScore >= 8 ? import_chalk18.default.green : result.harnessScore >= 6 ? import_chalk18.default.yellow : import_chalk18.default.red;
|
|
10008
10048
|
const filled = Math.round(result.harnessScore);
|
|
10009
10049
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
10010
10050
|
const compileTag = result.compileScore === 10 ? import_chalk18.default.green("pass") : import_chalk18.default.yellow("partial");
|
|
10011
10051
|
const reviewTag = result.reviewScore !== null ? `Review: ${result.reviewScore}/10` : import_chalk18.default.gray("Review: skipped");
|
|
10052
|
+
let modelCoverageTag = "";
|
|
10053
|
+
if (result.detail.modelsTotal > 0) {
|
|
10054
|
+
const pct = Math.round(result.detail.modelNameCoverage * 100);
|
|
10055
|
+
const tag = `Models: ${result.detail.modelNameMatched}/${result.detail.modelsTotal} (${pct}%)`;
|
|
10056
|
+
modelCoverageTag = pct >= 80 ? import_chalk18.default.green(tag) : pct >= 50 ? import_chalk18.default.yellow(tag) : import_chalk18.default.red(tag);
|
|
10057
|
+
}
|
|
10012
10058
|
console.log(import_chalk18.default.cyan("\n\u2500\u2500\u2500 Harness Self-Eval \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10013
|
-
console.log(` Score : ${
|
|
10059
|
+
console.log(` Score : ${scoreColor2(`[${bar}] ${result.harnessScore}/10`)}`);
|
|
10014
10060
|
console.log(
|
|
10015
|
-
` DSL : ${
|
|
10061
|
+
` DSL : ${scoreColor2(String(result.dslCoverageScore) + "/10")} Compile: ${compileTag} ${reviewTag}`
|
|
10016
10062
|
);
|
|
10063
|
+
if (modelCoverageTag) {
|
|
10064
|
+
console.log(
|
|
10065
|
+
` Detail : ${modelCoverageTag} ` + import_chalk18.default.gray(`Endpoints: ${result.detail.endpointsTotal} Files: ${result.detail.filesWritten}`)
|
|
10066
|
+
);
|
|
10067
|
+
}
|
|
10017
10068
|
console.log(import_chalk18.default.gray(` Prompt : ${result.promptHash}`));
|
|
10018
|
-
console.log(import_chalk18.default.
|
|
10069
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(49)));
|
|
10070
|
+
}
|
|
10071
|
+
|
|
10072
|
+
// core/run-trend.ts
|
|
10073
|
+
var fs23 = __toESM(require("fs-extra"));
|
|
10074
|
+
var path22 = __toESM(require("path"));
|
|
10075
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
10076
|
+
var LOG_DIR2 = ".ai-spec-logs";
|
|
10077
|
+
async function loadRunLogs(workingDir) {
|
|
10078
|
+
const logDir = path22.join(workingDir, LOG_DIR2);
|
|
10079
|
+
if (!await fs23.pathExists(logDir)) return [];
|
|
10080
|
+
const files = await fs23.readdir(logDir);
|
|
10081
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
|
|
10082
|
+
const logs = [];
|
|
10083
|
+
for (const file of jsonFiles) {
|
|
10084
|
+
try {
|
|
10085
|
+
const log = await fs23.readJson(path22.join(logDir, file));
|
|
10086
|
+
if (log.runId && log.startedAt) {
|
|
10087
|
+
logs.push(log);
|
|
10088
|
+
}
|
|
10089
|
+
} catch {
|
|
10090
|
+
}
|
|
10091
|
+
}
|
|
10092
|
+
return logs;
|
|
10093
|
+
}
|
|
10094
|
+
function buildTrendReport(logs, opts = {}) {
|
|
10095
|
+
let entries = logs.map((log) => ({
|
|
10096
|
+
runId: log.runId,
|
|
10097
|
+
startedAt: log.startedAt,
|
|
10098
|
+
promptHash: log.promptHash ?? null,
|
|
10099
|
+
harnessScore: log.harnessScore ?? null,
|
|
10100
|
+
specPath: log.specPath ?? null,
|
|
10101
|
+
provider: log.provider ?? null,
|
|
10102
|
+
model: log.model ?? null,
|
|
10103
|
+
filesWritten: log.filesWritten?.length ?? 0,
|
|
10104
|
+
totalDurationMs: log.totalDurationMs ?? null,
|
|
10105
|
+
errors: log.errors?.length ?? 0
|
|
10106
|
+
}));
|
|
10107
|
+
entries = entries.filter((e) => e.harnessScore !== null);
|
|
10108
|
+
if (opts.promptFilter) {
|
|
10109
|
+
entries = entries.filter(
|
|
10110
|
+
(e) => e.promptHash?.startsWith(opts.promptFilter)
|
|
10111
|
+
);
|
|
10112
|
+
}
|
|
10113
|
+
if (opts.last && opts.last > 0) {
|
|
10114
|
+
entries = entries.slice(0, opts.last);
|
|
10115
|
+
}
|
|
10116
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
10117
|
+
for (const e of entries) {
|
|
10118
|
+
const key = e.promptHash ?? "(none)";
|
|
10119
|
+
if (!groupMap.has(key)) groupMap.set(key, []);
|
|
10120
|
+
groupMap.get(key).push(e);
|
|
10121
|
+
}
|
|
10122
|
+
const currentHash = entries[0]?.promptHash ?? null;
|
|
10123
|
+
const promptGroups = [];
|
|
10124
|
+
for (const [hash, group] of groupMap.entries()) {
|
|
10125
|
+
const scores = group.map((e) => e.harnessScore);
|
|
10126
|
+
promptGroups.push({
|
|
10127
|
+
promptHash: hash,
|
|
10128
|
+
runs: group.length,
|
|
10129
|
+
avg: Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 10) / 10,
|
|
10130
|
+
best: Math.max(...scores),
|
|
10131
|
+
worst: Math.min(...scores),
|
|
10132
|
+
firstSeen: group[group.length - 1].startedAt,
|
|
10133
|
+
lastSeen: group[0].startedAt,
|
|
10134
|
+
isCurrent: hash === currentHash
|
|
10135
|
+
});
|
|
10136
|
+
}
|
|
10137
|
+
promptGroups.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
|
|
10138
|
+
return { entries, promptGroups, totalRuns: entries.length };
|
|
10139
|
+
}
|
|
10140
|
+
function scoreBar2(score) {
|
|
10141
|
+
const filled = Math.round(score);
|
|
10142
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
10143
|
+
}
|
|
10144
|
+
function scoreColor(score, text) {
|
|
10145
|
+
if (score >= 8) return import_chalk19.default.green(text);
|
|
10146
|
+
if (score >= 6) return import_chalk19.default.yellow(text);
|
|
10147
|
+
return import_chalk19.default.red(text);
|
|
10148
|
+
}
|
|
10149
|
+
function formatDate(iso) {
|
|
10150
|
+
return iso.slice(0, 10);
|
|
10151
|
+
}
|
|
10152
|
+
function formatDuration(ms2) {
|
|
10153
|
+
if (ms2 === null) return " \u2014 ";
|
|
10154
|
+
const s = Math.round(ms2 / 1e3);
|
|
10155
|
+
if (s < 60) return `${s}s`;
|
|
10156
|
+
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
10157
|
+
}
|
|
10158
|
+
function shortSpec(specPath) {
|
|
10159
|
+
if (!specPath) return import_chalk19.default.gray("\u2014");
|
|
10160
|
+
return path22.basename(specPath);
|
|
10161
|
+
}
|
|
10162
|
+
function printTrendReport(report, workingDir) {
|
|
10163
|
+
const { entries, promptGroups } = report;
|
|
10164
|
+
console.log(import_chalk19.default.cyan("\n\u2500\u2500\u2500 Harness Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10165
|
+
if (entries.length === 0) {
|
|
10166
|
+
console.log(import_chalk19.default.gray(" No scored runs found. Run `ai-spec create` to start tracking."));
|
|
10167
|
+
console.log(import_chalk19.default.cyan("\u2500".repeat(63)));
|
|
10168
|
+
return;
|
|
10169
|
+
}
|
|
10170
|
+
if (promptGroups.length > 0) {
|
|
10171
|
+
console.log(import_chalk19.default.bold("\n Prompt Versions:\n"));
|
|
10172
|
+
const colWidths = {
|
|
10173
|
+
hash: 10,
|
|
10174
|
+
runs: 5,
|
|
10175
|
+
avg: 5,
|
|
10176
|
+
best: 5,
|
|
10177
|
+
worst: 5
|
|
10178
|
+
};
|
|
10179
|
+
console.log(
|
|
10180
|
+
import_chalk19.default.gray(
|
|
10181
|
+
" " + "Hash ".padEnd(colWidths.hash) + " " + "Runs ".padStart(colWidths.runs) + " Avg Best Worst Last seen"
|
|
10182
|
+
)
|
|
10183
|
+
);
|
|
10184
|
+
console.log(import_chalk19.default.gray(" " + "\u2500".repeat(55)));
|
|
10185
|
+
for (const g of promptGroups) {
|
|
10186
|
+
const currentMark = g.isCurrent ? import_chalk19.default.cyan(" \u25C0 current") : "";
|
|
10187
|
+
const avgStr = scoreColor(g.avg, g.avg.toFixed(1).padStart(5));
|
|
10188
|
+
const bestStr = import_chalk19.default.green(g.best.toFixed(1).padStart(5));
|
|
10189
|
+
const worstStr = g.worst < 6 ? import_chalk19.default.red(g.worst.toFixed(1).padStart(5)) : import_chalk19.default.yellow(g.worst.toFixed(1).padStart(5));
|
|
10190
|
+
console.log(
|
|
10191
|
+
" " + import_chalk19.default.white(g.promptHash.padEnd(colWidths.hash)) + " " + import_chalk19.default.gray(String(g.runs).padStart(colWidths.runs)) + " " + avgStr + " " + bestStr + " " + worstStr + " " + import_chalk19.default.gray(formatDate(g.lastSeen)) + currentMark
|
|
10192
|
+
);
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
console.log(import_chalk19.default.bold("\n Run History:\n"));
|
|
10196
|
+
for (const e of entries) {
|
|
10197
|
+
const score = e.harnessScore;
|
|
10198
|
+
const bar = scoreColor(score, `[${scoreBar2(score)}]`);
|
|
10199
|
+
const scoreStr = scoreColor(score, score.toFixed(1).padStart(4));
|
|
10200
|
+
const hash = e.promptHash ? import_chalk19.default.gray(e.promptHash) : import_chalk19.default.gray("(no hash)");
|
|
10201
|
+
const dur = import_chalk19.default.gray(formatDuration(e.totalDurationMs));
|
|
10202
|
+
const errMark = e.errors > 0 ? import_chalk19.default.yellow(` \u26A0${e.errors}err`) : "";
|
|
10203
|
+
const spec = import_chalk19.default.gray(shortSpec(e.specPath));
|
|
10204
|
+
console.log(
|
|
10205
|
+
` ${import_chalk19.default.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
|
|
10206
|
+
);
|
|
10207
|
+
}
|
|
10208
|
+
const logRelDir = path22.relative(workingDir, path22.join(workingDir, LOG_DIR2));
|
|
10209
|
+
console.log(import_chalk19.default.gray(`
|
|
10210
|
+
${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
|
|
10211
|
+
console.log(import_chalk19.default.cyan("\u2500".repeat(63)));
|
|
10212
|
+
}
|
|
10213
|
+
|
|
10214
|
+
// core/dsl-feedback.ts
|
|
10215
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
10216
|
+
function assessDslRichness(dsl) {
|
|
10217
|
+
const gaps = [];
|
|
10218
|
+
if (dsl.endpoints.length === 0 && dsl.models.length === 0) {
|
|
10219
|
+
gaps.push({
|
|
10220
|
+
code: "no_models_no_endpoints",
|
|
10221
|
+
message: "DSL has no endpoints and no models \u2014 spec may be too abstract for structured extraction",
|
|
10222
|
+
hint: "Please add explicit API endpoint definitions (method, path, request/response) and any data models that this feature requires."
|
|
10223
|
+
});
|
|
10224
|
+
return gaps;
|
|
10225
|
+
}
|
|
10226
|
+
const GENERIC_DESC_KEYWORDS = ["handles", "processes", "manages", "\u64CD\u4F5C", "\u5904\u7406", "\u7BA1\u7406"];
|
|
10227
|
+
const GENERIC_DESC_MIN_LEN = 15;
|
|
10228
|
+
for (const ep of dsl.endpoints) {
|
|
10229
|
+
const desc = (ep.description ?? "").trim();
|
|
10230
|
+
const isGeneric = desc.length < GENERIC_DESC_MIN_LEN || GENERIC_DESC_KEYWORDS.some((kw) => desc.toLowerCase().startsWith(kw));
|
|
10231
|
+
if (isGeneric) {
|
|
10232
|
+
gaps.push({
|
|
10233
|
+
code: "generic_endpoint_desc",
|
|
10234
|
+
message: `Endpoint ${ep.method} ${ep.path} has a vague description: "${desc}"`,
|
|
10235
|
+
hint: `Clarify what ${ep.method} ${ep.path} does: what inputs are required, what the success response contains, and what business rule it enforces.`
|
|
10236
|
+
});
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
const endpointsWithoutErrors = dsl.endpoints.filter(
|
|
10240
|
+
(ep) => !ep.errors || ep.errors.length === 0
|
|
10241
|
+
);
|
|
10242
|
+
if (endpointsWithoutErrors.length > 0 && dsl.endpoints.length >= 2) {
|
|
10243
|
+
gaps.push({
|
|
10244
|
+
code: "missing_errors",
|
|
10245
|
+
message: `${endpointsWithoutErrors.length}/${dsl.endpoints.length} endpoints have no error definitions`,
|
|
10246
|
+
hint: `For each endpoint, specify at least the main error cases: e.g. 400 validation errors, 401 auth failures, 404 not found, 409 conflict. Include an error code (e.g. INVALID_INPUT) and description for each.`
|
|
10247
|
+
});
|
|
10248
|
+
}
|
|
10249
|
+
for (const model of dsl.models) {
|
|
10250
|
+
if (!model.fields || model.fields.length < 2) {
|
|
10251
|
+
gaps.push({
|
|
10252
|
+
code: "sparse_model",
|
|
10253
|
+
message: `Model "${model.name}" has only ${model.fields?.length ?? 0} field(s) \u2014 likely incomplete`,
|
|
10254
|
+
hint: `List all fields for "${model.name}" with their types and whether they are required. Include at minimum an id, created_at, and the core domain fields this model needs.`
|
|
10255
|
+
});
|
|
10256
|
+
}
|
|
10257
|
+
}
|
|
10258
|
+
return gaps;
|
|
10259
|
+
}
|
|
10260
|
+
function buildDslGapRefinementPrompt(spec, gaps) {
|
|
10261
|
+
const gapList = gaps.map((g, i) => `${i + 1}. [${g.code}] ${g.message}
|
|
10262
|
+
\u2192 ${g.hint}`).join("\n\n");
|
|
10263
|
+
return `The following feature spec has been structurally analysed. The DSL extracted from it was found to be incomplete in these specific areas:
|
|
10264
|
+
|
|
10265
|
+
${gapList}
|
|
10266
|
+
|
|
10267
|
+
Your task: revise the spec below to address ONLY the gaps listed above.
|
|
10268
|
+
- Do NOT change the overall feature scope or business logic.
|
|
10269
|
+
- Do NOT rewrite sections that are already complete.
|
|
10270
|
+
- Add missing error cases, clarify vague endpoint descriptions, complete sparse model field lists.
|
|
10271
|
+
- Output ONLY the complete revised Markdown spec. No preamble, no explanation.
|
|
10272
|
+
|
|
10273
|
+
=== Current Spec ===
|
|
10274
|
+
${spec}`;
|
|
10275
|
+
}
|
|
10276
|
+
function extractStructuralFindings(reviewText) {
|
|
10277
|
+
const parts = reviewText.split(/─{20,}/);
|
|
10278
|
+
const pass1Text = parts[0] ?? "";
|
|
10279
|
+
const pass1Score = extractPassScore(pass1Text);
|
|
10280
|
+
if (pass1Score !== null && pass1Score >= 8) return [];
|
|
10281
|
+
const findings = [];
|
|
10282
|
+
if (/缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should/i.test(pass1Text)) {
|
|
10283
|
+
const match = pass1Text.match(/[^。\n]*(?:缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should)[^。\n]*/i);
|
|
10284
|
+
findings.push({
|
|
10285
|
+
category: "auth_design",
|
|
10286
|
+
description: match ? match[0].trim() : "One or more endpoints may have incorrect authentication requirements"
|
|
10287
|
+
});
|
|
10288
|
+
}
|
|
10289
|
+
if (/接口设计.*问题|接口.*不合理|API design|response.*missing|request.*missing|接口.*缺少/i.test(pass1Text)) {
|
|
10290
|
+
const match = pass1Text.match(/[^。\n]*(?:接口设计.*问题|接口.*不合理|API design|response.*missing|接口.*缺少)[^。\n]*/i);
|
|
10291
|
+
findings.push({
|
|
10292
|
+
category: "api_contract",
|
|
10293
|
+
description: match ? match[0].trim() : "API contract design may have issues"
|
|
10294
|
+
});
|
|
10295
|
+
}
|
|
10296
|
+
if (/模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete|字段.*missing/i.test(pass1Text)) {
|
|
10297
|
+
const match = pass1Text.match(/[^。\n]*(?:模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete)[^。\n]*/i);
|
|
10298
|
+
findings.push({
|
|
10299
|
+
category: "model_design",
|
|
10300
|
+
description: match ? match[0].trim() : "Data model design may be incomplete"
|
|
10301
|
+
});
|
|
10302
|
+
}
|
|
10303
|
+
if (/层级.*违反|layer.*violation|business logic.*controller|controller.*service.*混|分层.*问题/i.test(pass1Text)) {
|
|
10304
|
+
const match = pass1Text.match(/[^。\n]*(?:层级.*违反|layer.*violation|business logic.*controller|分层.*问题)[^。\n]*/i);
|
|
10305
|
+
findings.push({
|
|
10306
|
+
category: "layer_violation",
|
|
10307
|
+
description: match ? match[0].trim() : "Layer separation may be violated in the generated code"
|
|
10308
|
+
});
|
|
10309
|
+
}
|
|
10310
|
+
return findings;
|
|
10311
|
+
}
|
|
10312
|
+
function extractPassScore(text) {
|
|
10313
|
+
const m = text.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
10314
|
+
return m ? parseFloat(m[1]) : null;
|
|
10315
|
+
}
|
|
10316
|
+
function buildStructuralAmendmentPrompt(spec, findings) {
|
|
10317
|
+
const findingList = findings.map((f, i) => `${i + 1}. [${f.category}] ${f.description}`).join("\n");
|
|
10318
|
+
return `A code review of the feature built from this spec found the following DESIGN-LEVEL issues.
|
|
10319
|
+
These are problems in the spec/contract itself, not in the implementation.
|
|
10320
|
+
|
|
10321
|
+
=== Structural Findings ===
|
|
10322
|
+
${findingList}
|
|
10323
|
+
|
|
10324
|
+
Your task:
|
|
10325
|
+
- Revise the spec below to correct the design issues listed above.
|
|
10326
|
+
- Do NOT change the feature scope, business logic, or sections unrelated to these findings.
|
|
10327
|
+
- Be minimal: only change what is necessary to fix the design issues.
|
|
10328
|
+
- Output ONLY the complete revised Markdown spec. No preamble, no explanation.
|
|
10329
|
+
|
|
10330
|
+
=== Current Spec ===
|
|
10331
|
+
${spec}`;
|
|
10332
|
+
}
|
|
10333
|
+
function printDslGaps(gaps) {
|
|
10334
|
+
console.log(import_chalk20.default.yellow("\n \u26A0 DSL Completeness Check \u2014 gaps detected:"));
|
|
10335
|
+
for (const gap of gaps) {
|
|
10336
|
+
console.log(import_chalk20.default.yellow(` \xB7 ${gap.message}`));
|
|
10337
|
+
}
|
|
10338
|
+
console.log(import_chalk20.default.gray(" \u2192 A targeted spec refinement can fill these gaps before codegen."));
|
|
10339
|
+
}
|
|
10340
|
+
function printStructuralFindings(findings) {
|
|
10341
|
+
console.log(import_chalk20.default.yellow("\n \u26A0 Review \u2014 structural (design-level) issues found:"));
|
|
10342
|
+
for (const f of findings) {
|
|
10343
|
+
const label = import_chalk20.default.gray(`[${f.category}]`);
|
|
10344
|
+
console.log(` ${label} ${f.description}`);
|
|
10345
|
+
}
|
|
10346
|
+
console.log(import_chalk20.default.gray(" \u2192 These are contract issues in the Spec/DSL, not just implementation problems."));
|
|
10347
|
+
console.log(import_chalk20.default.gray(" \u2192 Fixing the spec now means the next run generates correct code from the start."));
|
|
10019
10348
|
}
|
|
10020
10349
|
|
|
10021
10350
|
// cli/index.ts
|
|
10022
10351
|
dotenv.config();
|
|
10023
10352
|
var CONFIG_FILE = ".ai-spec.json";
|
|
10024
10353
|
async function loadConfig(dir) {
|
|
10025
|
-
const p =
|
|
10026
|
-
if (await
|
|
10027
|
-
return
|
|
10354
|
+
const p = path23.join(dir, CONFIG_FILE);
|
|
10355
|
+
if (await fs24.pathExists(p)) {
|
|
10356
|
+
return fs24.readJson(p);
|
|
10028
10357
|
}
|
|
10029
10358
|
return {};
|
|
10030
10359
|
}
|
|
@@ -10049,20 +10378,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
10049
10378
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
10050
10379
|
});
|
|
10051
10380
|
await saveKey(providerName, newKey.trim());
|
|
10052
|
-
console.log(
|
|
10381
|
+
console.log(import_chalk21.default.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
10053
10382
|
return newKey.trim();
|
|
10054
10383
|
}
|
|
10055
10384
|
function printBanner(opts) {
|
|
10056
|
-
console.log(
|
|
10057
|
-
console.log(
|
|
10058
|
-
console.log(
|
|
10059
|
-
console.log(
|
|
10385
|
+
console.log(import_chalk21.default.blue("\n" + "\u2500".repeat(52)));
|
|
10386
|
+
console.log(import_chalk21.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
10387
|
+
console.log(import_chalk21.default.blue("\u2500".repeat(52)));
|
|
10388
|
+
console.log(import_chalk21.default.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
10060
10389
|
console.log(
|
|
10061
|
-
|
|
10390
|
+
import_chalk21.default.gray(
|
|
10062
10391
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
10063
10392
|
)
|
|
10064
10393
|
);
|
|
10065
|
-
console.log(
|
|
10394
|
+
console.log(import_chalk21.default.blue("\u2500".repeat(52) + "\n"));
|
|
10066
10395
|
}
|
|
10067
10396
|
var program = new import_commander.Command();
|
|
10068
10397
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -10089,42 +10418,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10089
10418
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10090
10419
|
const workspaceConfig = await workspaceLoader.load();
|
|
10091
10420
|
if (workspaceConfig) {
|
|
10092
|
-
console.log(
|
|
10421
|
+
console.log(import_chalk21.default.cyan(`
|
|
10093
10422
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
10094
|
-
console.log(
|
|
10423
|
+
console.log(import_chalk21.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
10095
10424
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
10096
10425
|
if (opts.serve) {
|
|
10097
|
-
console.log(
|
|
10426
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10098
10427
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
10099
10428
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
10100
10429
|
if (!backendResult) {
|
|
10101
|
-
console.log(
|
|
10430
|
+
console.log(import_chalk21.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
10102
10431
|
} else {
|
|
10103
10432
|
const mockPort = 3001;
|
|
10104
10433
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
10105
|
-
const serverJsPath =
|
|
10106
|
-
console.log(
|
|
10434
|
+
const serverJsPath = path23.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
10435
|
+
console.log(import_chalk21.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
10107
10436
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
10108
|
-
console.log(
|
|
10437
|
+
console.log(import_chalk21.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
10109
10438
|
if (frontendResult) {
|
|
10110
10439
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
10111
10440
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
10112
10441
|
if (proxyResult.applied) {
|
|
10113
|
-
console.log(
|
|
10114
|
-
console.log(
|
|
10442
|
+
console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
10443
|
+
console.log(import_chalk21.default.bold.cyan(`
|
|
10115
10444
|
Ready! Run your frontend dev server:`));
|
|
10116
|
-
console.log(
|
|
10117
|
-
console.log(
|
|
10118
|
-
console.log(
|
|
10445
|
+
console.log(import_chalk21.default.white(` cd ${frontendResult.repoAbsPath}`));
|
|
10446
|
+
console.log(import_chalk21.default.white(` ${proxyResult.devCommand}`));
|
|
10447
|
+
console.log(import_chalk21.default.gray(`
|
|
10119
10448
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
10120
10449
|
} else {
|
|
10121
|
-
console.log(
|
|
10122
|
-
if (proxyResult.note) console.log(
|
|
10123
|
-
console.log(
|
|
10450
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
10451
|
+
if (proxyResult.note) console.log(import_chalk21.default.gray(` ${proxyResult.note}`));
|
|
10452
|
+
console.log(import_chalk21.default.gray(` Mock server: http://localhost:${mockPort}`));
|
|
10124
10453
|
}
|
|
10125
10454
|
} else {
|
|
10126
|
-
console.log(
|
|
10127
|
-
console.log(
|
|
10455
|
+
console.log(import_chalk21.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
10456
|
+
console.log(import_chalk21.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
10128
10457
|
}
|
|
10129
10458
|
}
|
|
10130
10459
|
}
|
|
@@ -10145,7 +10474,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10145
10474
|
codegenModel: codegenModelName
|
|
10146
10475
|
});
|
|
10147
10476
|
const runId = generateRunId();
|
|
10148
|
-
console.log(
|
|
10477
|
+
console.log(import_chalk21.default.gray(` Run ID: ${runId}`));
|
|
10149
10478
|
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10150
10479
|
setActiveSnapshot(runSnapshot);
|
|
10151
10480
|
const runLogger = new RunLogger(currentDir, runId, {
|
|
@@ -10155,25 +10484,25 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10155
10484
|
setActiveLogger(runLogger);
|
|
10156
10485
|
const promptHash = computePromptHash();
|
|
10157
10486
|
runLogger.setPromptHash(promptHash);
|
|
10158
|
-
console.log(
|
|
10487
|
+
console.log(import_chalk21.default.blue("[1/6] Loading project context..."));
|
|
10159
10488
|
runLogger.stageStart("context_load");
|
|
10160
10489
|
const loader = new ContextLoader(currentDir);
|
|
10161
10490
|
const context = await loader.loadProjectContext();
|
|
10162
10491
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10163
10492
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10164
|
-
console.log(
|
|
10165
|
-
console.log(
|
|
10166
|
-
console.log(
|
|
10493
|
+
console.log(import_chalk21.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10494
|
+
console.log(import_chalk21.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10495
|
+
console.log(import_chalk21.default.gray(` API files : ${context.apiStructure.length} files`));
|
|
10167
10496
|
if (context.schema) {
|
|
10168
|
-
console.log(
|
|
10497
|
+
console.log(import_chalk21.default.gray(` Prisma schema: found`));
|
|
10169
10498
|
}
|
|
10170
10499
|
if (context.constitution) {
|
|
10171
|
-
console.log(
|
|
10500
|
+
console.log(import_chalk21.default.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
10172
10501
|
if (context.constitution.length > 6e3) {
|
|
10173
|
-
console.log(
|
|
10502
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10174
10503
|
}
|
|
10175
10504
|
} else {
|
|
10176
|
-
console.log(
|
|
10505
|
+
console.log(import_chalk21.default.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
10177
10506
|
try {
|
|
10178
10507
|
const constitutionGen = new ConstitutionGenerator(
|
|
10179
10508
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -10181,12 +10510,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10181
10510
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
10182
10511
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
10183
10512
|
context.constitution = constitutionContent;
|
|
10184
|
-
console.log(
|
|
10513
|
+
console.log(import_chalk21.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
10185
10514
|
} catch (err) {
|
|
10186
|
-
console.log(
|
|
10515
|
+
console.log(import_chalk21.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
10187
10516
|
}
|
|
10188
10517
|
}
|
|
10189
|
-
console.log(
|
|
10518
|
+
console.log(import_chalk21.default.blue(`
|
|
10190
10519
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
10191
10520
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10192
10521
|
let initialSpec;
|
|
@@ -10196,30 +10525,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10196
10525
|
if (opts.skipTasks) {
|
|
10197
10526
|
const generator = new SpecGenerator(specProvider);
|
|
10198
10527
|
initialSpec = await generator.generateSpec(idea, context);
|
|
10199
|
-
console.log(
|
|
10528
|
+
console.log(import_chalk21.default.green(" \u2714 Spec generated."));
|
|
10200
10529
|
} else {
|
|
10201
10530
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
10202
10531
|
initialSpec = result.spec;
|
|
10203
10532
|
initialTasks = result.tasks;
|
|
10204
|
-
console.log(
|
|
10533
|
+
console.log(import_chalk21.default.green(` \u2714 Spec generated.`));
|
|
10205
10534
|
if (initialTasks.length > 0) {
|
|
10206
|
-
console.log(
|
|
10535
|
+
console.log(import_chalk21.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
10207
10536
|
} else {
|
|
10208
|
-
console.log(
|
|
10537
|
+
console.log(import_chalk21.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10209
10538
|
}
|
|
10210
10539
|
}
|
|
10211
10540
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10212
10541
|
} catch (err) {
|
|
10213
10542
|
runLogger.stageFail("spec_gen", err.message);
|
|
10214
|
-
console.error(
|
|
10543
|
+
console.error(import_chalk21.default.red(" \u2718 Spec generation failed:"), err);
|
|
10215
10544
|
process.exit(1);
|
|
10216
10545
|
}
|
|
10217
10546
|
let finalSpec;
|
|
10218
10547
|
if (opts.fast) {
|
|
10219
|
-
console.log(
|
|
10548
|
+
console.log(import_chalk21.default.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
10220
10549
|
finalSpec = initialSpec;
|
|
10221
10550
|
} else {
|
|
10222
|
-
console.log(
|
|
10551
|
+
console.log(import_chalk21.default.blue("\n[3/6] Interactive spec refinement..."));
|
|
10223
10552
|
runLogger.stageStart("spec_refine");
|
|
10224
10553
|
const refiner = new SpecRefiner(specProvider);
|
|
10225
10554
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
@@ -10230,7 +10559,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10230
10559
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
10231
10560
|
if (shouldRunAssessment) {
|
|
10232
10561
|
if (!opts.auto) {
|
|
10233
|
-
console.log(
|
|
10562
|
+
console.log(import_chalk21.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10234
10563
|
}
|
|
10235
10564
|
runLogger.stageStart("spec_assess");
|
|
10236
10565
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
@@ -10239,45 +10568,45 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10239
10568
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10240
10569
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10241
10570
|
if (opts.force) {
|
|
10242
|
-
console.log(
|
|
10571
|
+
console.log(import_chalk21.default.yellow(`
|
|
10243
10572
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10244
10573
|
} else {
|
|
10245
10574
|
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10246
|
-
console.log(
|
|
10575
|
+
console.log(import_chalk21.default.red(`
|
|
10247
10576
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10248
10577
|
if (!opts.auto) {
|
|
10249
|
-
console.log(
|
|
10578
|
+
console.log(import_chalk21.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
10250
10579
|
} else {
|
|
10251
|
-
console.log(
|
|
10580
|
+
console.log(import_chalk21.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
10252
10581
|
}
|
|
10253
|
-
console.log(
|
|
10582
|
+
console.log(import_chalk21.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
10254
10583
|
process.exit(1);
|
|
10255
10584
|
}
|
|
10256
10585
|
}
|
|
10257
10586
|
} else {
|
|
10258
10587
|
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10259
10588
|
if (!opts.auto) {
|
|
10260
|
-
console.log(
|
|
10589
|
+
console.log(import_chalk21.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10261
10590
|
}
|
|
10262
10591
|
}
|
|
10263
10592
|
}
|
|
10264
10593
|
if (!opts.auto) {
|
|
10265
|
-
console.log(
|
|
10594
|
+
console.log(import_chalk21.default.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
10266
10595
|
const specLines = finalSpec.split("\n").length;
|
|
10267
10596
|
const specWords = finalSpec.split(/\s+/).length;
|
|
10268
10597
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10269
|
-
console.log(
|
|
10270
|
-
if (taskCountHint) console.log(
|
|
10271
|
-
const previewSpecsDir =
|
|
10598
|
+
console.log(import_chalk21.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10599
|
+
if (taskCountHint) console.log(import_chalk21.default.gray(taskCountHint));
|
|
10600
|
+
const previewSpecsDir = path23.join(currentDir, "specs");
|
|
10272
10601
|
const slug = featureSlug;
|
|
10273
10602
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10274
10603
|
if (prevVersion) {
|
|
10275
|
-
console.log(
|
|
10604
|
+
console.log(import_chalk21.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
10276
10605
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
10277
|
-
console.log(
|
|
10606
|
+
console.log(import_chalk21.default.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10278
10607
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
10279
10608
|
printDiff(diff);
|
|
10280
|
-
console.log(
|
|
10609
|
+
console.log(import_chalk21.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10281
10610
|
}
|
|
10282
10611
|
const gate = await (0, import_prompts3.select)({
|
|
10283
10612
|
message: "Ready to proceed to code generation?",
|
|
@@ -10288,9 +10617,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10288
10617
|
]
|
|
10289
10618
|
});
|
|
10290
10619
|
if (gate === "view") {
|
|
10291
|
-
console.log(
|
|
10620
|
+
console.log(import_chalk21.default.cyan("\n" + "\u2500".repeat(52)));
|
|
10292
10621
|
console.log(finalSpec);
|
|
10293
|
-
console.log(
|
|
10622
|
+
console.log(import_chalk21.default.cyan("\u2500".repeat(52) + "\n"));
|
|
10294
10623
|
const confirm22 = await (0, import_prompts3.select)({
|
|
10295
10624
|
message: "Proceed to code generation?",
|
|
10296
10625
|
choices: [
|
|
@@ -10299,92 +10628,135 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10299
10628
|
]
|
|
10300
10629
|
});
|
|
10301
10630
|
if (confirm22 === "abort") {
|
|
10302
|
-
console.log(
|
|
10631
|
+
console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10303
10632
|
process.exit(0);
|
|
10304
10633
|
}
|
|
10305
10634
|
} else if (gate === "abort") {
|
|
10306
|
-
console.log(
|
|
10635
|
+
console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10307
10636
|
process.exit(0);
|
|
10308
10637
|
}
|
|
10309
|
-
console.log(
|
|
10638
|
+
console.log(import_chalk21.default.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
10310
10639
|
} else {
|
|
10311
|
-
console.log(
|
|
10640
|
+
console.log(import_chalk21.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
10312
10641
|
}
|
|
10313
10642
|
let extractedDsl = null;
|
|
10314
10643
|
if (opts.skipDsl) {
|
|
10315
|
-
console.log(
|
|
10644
|
+
console.log(import_chalk21.default.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
10316
10645
|
} else {
|
|
10317
|
-
console.log(
|
|
10318
|
-
console.log(
|
|
10646
|
+
console.log(import_chalk21.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10647
|
+
console.log(import_chalk21.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10319
10648
|
runLogger.stageStart("dsl_extract");
|
|
10320
10649
|
try {
|
|
10321
10650
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10322
|
-
if (isFrontend) console.log(
|
|
10651
|
+
if (isFrontend) console.log(import_chalk21.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10323
10652
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10324
10653
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10325
10654
|
if (extractedDsl) {
|
|
10326
10655
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10327
|
-
console.log(
|
|
10656
|
+
console.log(import_chalk21.default.green(" \u2714 DSL extracted and validated."));
|
|
10328
10657
|
} else {
|
|
10329
10658
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10330
|
-
console.log(
|
|
10659
|
+
console.log(import_chalk21.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10331
10660
|
}
|
|
10332
10661
|
} catch (err) {
|
|
10333
10662
|
runLogger.stageFail("dsl_extract", err.message);
|
|
10334
|
-
console.log(
|
|
10663
|
+
console.log(import_chalk21.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10664
|
+
}
|
|
10665
|
+
}
|
|
10666
|
+
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
10667
|
+
const dslGaps = assessDslRichness(extractedDsl);
|
|
10668
|
+
if (dslGaps.length > 0) {
|
|
10669
|
+
printDslGaps(dslGaps);
|
|
10670
|
+
runLogger.stageStart("dsl_gap_feedback", { gapCount: dslGaps.length, gaps: dslGaps.map((g) => g.code) });
|
|
10671
|
+
const refineChoice = await (0, import_prompts3.select)({
|
|
10672
|
+
message: "How would you like to proceed?",
|
|
10673
|
+
choices: [
|
|
10674
|
+
{ name: "\u{1F527} Refine spec (AI fills the gaps, then re-extract DSL)", value: "refine" },
|
|
10675
|
+
{ name: "\u23ED Skip \u2014 proceed with the current DSL", value: "skip" }
|
|
10676
|
+
]
|
|
10677
|
+
});
|
|
10678
|
+
if (refineChoice === "refine") {
|
|
10679
|
+
console.log(import_chalk21.default.blue(" Refining spec to fill DSL gaps..."));
|
|
10680
|
+
try {
|
|
10681
|
+
const refinedSpec = await specProvider.generate(
|
|
10682
|
+
buildDslGapRefinementPrompt(finalSpec, dslGaps),
|
|
10683
|
+
"You are a Senior Tech Lead doing a targeted spec revision. Output only the complete revised Markdown spec."
|
|
10684
|
+
);
|
|
10685
|
+
finalSpec = refinedSpec;
|
|
10686
|
+
console.log(import_chalk21.default.green(" \u2714 Spec refined."));
|
|
10687
|
+
console.log(import_chalk21.default.blue(" Re-extracting DSL from refined spec..."));
|
|
10688
|
+
const isFrontend2 = isFrontendDeps(context.dependencies);
|
|
10689
|
+
const reExtractor = new DslExtractor(specProvider);
|
|
10690
|
+
const reExtractedDsl = await reExtractor.extract(finalSpec, { auto: true, isFrontend: isFrontend2 });
|
|
10691
|
+
if (reExtractedDsl) {
|
|
10692
|
+
extractedDsl = reExtractedDsl;
|
|
10693
|
+
console.log(import_chalk21.default.green(` \u2714 DSL re-extracted: ${extractedDsl.endpoints.length} endpoint(s), ${extractedDsl.models.length} model(s).`));
|
|
10694
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined", endpoints: extractedDsl.endpoints.length, models: extractedDsl.models.length });
|
|
10695
|
+
} else {
|
|
10696
|
+
console.log(import_chalk21.default.yellow(" \u26A0 Re-extraction failed \u2014 keeping original DSL."));
|
|
10697
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined_but_reextract_failed" });
|
|
10698
|
+
}
|
|
10699
|
+
} catch (err) {
|
|
10700
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Spec refinement failed: ${err.message} \u2014 keeping original DSL.`));
|
|
10701
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refinement_error", error: err.message });
|
|
10702
|
+
}
|
|
10703
|
+
} else {
|
|
10704
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "skipped" });
|
|
10705
|
+
console.log(import_chalk21.default.gray(" Continuing with current DSL."));
|
|
10706
|
+
}
|
|
10335
10707
|
}
|
|
10336
10708
|
}
|
|
10337
10709
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
10338
10710
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
10339
10711
|
let workingDir = currentDir;
|
|
10340
10712
|
if (!skipWorktree) {
|
|
10341
|
-
console.log(
|
|
10713
|
+
console.log(import_chalk21.default.blue("\n[4/6] Setting up git worktree..."));
|
|
10342
10714
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
10343
10715
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10344
10716
|
if (worktreePath) workingDir = worktreePath;
|
|
10345
10717
|
} else {
|
|
10346
10718
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10347
|
-
console.log(
|
|
10719
|
+
console.log(import_chalk21.default.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10348
10720
|
}
|
|
10349
|
-
const specsDir =
|
|
10350
|
-
await
|
|
10721
|
+
const specsDir = path23.join(workingDir, "specs");
|
|
10722
|
+
await fs24.ensureDir(specsDir);
|
|
10351
10723
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10352
|
-
await
|
|
10353
|
-
console.log(
|
|
10354
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10724
|
+
await fs24.writeFile(specFile, finalSpec, "utf-8");
|
|
10725
|
+
console.log(import_chalk21.default.green(`
|
|
10726
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + import_chalk21.default.gray(` (v${specVersion})`));
|
|
10355
10727
|
let savedDslFile = null;
|
|
10356
10728
|
if (extractedDsl) {
|
|
10357
10729
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10358
10730
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
10359
|
-
console.log(
|
|
10731
|
+
console.log(import_chalk21.default.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
10360
10732
|
}
|
|
10361
10733
|
if (!opts.skipTasks) {
|
|
10362
10734
|
const taskGen = new TaskGenerator(specProvider);
|
|
10363
10735
|
let tasksToSave = initialTasks;
|
|
10364
10736
|
if (tasksToSave.length === 0) {
|
|
10365
|
-
console.log(
|
|
10737
|
+
console.log(import_chalk21.default.blue(`
|
|
10366
10738
|
Generating tasks (separate call)...`));
|
|
10367
10739
|
try {
|
|
10368
10740
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
10369
10741
|
} catch (err) {
|
|
10370
|
-
console.log(
|
|
10742
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
10371
10743
|
}
|
|
10372
10744
|
}
|
|
10373
10745
|
if (tasksToSave.length > 0) {
|
|
10374
10746
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
10375
10747
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
10376
10748
|
printTasks(sorted);
|
|
10377
|
-
console.log(
|
|
10749
|
+
console.log(import_chalk21.default.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
10378
10750
|
} else {
|
|
10379
|
-
console.log(
|
|
10751
|
+
console.log(import_chalk21.default.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
10380
10752
|
}
|
|
10381
10753
|
}
|
|
10382
|
-
console.log(
|
|
10754
|
+
console.log(import_chalk21.default.blue(`
|
|
10383
10755
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
10384
10756
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10385
10757
|
let generatedTestFiles = [];
|
|
10386
10758
|
if (opts.tdd && extractedDsl) {
|
|
10387
|
-
console.log(
|
|
10759
|
+
console.log(import_chalk21.default.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
10388
10760
|
const testGen = new TestGenerator(codegenProvider);
|
|
10389
10761
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10390
10762
|
}
|
|
@@ -10398,13 +10770,13 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10398
10770
|
});
|
|
10399
10771
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10400
10772
|
if (opts.tdd) {
|
|
10401
|
-
console.log(
|
|
10773
|
+
console.log(import_chalk21.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10402
10774
|
} else if (opts.skipTests) {
|
|
10403
|
-
console.log(
|
|
10775
|
+
console.log(import_chalk21.default.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
10404
10776
|
} else if (!extractedDsl) {
|
|
10405
|
-
console.log(
|
|
10777
|
+
console.log(import_chalk21.default.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
10406
10778
|
} else {
|
|
10407
|
-
console.log(
|
|
10779
|
+
console.log(import_chalk21.default.blue(`
|
|
10408
10780
|
[7/9] Test skeleton generation...`));
|
|
10409
10781
|
runLogger.stageStart("test_gen");
|
|
10410
10782
|
const testGen = new TestGenerator(codegenProvider);
|
|
@@ -10413,11 +10785,11 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10413
10785
|
}
|
|
10414
10786
|
let compilePassed = false;
|
|
10415
10787
|
if (opts.skipErrorFeedback) {
|
|
10416
|
-
console.log(
|
|
10788
|
+
console.log(import_chalk21.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10417
10789
|
compilePassed = true;
|
|
10418
10790
|
} else {
|
|
10419
10791
|
if (opts.tdd) {
|
|
10420
|
-
console.log(
|
|
10792
|
+
console.log(import_chalk21.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10421
10793
|
}
|
|
10422
10794
|
runLogger.stageStart("error_feedback");
|
|
10423
10795
|
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
@@ -10428,10 +10800,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10428
10800
|
}
|
|
10429
10801
|
let reviewResult = "";
|
|
10430
10802
|
if (!opts.skipReview) {
|
|
10431
|
-
console.log(
|
|
10803
|
+
console.log(import_chalk21.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10432
10804
|
runLogger.stageStart("review");
|
|
10433
10805
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10434
|
-
const savedSpec = await
|
|
10806
|
+
const savedSpec = await fs24.readFile(specFile, "utf-8");
|
|
10435
10807
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
10436
10808
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
10437
10809
|
} else {
|
|
@@ -10446,6 +10818,65 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10446
10818
|
runLogger.stageEnd("review");
|
|
10447
10819
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10448
10820
|
}
|
|
10821
|
+
if (reviewResult && !opts.skipReview && !opts.auto && extractedDsl && savedDslFile) {
|
|
10822
|
+
const structuralFindings = extractStructuralFindings(reviewResult);
|
|
10823
|
+
if (structuralFindings.length > 0) {
|
|
10824
|
+
printStructuralFindings(structuralFindings);
|
|
10825
|
+
runLogger.stageStart("review_dsl_feedback", { findingCount: structuralFindings.length, categories: structuralFindings.map((f) => f.category) });
|
|
10826
|
+
const savedSpecContent = await fs24.readFile(specFile, "utf-8");
|
|
10827
|
+
const patchChoice = await (0, import_prompts3.select)({
|
|
10828
|
+
message: "These are design issues in the Spec/DSL. How would you like to handle them?",
|
|
10829
|
+
choices: [
|
|
10830
|
+
{ name: "\u{1F527} Amend spec + update DSL (AI fixes the design issues, no regen yet)", value: "amend" },
|
|
10831
|
+
{ name: "\u{1F4DD} Note in \xA79 only (already done \u2014 no DSL change)", value: "note" },
|
|
10832
|
+
{ name: "\u23ED Skip", value: "skip" }
|
|
10833
|
+
]
|
|
10834
|
+
});
|
|
10835
|
+
if (patchChoice === "amend") {
|
|
10836
|
+
console.log(import_chalk21.default.blue(" Amending spec to address structural findings..."));
|
|
10837
|
+
try {
|
|
10838
|
+
const amendedSpec = await specProvider.generate(
|
|
10839
|
+
buildStructuralAmendmentPrompt(savedSpecContent, structuralFindings),
|
|
10840
|
+
"You are a Senior Tech Lead doing a targeted spec correction. Output only the complete revised Markdown spec."
|
|
10841
|
+
);
|
|
10842
|
+
await runSnapshot.snapshotFile(specFile);
|
|
10843
|
+
if (savedDslFile) await runSnapshot.snapshotFile(savedDslFile);
|
|
10844
|
+
await fs24.writeFile(specFile, amendedSpec, "utf-8");
|
|
10845
|
+
console.log(import_chalk21.default.green(` \u2714 Spec updated: ${specFile}`));
|
|
10846
|
+
console.log(import_chalk21.default.blue(" Re-extracting DSL from amended spec..."));
|
|
10847
|
+
const isFrontend3 = isFrontendDeps(context.dependencies);
|
|
10848
|
+
const amendExtractor = new DslExtractor(specProvider);
|
|
10849
|
+
const amendedDsl = await amendExtractor.extract(amendedSpec, { auto: true, isFrontend: isFrontend3 });
|
|
10850
|
+
if (amendedDsl) {
|
|
10851
|
+
const dslWriter = new DslExtractor(specProvider);
|
|
10852
|
+
const newDslPath = await dslWriter.saveDsl(amendedDsl, specFile);
|
|
10853
|
+
extractedDsl = amendedDsl;
|
|
10854
|
+
console.log(import_chalk21.default.green(` \u2714 DSL updated: ${newDslPath}`));
|
|
10855
|
+
console.log(import_chalk21.default.cyan(
|
|
10856
|
+
`
|
|
10857
|
+
Next step: run ${import_chalk21.default.white("ai-spec update --codegen")} to regenerate files affected by the DSL change.`
|
|
10858
|
+
));
|
|
10859
|
+
runLogger.stageEnd("review_dsl_feedback", {
|
|
10860
|
+
action: "amended",
|
|
10861
|
+
endpoints: amendedDsl.endpoints.length,
|
|
10862
|
+
models: amendedDsl.models.length
|
|
10863
|
+
});
|
|
10864
|
+
} else {
|
|
10865
|
+
console.log(import_chalk21.default.yellow(" \u26A0 DSL re-extraction failed \u2014 spec was updated but DSL file unchanged."));
|
|
10866
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amended_spec_only" });
|
|
10867
|
+
}
|
|
10868
|
+
} catch (err) {
|
|
10869
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Spec amendment failed: ${err.message}`));
|
|
10870
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amendment_error", error: err.message });
|
|
10871
|
+
}
|
|
10872
|
+
} else {
|
|
10873
|
+
runLogger.stageEnd("review_dsl_feedback", { action: patchChoice });
|
|
10874
|
+
if (patchChoice === "note") {
|
|
10875
|
+
console.log(import_chalk21.default.gray(" Structural findings retained in \xA79. DSL unchanged."));
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10449
10880
|
runLogger.stageStart("self_eval");
|
|
10450
10881
|
const selfEvalResult = runSelfEval({
|
|
10451
10882
|
dsl: extractedDsl,
|
|
@@ -10457,19 +10888,19 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10457
10888
|
});
|
|
10458
10889
|
printSelfEval(selfEvalResult);
|
|
10459
10890
|
runLogger.finish();
|
|
10460
|
-
console.log(
|
|
10461
|
-
console.log(
|
|
10462
|
-
if (savedDslFile) console.log(
|
|
10891
|
+
console.log(import_chalk21.default.bold.green("\n\u2714 All done!"));
|
|
10892
|
+
console.log(import_chalk21.default.gray(` Spec : ${specFile}`));
|
|
10893
|
+
if (savedDslFile) console.log(import_chalk21.default.gray(` DSL : ${savedDslFile}`));
|
|
10463
10894
|
if (generatedTestFiles.length > 0) {
|
|
10464
|
-
console.log(
|
|
10895
|
+
console.log(import_chalk21.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
10465
10896
|
}
|
|
10466
|
-
console.log(
|
|
10897
|
+
console.log(import_chalk21.default.gray(` Working dir : ${workingDir}`));
|
|
10467
10898
|
if (workingDir !== currentDir) {
|
|
10468
|
-
console.log(
|
|
10899
|
+
console.log(import_chalk21.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10469
10900
|
}
|
|
10470
10901
|
runLogger.printSummary();
|
|
10471
10902
|
if (runSnapshot.fileCount > 0) {
|
|
10472
|
-
console.log(
|
|
10903
|
+
console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
10473
10904
|
}
|
|
10474
10905
|
});
|
|
10475
10906
|
program.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
|
|
@@ -10486,24 +10917,24 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10486
10917
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
10487
10918
|
let specContent = "";
|
|
10488
10919
|
let resolvedSpecFile;
|
|
10489
|
-
if (specFile && await
|
|
10490
|
-
specContent = await
|
|
10920
|
+
if (specFile && await fs24.pathExists(specFile)) {
|
|
10921
|
+
specContent = await fs24.readFile(specFile, "utf-8");
|
|
10491
10922
|
resolvedSpecFile = specFile;
|
|
10492
|
-
console.log(
|
|
10923
|
+
console.log(import_chalk21.default.gray(`Using spec: ${specFile}`));
|
|
10493
10924
|
} else {
|
|
10494
|
-
const specsDir =
|
|
10495
|
-
if (await
|
|
10496
|
-
const files = (await
|
|
10925
|
+
const specsDir = path23.join(currentDir, "specs");
|
|
10926
|
+
if (await fs24.pathExists(specsDir)) {
|
|
10927
|
+
const files = (await fs24.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
10497
10928
|
if (files.length > 0) {
|
|
10498
|
-
const latest =
|
|
10499
|
-
specContent = await
|
|
10929
|
+
const latest = path23.join(specsDir, files[0]);
|
|
10930
|
+
specContent = await fs24.readFile(latest, "utf-8");
|
|
10500
10931
|
resolvedSpecFile = latest;
|
|
10501
|
-
console.log(
|
|
10932
|
+
console.log(import_chalk21.default.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10502
10933
|
}
|
|
10503
10934
|
}
|
|
10504
10935
|
}
|
|
10505
10936
|
if (!specContent) {
|
|
10506
|
-
console.log(
|
|
10937
|
+
console.log(import_chalk21.default.yellow("No spec file found. Running review without spec context."));
|
|
10507
10938
|
}
|
|
10508
10939
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10509
10940
|
await reviewer.printScoreTrend();
|
|
@@ -10530,15 +10961,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10530
10961
|
auto: opts.auto
|
|
10531
10962
|
});
|
|
10532
10963
|
if (result.written) {
|
|
10533
|
-
console.log(
|
|
10534
|
-
console.log(
|
|
10535
|
-
console.log(
|
|
10964
|
+
console.log(import_chalk21.default.blue("\n Summary:"));
|
|
10965
|
+
console.log(import_chalk21.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10966
|
+
console.log(import_chalk21.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10536
10967
|
if (result.backupPath) {
|
|
10537
|
-
console.log(
|
|
10968
|
+
console.log(import_chalk21.default.gray(` Backup: ${path23.basename(result.backupPath)}`));
|
|
10538
10969
|
}
|
|
10539
10970
|
}
|
|
10540
10971
|
} catch (err) {
|
|
10541
|
-
console.error(
|
|
10972
|
+
console.error(import_chalk21.default.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10542
10973
|
process.exit(1);
|
|
10543
10974
|
}
|
|
10544
10975
|
return;
|
|
@@ -10546,75 +10977,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10546
10977
|
if (opts.global) {
|
|
10547
10978
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10548
10979
|
if (existing && !opts.force) {
|
|
10549
|
-
console.log(
|
|
10980
|
+
console.log(import_chalk21.default.yellow(`
|
|
10550
10981
|
Global constitution already exists at: ${existing.source}`));
|
|
10551
|
-
console.log(
|
|
10982
|
+
console.log(import_chalk21.default.gray(" Use --force to overwrite it."));
|
|
10552
10983
|
return;
|
|
10553
10984
|
}
|
|
10554
|
-
console.log(
|
|
10555
|
-
console.log(
|
|
10556
|
-
console.log(
|
|
10985
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10986
|
+
console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
10987
|
+
console.log(import_chalk21.default.gray(" Scanning repos in workspace..."));
|
|
10557
10988
|
const loader = new ContextLoader(currentDir);
|
|
10558
10989
|
const ctx = await loader.loadProjectContext();
|
|
10559
10990
|
const summary = [
|
|
10560
10991
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10561
10992
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10562
10993
|
].join("\n");
|
|
10563
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10994
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
|
|
10564
10995
|
let globalConstitution;
|
|
10565
10996
|
try {
|
|
10566
10997
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10567
10998
|
} catch (err) {
|
|
10568
|
-
console.error(
|
|
10999
|
+
console.error(import_chalk21.default.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10569
11000
|
process.exit(1);
|
|
10570
11001
|
}
|
|
10571
11002
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10572
|
-
console.log(
|
|
11003
|
+
console.log(import_chalk21.default.green(`
|
|
10573
11004
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10574
|
-
console.log(
|
|
10575
|
-
console.log(
|
|
10576
|
-
console.log(
|
|
10577
|
-
console.log(
|
|
11005
|
+
console.log(import_chalk21.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
11006
|
+
console.log(import_chalk21.default.gray(" Project-level rules always override global rules.\n"));
|
|
11007
|
+
console.log(import_chalk21.default.bold(" Preview:"));
|
|
11008
|
+
console.log(import_chalk21.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10578
11009
|
if (globalConstitution.split("\n").length > 12) {
|
|
10579
|
-
console.log(
|
|
11010
|
+
console.log(import_chalk21.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10580
11011
|
}
|
|
10581
11012
|
return;
|
|
10582
11013
|
}
|
|
10583
|
-
const constitutionPath =
|
|
10584
|
-
if (!opts.force && await
|
|
10585
|
-
console.log(
|
|
11014
|
+
const constitutionPath = path23.join(currentDir, CONSTITUTION_FILE);
|
|
11015
|
+
if (!opts.force && await fs24.pathExists(constitutionPath)) {
|
|
11016
|
+
console.log(import_chalk21.default.yellow(`
|
|
10586
11017
|
${CONSTITUTION_FILE} already exists.`));
|
|
10587
|
-
console.log(
|
|
10588
|
-
console.log(
|
|
11018
|
+
console.log(import_chalk21.default.gray(" Use --force to overwrite it."));
|
|
11019
|
+
console.log(import_chalk21.default.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10589
11020
|
return;
|
|
10590
11021
|
}
|
|
10591
|
-
console.log(
|
|
10592
|
-
console.log(
|
|
10593
|
-
console.log(
|
|
11022
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11023
|
+
console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
11024
|
+
console.log(import_chalk21.default.gray(" Analyzing codebase..."));
|
|
10594
11025
|
const generator = new ConstitutionGenerator(provider);
|
|
10595
11026
|
let constitution;
|
|
10596
11027
|
try {
|
|
10597
11028
|
constitution = await generator.generate(currentDir);
|
|
10598
11029
|
} catch (err) {
|
|
10599
|
-
console.error(
|
|
11030
|
+
console.error(import_chalk21.default.red(" \u2718 Failed to generate constitution:"), err);
|
|
10600
11031
|
process.exit(1);
|
|
10601
11032
|
}
|
|
10602
11033
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10603
|
-
const globalResult = await loadGlobalConstitution([
|
|
11034
|
+
const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
|
|
10604
11035
|
if (globalResult) {
|
|
10605
|
-
console.log(
|
|
11036
|
+
console.log(import_chalk21.default.cyan(`
|
|
10606
11037
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10607
|
-
console.log(
|
|
10608
|
-
console.log(
|
|
11038
|
+
console.log(import_chalk21.default.gray(" It will be merged with this project constitution at runtime."));
|
|
11039
|
+
console.log(import_chalk21.default.gray(" Project rules take priority over global rules."));
|
|
10609
11040
|
}
|
|
10610
|
-
console.log(
|
|
11041
|
+
console.log(import_chalk21.default.green(`
|
|
10611
11042
|
\u2714 Constitution saved: ${saved}`));
|
|
10612
|
-
console.log(
|
|
10613
|
-
console.log(
|
|
10614
|
-
console.log(
|
|
10615
|
-
console.log(
|
|
11043
|
+
console.log(import_chalk21.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
11044
|
+
console.log(import_chalk21.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
11045
|
+
console.log(import_chalk21.default.bold(" Preview:"));
|
|
11046
|
+
console.log(import_chalk21.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10616
11047
|
if (constitution.split("\n").length > 15) {
|
|
10617
|
-
console.log(
|
|
11048
|
+
console.log(import_chalk21.default.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10618
11049
|
}
|
|
10619
11050
|
});
|
|
10620
11051
|
program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
|
|
@@ -10622,44 +11053,44 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10622
11053
|
"Default code generation mode (claude-code|api|plan)"
|
|
10623
11054
|
).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
10624
11055
|
const currentDir = process.cwd();
|
|
10625
|
-
const configPath =
|
|
11056
|
+
const configPath = path23.join(currentDir, CONFIG_FILE);
|
|
10626
11057
|
if (opts.clearKeys) {
|
|
10627
11058
|
await clearAllKeys();
|
|
10628
|
-
console.log(
|
|
11059
|
+
console.log(import_chalk21.default.green(`\u2714 All saved API keys cleared.`));
|
|
10629
11060
|
return;
|
|
10630
11061
|
}
|
|
10631
11062
|
if (opts.clearKey) {
|
|
10632
11063
|
await clearKey(opts.clearKey);
|
|
10633
|
-
console.log(
|
|
11064
|
+
console.log(import_chalk21.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10634
11065
|
return;
|
|
10635
11066
|
}
|
|
10636
11067
|
if (opts.listKeys) {
|
|
10637
|
-
const store = await
|
|
11068
|
+
const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10638
11069
|
const providers = Object.keys(store);
|
|
10639
11070
|
if (providers.length === 0) {
|
|
10640
|
-
console.log(
|
|
11071
|
+
console.log(import_chalk21.default.gray("No saved API keys."));
|
|
10641
11072
|
} else {
|
|
10642
|
-
console.log(
|
|
11073
|
+
console.log(import_chalk21.default.bold("Saved API keys:"));
|
|
10643
11074
|
for (const p of providers) {
|
|
10644
11075
|
const k2 = store[p];
|
|
10645
|
-
console.log(
|
|
11076
|
+
console.log(import_chalk21.default.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10646
11077
|
}
|
|
10647
|
-
console.log(
|
|
11078
|
+
console.log(import_chalk21.default.gray(`
|
|
10648
11079
|
File: ${KEY_STORE_FILE}`));
|
|
10649
11080
|
}
|
|
10650
11081
|
return;
|
|
10651
11082
|
}
|
|
10652
11083
|
if (opts.reset) {
|
|
10653
|
-
await
|
|
10654
|
-
console.log(
|
|
11084
|
+
await fs24.writeJson(configPath, {}, { spaces: 2 });
|
|
11085
|
+
console.log(import_chalk21.default.green(`\u2714 Config reset: ${configPath}`));
|
|
10655
11086
|
return;
|
|
10656
11087
|
}
|
|
10657
11088
|
const existing = await loadConfig(currentDir);
|
|
10658
11089
|
if (opts.show) {
|
|
10659
11090
|
if (Object.keys(existing).length === 0) {
|
|
10660
|
-
console.log(
|
|
11091
|
+
console.log(import_chalk21.default.gray("No config file found. Using built-in defaults."));
|
|
10661
11092
|
} else {
|
|
10662
|
-
console.log(
|
|
11093
|
+
console.log(import_chalk21.default.bold(`${configPath}:`));
|
|
10663
11094
|
console.log(JSON.stringify(existing, null, 2));
|
|
10664
11095
|
}
|
|
10665
11096
|
return;
|
|
@@ -10673,27 +11104,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10673
11104
|
if (opts.minSpecScore !== void 0) {
|
|
10674
11105
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10675
11106
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10676
|
-
console.error(
|
|
11107
|
+
console.error(import_chalk21.default.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10677
11108
|
process.exit(1);
|
|
10678
11109
|
}
|
|
10679
11110
|
updated.minSpecScore = score;
|
|
10680
11111
|
}
|
|
10681
|
-
await
|
|
10682
|
-
console.log(
|
|
11112
|
+
await fs24.writeJson(configPath, updated, { spaces: 2 });
|
|
11113
|
+
console.log(import_chalk21.default.green(`\u2714 Config saved to ${configPath}`));
|
|
10683
11114
|
console.log(JSON.stringify(updated, null, 2));
|
|
10684
11115
|
});
|
|
10685
11116
|
program.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
|
|
10686
11117
|
const currentDir = process.cwd();
|
|
10687
|
-
const configPath =
|
|
11118
|
+
const configPath = path23.join(currentDir, CONFIG_FILE);
|
|
10688
11119
|
if (opts.list) {
|
|
10689
|
-
console.log(
|
|
11120
|
+
console.log(import_chalk21.default.bold("\nAvailable providers & models:\n"));
|
|
10690
11121
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10691
11122
|
console.log(
|
|
10692
|
-
` ${
|
|
11123
|
+
` ${import_chalk21.default.bold.cyan(key.padEnd(10))} ${import_chalk21.default.white(meta.displayName)}`
|
|
10693
11124
|
);
|
|
10694
|
-
console.log(
|
|
11125
|
+
console.log(import_chalk21.default.gray(` ${meta.description}`));
|
|
10695
11126
|
console.log(
|
|
10696
|
-
|
|
11127
|
+
import_chalk21.default.gray(
|
|
10697
11128
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10698
11129
|
)
|
|
10699
11130
|
);
|
|
@@ -10702,10 +11133,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10702
11133
|
return;
|
|
10703
11134
|
}
|
|
10704
11135
|
const existing = await loadConfig(currentDir);
|
|
10705
|
-
console.log(
|
|
11136
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10706
11137
|
if (Object.keys(existing).length > 0) {
|
|
10707
11138
|
console.log(
|
|
10708
|
-
|
|
11139
|
+
import_chalk21.default.gray(
|
|
10709
11140
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10710
11141
|
)
|
|
10711
11142
|
);
|
|
@@ -10723,7 +11154,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10723
11154
|
const providerKey = await (0, import_prompts3.select)({
|
|
10724
11155
|
message: `${label} \u2014 select provider:`,
|
|
10725
11156
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10726
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
11157
|
+
name: `${meta2.displayName.padEnd(22)} ${import_chalk21.default.gray(meta2.description)}`,
|
|
10727
11158
|
value: key,
|
|
10728
11159
|
short: meta2.displayName
|
|
10729
11160
|
}))
|
|
@@ -10731,7 +11162,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10731
11162
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10732
11163
|
const modelChoices = [
|
|
10733
11164
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10734
|
-
{ name:
|
|
11165
|
+
{ name: import_chalk21.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10735
11166
|
];
|
|
10736
11167
|
let chosenModel = await (0, import_prompts3.select)({
|
|
10737
11168
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10765,37 +11196,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10765
11196
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10766
11197
|
updated.codegen = "api";
|
|
10767
11198
|
console.log(
|
|
10768
|
-
|
|
11199
|
+
import_chalk21.default.yellow(
|
|
10769
11200
|
`
|
|
10770
11201
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10771
11202
|
)
|
|
10772
11203
|
);
|
|
10773
|
-
console.log(
|
|
11204
|
+
console.log(import_chalk21.default.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10774
11205
|
}
|
|
10775
11206
|
}
|
|
10776
11207
|
}
|
|
10777
|
-
console.log(
|
|
10778
|
-
console.log(
|
|
11208
|
+
console.log(import_chalk21.default.blue("\n Preview:"));
|
|
11209
|
+
console.log(import_chalk21.default.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10779
11210
|
if (updated.codegenProvider) {
|
|
10780
11211
|
console.log(
|
|
10781
|
-
|
|
11212
|
+
import_chalk21.default.gray(
|
|
10782
11213
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10783
11214
|
)
|
|
10784
11215
|
);
|
|
10785
11216
|
}
|
|
10786
11217
|
const ok = await (0, import_prompts3.confirm)({ message: "Save to .ai-spec.json?", default: true });
|
|
10787
11218
|
if (!ok) {
|
|
10788
|
-
console.log(
|
|
11219
|
+
console.log(import_chalk21.default.gray(" Cancelled."));
|
|
10789
11220
|
return;
|
|
10790
11221
|
}
|
|
10791
|
-
await
|
|
10792
|
-
console.log(
|
|
11222
|
+
await fs24.writeJson(configPath, updated, { spaces: 2 });
|
|
11223
|
+
console.log(import_chalk21.default.green(`
|
|
10793
11224
|
\u2714 Saved to ${configPath}`));
|
|
10794
11225
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10795
11226
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10796
11227
|
if (envKey && !process.env[envKey]) {
|
|
10797
11228
|
console.log(
|
|
10798
|
-
|
|
11229
|
+
import_chalk21.default.yellow(
|
|
10799
11230
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10800
11231
|
)
|
|
10801
11232
|
);
|
|
@@ -10814,29 +11245,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10814
11245
|
cliOpts,
|
|
10815
11246
|
contractContextSection
|
|
10816
11247
|
} = opts;
|
|
10817
|
-
console.log(
|
|
11248
|
+
console.log(import_chalk21.default.blue(`
|
|
10818
11249
|
[${repoName}] Loading project context...`));
|
|
10819
11250
|
const loader = new ContextLoader(repoAbsPath);
|
|
10820
11251
|
let context = await loader.loadProjectContext();
|
|
10821
11252
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10822
|
-
console.log(
|
|
10823
|
-
console.log(
|
|
11253
|
+
console.log(import_chalk21.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
11254
|
+
console.log(import_chalk21.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10824
11255
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10825
|
-
console.log(
|
|
11256
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10826
11257
|
}
|
|
10827
11258
|
if (!context.constitution) {
|
|
10828
|
-
console.log(
|
|
11259
|
+
console.log(import_chalk21.default.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10829
11260
|
try {
|
|
10830
11261
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10831
11262
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10832
11263
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10833
11264
|
context.constitution = constitutionContent;
|
|
10834
|
-
console.log(
|
|
11265
|
+
console.log(import_chalk21.default.green(` Constitution: generated`));
|
|
10835
11266
|
} catch (err) {
|
|
10836
|
-
console.log(
|
|
11267
|
+
console.log(import_chalk21.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10837
11268
|
}
|
|
10838
11269
|
} else {
|
|
10839
|
-
console.log(
|
|
11270
|
+
console.log(import_chalk21.default.green(` Constitution: found`));
|
|
10840
11271
|
}
|
|
10841
11272
|
let fullIdea = idea;
|
|
10842
11273
|
if (contractContextSection) {
|
|
@@ -10844,58 +11275,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10844
11275
|
|
|
10845
11276
|
${contractContextSection}`;
|
|
10846
11277
|
}
|
|
10847
|
-
console.log(
|
|
11278
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Generating spec...`));
|
|
10848
11279
|
let finalSpec;
|
|
10849
11280
|
try {
|
|
10850
11281
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10851
11282
|
finalSpec = result.spec;
|
|
10852
|
-
console.log(
|
|
11283
|
+
console.log(import_chalk21.default.green(` Spec generated.`));
|
|
10853
11284
|
} catch (err) {
|
|
10854
|
-
console.error(
|
|
11285
|
+
console.error(import_chalk21.default.red(` Spec generation failed: ${err.message}`));
|
|
10855
11286
|
return { dsl: null, specFile: null };
|
|
10856
11287
|
}
|
|
10857
11288
|
let extractedDsl = null;
|
|
10858
11289
|
if (!cliOpts.skipDsl) {
|
|
10859
|
-
console.log(
|
|
11290
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Extracting DSL...`));
|
|
10860
11291
|
try {
|
|
10861
11292
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10862
11293
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10863
11294
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10864
11295
|
if (extractedDsl) {
|
|
10865
|
-
console.log(
|
|
11296
|
+
console.log(import_chalk21.default.green(` DSL extracted.`));
|
|
10866
11297
|
}
|
|
10867
11298
|
} catch (err) {
|
|
10868
|
-
console.log(
|
|
11299
|
+
console.log(import_chalk21.default.yellow(` DSL extraction failed: ${err.message}`));
|
|
10869
11300
|
}
|
|
10870
11301
|
}
|
|
10871
11302
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10872
11303
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10873
11304
|
let workingDir = repoAbsPath;
|
|
10874
11305
|
if (!skipWorktreeForRepo) {
|
|
10875
|
-
console.log(
|
|
11306
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Setting up git worktree...`));
|
|
10876
11307
|
try {
|
|
10877
11308
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10878
11309
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10879
11310
|
if (worktreePath) workingDir = worktreePath;
|
|
10880
11311
|
} catch (err) {
|
|
10881
|
-
console.log(
|
|
11312
|
+
console.log(import_chalk21.default.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10882
11313
|
}
|
|
10883
11314
|
} else {
|
|
10884
|
-
console.log(
|
|
11315
|
+
console.log(import_chalk21.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10885
11316
|
}
|
|
10886
|
-
const specsDir =
|
|
10887
|
-
await
|
|
11317
|
+
const specsDir = path23.join(workingDir, "specs");
|
|
11318
|
+
await fs24.ensureDir(specsDir);
|
|
10888
11319
|
const featureSlug = slugify(idea);
|
|
10889
11320
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10890
|
-
await
|
|
10891
|
-
console.log(
|
|
11321
|
+
await fs24.writeFile(specFile, finalSpec, "utf-8");
|
|
11322
|
+
console.log(import_chalk21.default.green(` Spec saved: ${path23.relative(repoAbsPath, specFile)}`));
|
|
10892
11323
|
let savedDslFile = null;
|
|
10893
11324
|
if (extractedDsl) {
|
|
10894
11325
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10895
11326
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10896
|
-
console.log(
|
|
11327
|
+
console.log(import_chalk21.default.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
|
|
10897
11328
|
}
|
|
10898
|
-
console.log(
|
|
11329
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10899
11330
|
try {
|
|
10900
11331
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10901
11332
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10903,29 +11334,29 @@ ${contractContextSection}`;
|
|
|
10903
11334
|
dslFilePath: savedDslFile ?? void 0,
|
|
10904
11335
|
repoType: detectedRepoType
|
|
10905
11336
|
});
|
|
10906
|
-
console.log(
|
|
11337
|
+
console.log(import_chalk21.default.green(` Code generation complete.`));
|
|
10907
11338
|
} catch (err) {
|
|
10908
|
-
console.log(
|
|
11339
|
+
console.log(import_chalk21.default.yellow(` Code generation failed: ${err.message}`));
|
|
10909
11340
|
}
|
|
10910
11341
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10911
|
-
console.log(
|
|
11342
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Generating test skeletons...`));
|
|
10912
11343
|
try {
|
|
10913
11344
|
const testGen = new TestGenerator(codegenProvider);
|
|
10914
11345
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10915
|
-
console.log(
|
|
11346
|
+
console.log(import_chalk21.default.green(` ${testFiles.length} test file(s) generated.`));
|
|
10916
11347
|
} catch (err) {
|
|
10917
|
-
console.log(
|
|
11348
|
+
console.log(import_chalk21.default.yellow(` Test generation failed: ${err.message}`));
|
|
10918
11349
|
}
|
|
10919
11350
|
}
|
|
10920
11351
|
if (!cliOpts.skipErrorFeedback) {
|
|
10921
11352
|
try {
|
|
10922
11353
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10923
11354
|
} catch (err) {
|
|
10924
|
-
console.log(
|
|
11355
|
+
console.log(import_chalk21.default.yellow(` Error feedback failed: ${err.message}`));
|
|
10925
11356
|
}
|
|
10926
11357
|
}
|
|
10927
11358
|
if (!cliOpts.skipReview) {
|
|
10928
|
-
console.log(
|
|
11359
|
+
console.log(import_chalk21.default.blue(` [${repoName}] Running code review...`));
|
|
10929
11360
|
try {
|
|
10930
11361
|
const reviewer = new CodeReviewer(specProvider);
|
|
10931
11362
|
const originalDir = process.cwd();
|
|
@@ -10937,9 +11368,9 @@ ${contractContextSection}`;
|
|
|
10937
11368
|
process.chdir(originalDir);
|
|
10938
11369
|
}
|
|
10939
11370
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10940
|
-
console.log(
|
|
11371
|
+
console.log(import_chalk21.default.green(` Code review complete.`));
|
|
10941
11372
|
} catch (err) {
|
|
10942
|
-
console.log(
|
|
11373
|
+
console.log(import_chalk21.default.yellow(` Code review failed: ${err.message}`));
|
|
10943
11374
|
}
|
|
10944
11375
|
}
|
|
10945
11376
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10962,7 +11393,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10962
11393
|
codegenModel: codegenModelName
|
|
10963
11394
|
});
|
|
10964
11395
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10965
|
-
console.log(
|
|
11396
|
+
console.log(import_chalk21.default.blue("\n[W1] Loading per-repo contexts..."));
|
|
10966
11397
|
const contexts = /* @__PURE__ */ new Map();
|
|
10967
11398
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10968
11399
|
for (const repo of workspace.repos) {
|
|
@@ -10974,27 +11405,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10974
11405
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10975
11406
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10976
11407
|
frontendContexts.set(repo.name, fctx);
|
|
10977
|
-
console.log(
|
|
11408
|
+
console.log(import_chalk21.default.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10978
11409
|
} else {
|
|
10979
|
-
console.log(
|
|
11410
|
+
console.log(import_chalk21.default.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10980
11411
|
}
|
|
10981
11412
|
} catch (err) {
|
|
10982
|
-
console.log(
|
|
11413
|
+
console.log(import_chalk21.default.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10983
11414
|
}
|
|
10984
11415
|
}
|
|
10985
|
-
console.log(
|
|
11416
|
+
console.log(import_chalk21.default.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10986
11417
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10987
11418
|
let decomposition;
|
|
10988
11419
|
try {
|
|
10989
11420
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10990
|
-
console.log(
|
|
10991
|
-
console.log(
|
|
11421
|
+
console.log(import_chalk21.default.green(` Summary: ${decomposition.summary}`));
|
|
11422
|
+
console.log(import_chalk21.default.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10992
11423
|
if (decomposition.coordinationNotes) {
|
|
10993
|
-
console.log(
|
|
11424
|
+
console.log(import_chalk21.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10994
11425
|
}
|
|
10995
11426
|
} catch (err) {
|
|
10996
|
-
console.error(
|
|
10997
|
-
console.log(
|
|
11427
|
+
console.error(import_chalk21.default.red(` Decomposition failed: ${err.message}`));
|
|
11428
|
+
console.log(import_chalk21.default.yellow(" Falling back to running all repos independently."));
|
|
10998
11429
|
decomposition = {
|
|
10999
11430
|
originalRequirement: idea,
|
|
11000
11431
|
summary: idea,
|
|
@@ -11010,11 +11441,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11010
11441
|
};
|
|
11011
11442
|
}
|
|
11012
11443
|
if (!opts.auto) {
|
|
11013
|
-
console.log(
|
|
11014
|
-
console.log(
|
|
11444
|
+
console.log(import_chalk21.default.cyan("\n[W3] Decomposition Preview:"));
|
|
11445
|
+
console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
|
|
11015
11446
|
for (const r of decomposition.repos) {
|
|
11016
|
-
console.log(
|
|
11017
|
-
console.log(
|
|
11447
|
+
console.log(import_chalk21.default.bold(` ${r.repoName} (${r.role})`));
|
|
11448
|
+
console.log(import_chalk21.default.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
11018
11449
|
if (r.uxDecisions) {
|
|
11019
11450
|
const ux = r.uxDecisions;
|
|
11020
11451
|
const uxSummary = [
|
|
@@ -11023,13 +11454,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11023
11454
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
11024
11455
|
ux.errorRollback ? "rollback" : ""
|
|
11025
11456
|
].filter(Boolean).join(", ");
|
|
11026
|
-
if (uxSummary) console.log(
|
|
11457
|
+
if (uxSummary) console.log(import_chalk21.default.cyan(` UX: ${uxSummary}`));
|
|
11027
11458
|
}
|
|
11028
11459
|
if (r.dependsOnRepos.length > 0) {
|
|
11029
|
-
console.log(
|
|
11460
|
+
console.log(import_chalk21.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
11030
11461
|
}
|
|
11031
11462
|
}
|
|
11032
|
-
console.log(
|
|
11463
|
+
console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
|
|
11033
11464
|
const gate = await (0, import_prompts3.select)({
|
|
11034
11465
|
message: "Proceed with multi-repo pipeline?",
|
|
11035
11466
|
choices: [
|
|
@@ -11038,24 +11469,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11038
11469
|
]
|
|
11039
11470
|
});
|
|
11040
11471
|
if (gate === "abort") {
|
|
11041
|
-
console.log(
|
|
11472
|
+
console.log(import_chalk21.default.yellow(" Aborted."));
|
|
11042
11473
|
process.exit(0);
|
|
11043
11474
|
}
|
|
11044
11475
|
}
|
|
11045
11476
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
11046
11477
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
11047
|
-
console.log(
|
|
11478
|
+
console.log(import_chalk21.default.blue(`
|
|
11048
11479
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
11049
11480
|
const results = [];
|
|
11050
11481
|
for (const repoReq of sortedRepoRequirements) {
|
|
11051
11482
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
11052
11483
|
if (!repoConfig) {
|
|
11053
|
-
console.log(
|
|
11484
|
+
console.log(import_chalk21.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
11054
11485
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
11055
11486
|
continue;
|
|
11056
11487
|
}
|
|
11057
11488
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
11058
|
-
console.log(
|
|
11489
|
+
console.log(import_chalk21.default.bold.blue(`
|
|
11059
11490
|
\u2500\u2500 ${repoReq.repoName} (${repoReq.role}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
11060
11491
|
let contractContextSection;
|
|
11061
11492
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -11063,7 +11494,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11063
11494
|
for (const depName of repoReq.dependsOnRepos) {
|
|
11064
11495
|
const depDsl = contractDsls.get(depName);
|
|
11065
11496
|
if (depDsl) {
|
|
11066
|
-
console.log(
|
|
11497
|
+
console.log(import_chalk21.default.gray(` Using API contract from: ${depName}`));
|
|
11067
11498
|
const contract = buildFrontendApiContract(depDsl);
|
|
11068
11499
|
contractParts.push(buildContractContextSection(contract));
|
|
11069
11500
|
}
|
|
@@ -11083,7 +11514,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11083
11514
|
frontendContext: frontendCtx
|
|
11084
11515
|
});
|
|
11085
11516
|
contractContextSection = void 0;
|
|
11086
|
-
console.log(
|
|
11517
|
+
console.log(import_chalk21.default.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
11087
11518
|
}
|
|
11088
11519
|
try {
|
|
11089
11520
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -11100,22 +11531,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11100
11531
|
});
|
|
11101
11532
|
if (repoReq.isContractProvider && dsl) {
|
|
11102
11533
|
contractDsls.set(repoReq.repoName, dsl);
|
|
11103
|
-
console.log(
|
|
11534
|
+
console.log(import_chalk21.default.green(` Contract stored for downstream repos.`));
|
|
11104
11535
|
}
|
|
11105
11536
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
11106
|
-
console.log(
|
|
11537
|
+
console.log(import_chalk21.default.green(` \u2714 ${repoReq.repoName} complete`));
|
|
11107
11538
|
} catch (err) {
|
|
11108
|
-
console.error(
|
|
11539
|
+
console.error(import_chalk21.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
11109
11540
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
11110
11541
|
}
|
|
11111
11542
|
}
|
|
11112
|
-
console.log(
|
|
11113
|
-
console.log(
|
|
11114
|
-
console.log(
|
|
11543
|
+
console.log(import_chalk21.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
11544
|
+
console.log(import_chalk21.default.gray(` Workspace: ${workspace.name}`));
|
|
11545
|
+
console.log(import_chalk21.default.gray(` Requirement: ${idea}`));
|
|
11115
11546
|
console.log();
|
|
11116
11547
|
for (const r of results) {
|
|
11117
|
-
const icon = r.status === "success" ?
|
|
11118
|
-
const specInfo = r.specFile ?
|
|
11548
|
+
const icon = r.status === "success" ? import_chalk21.default.green("\u2714") : r.status === "failed" ? import_chalk21.default.red("\u2718") : import_chalk21.default.gray("\u2212");
|
|
11549
|
+
const specInfo = r.specFile ? import_chalk21.default.gray(` \u2192 ${r.specFile}`) : "";
|
|
11119
11550
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
11120
11551
|
}
|
|
11121
11552
|
return results;
|
|
@@ -11123,18 +11554,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11123
11554
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
11124
11555
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
11125
11556
|
const currentDir = process.cwd();
|
|
11126
|
-
const configPath =
|
|
11127
|
-
if (await
|
|
11557
|
+
const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
11558
|
+
if (await fs24.pathExists(configPath)) {
|
|
11128
11559
|
const overwrite = await (0, import_prompts3.confirm)({
|
|
11129
11560
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
11130
11561
|
default: false
|
|
11131
11562
|
});
|
|
11132
11563
|
if (!overwrite) {
|
|
11133
|
-
console.log(
|
|
11564
|
+
console.log(import_chalk21.default.gray(" Cancelled."));
|
|
11134
11565
|
return;
|
|
11135
11566
|
}
|
|
11136
11567
|
}
|
|
11137
|
-
console.log(
|
|
11568
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11138
11569
|
const workspaceName = await (0, import_prompts3.input)({
|
|
11139
11570
|
message: "Workspace name:",
|
|
11140
11571
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -11148,11 +11579,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11148
11579
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11149
11580
|
const detected = await workspaceLoader.autoDetect();
|
|
11150
11581
|
if (detected.length === 0) {
|
|
11151
|
-
console.log(
|
|
11582
|
+
console.log(import_chalk21.default.yellow(" No recognizable repos found in sibling directories."));
|
|
11152
11583
|
} else {
|
|
11153
|
-
console.log(
|
|
11584
|
+
console.log(import_chalk21.default.cyan("\n Detected repos:"));
|
|
11154
11585
|
for (const r of detected) {
|
|
11155
|
-
console.log(
|
|
11586
|
+
console.log(import_chalk21.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11156
11587
|
}
|
|
11157
11588
|
const keepAll = await (0, import_prompts3.confirm)({
|
|
11158
11589
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -11169,7 +11600,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11169
11600
|
if (keep) repos.push(r);
|
|
11170
11601
|
}
|
|
11171
11602
|
}
|
|
11172
|
-
console.log(
|
|
11603
|
+
console.log(import_chalk21.default.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
11173
11604
|
}
|
|
11174
11605
|
}
|
|
11175
11606
|
const repoTypeChoices = [
|
|
@@ -11191,7 +11622,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11191
11622
|
default: repos.length === 0
|
|
11192
11623
|
});
|
|
11193
11624
|
while (addMore) {
|
|
11194
|
-
console.log(
|
|
11625
|
+
console.log(import_chalk21.default.cyan(`
|
|
11195
11626
|
Adding repo #${repos.length + 1}`));
|
|
11196
11627
|
const repoName = await (0, import_prompts3.input)({
|
|
11197
11628
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -11205,16 +11636,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11205
11636
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
11206
11637
|
default: `./${repoName}`
|
|
11207
11638
|
});
|
|
11208
|
-
const absPath =
|
|
11639
|
+
const absPath = path23.resolve(currentDir, repoPath);
|
|
11209
11640
|
let detectedType = "unknown";
|
|
11210
11641
|
let detectedRole = "shared";
|
|
11211
|
-
if (await
|
|
11642
|
+
if (await fs24.pathExists(absPath)) {
|
|
11212
11643
|
const { type, role } = await detectRepoType(absPath);
|
|
11213
11644
|
detectedType = type;
|
|
11214
11645
|
detectedRole = role;
|
|
11215
|
-
console.log(
|
|
11646
|
+
console.log(import_chalk21.default.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
11216
11647
|
} else {
|
|
11217
|
-
console.log(
|
|
11648
|
+
console.log(import_chalk21.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
11218
11649
|
}
|
|
11219
11650
|
const repoType = await (0, import_prompts3.select)({
|
|
11220
11651
|
message: `Repo type for "${repoName}":`,
|
|
@@ -11237,53 +11668,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11237
11668
|
type: repoType,
|
|
11238
11669
|
role: repoRole
|
|
11239
11670
|
});
|
|
11240
|
-
console.log(
|
|
11671
|
+
console.log(import_chalk21.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
11241
11672
|
addMore = await (0, import_prompts3.confirm)({
|
|
11242
11673
|
message: "Add another repo?",
|
|
11243
11674
|
default: false
|
|
11244
11675
|
});
|
|
11245
11676
|
}
|
|
11246
11677
|
const workspaceConfig = { name: workspaceName, repos };
|
|
11247
|
-
console.log(
|
|
11248
|
-
console.log(
|
|
11678
|
+
console.log(import_chalk21.default.cyan("\n Workspace summary:"));
|
|
11679
|
+
console.log(import_chalk21.default.gray(` Name: ${workspaceName}`));
|
|
11249
11680
|
for (const r of repos) {
|
|
11250
|
-
console.log(
|
|
11681
|
+
console.log(import_chalk21.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11251
11682
|
}
|
|
11252
11683
|
const ok = await (0, import_prompts3.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
11253
11684
|
if (!ok) {
|
|
11254
|
-
console.log(
|
|
11685
|
+
console.log(import_chalk21.default.gray(" Cancelled."));
|
|
11255
11686
|
return;
|
|
11256
11687
|
}
|
|
11257
11688
|
const loader = new WorkspaceLoader(currentDir);
|
|
11258
11689
|
const saved = await loader.save(workspaceConfig);
|
|
11259
|
-
console.log(
|
|
11690
|
+
console.log(import_chalk21.default.green(`
|
|
11260
11691
|
\u2714 Workspace saved: ${saved}`));
|
|
11261
|
-
console.log(
|
|
11692
|
+
console.log(import_chalk21.default.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
11262
11693
|
});
|
|
11263
11694
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
11264
11695
|
const currentDir = process.cwd();
|
|
11265
11696
|
const loader = new WorkspaceLoader(currentDir);
|
|
11266
11697
|
const config2 = await loader.load();
|
|
11267
11698
|
if (!config2) {
|
|
11268
|
-
console.log(
|
|
11269
|
-
console.log(
|
|
11699
|
+
console.log(import_chalk21.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11700
|
+
console.log(import_chalk21.default.gray(" Run `ai-spec workspace init` to create one."));
|
|
11270
11701
|
return;
|
|
11271
11702
|
}
|
|
11272
|
-
console.log(
|
|
11703
|
+
console.log(import_chalk21.default.bold(`
|
|
11273
11704
|
Workspace: ${config2.name}`));
|
|
11274
|
-
console.log(
|
|
11275
|
-
console.log(
|
|
11705
|
+
console.log(import_chalk21.default.gray(` Config: ${path23.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11706
|
+
console.log(import_chalk21.default.gray(` Repos (${config2.repos.length}):
|
|
11276
11707
|
`));
|
|
11277
11708
|
for (const repo of config2.repos) {
|
|
11278
11709
|
const absPath = loader.resolveAbsPath(repo);
|
|
11279
|
-
const exists = await
|
|
11280
|
-
const status = exists ?
|
|
11710
|
+
const exists = await fs24.pathExists(absPath);
|
|
11711
|
+
const status = exists ? import_chalk21.default.green("found") : import_chalk21.default.red("not found");
|
|
11281
11712
|
console.log(
|
|
11282
|
-
` ${
|
|
11713
|
+
` ${import_chalk21.default.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
11283
11714
|
);
|
|
11284
|
-
console.log(
|
|
11715
|
+
console.log(import_chalk21.default.gray(` path: ${absPath}`));
|
|
11285
11716
|
if (repo.constitution) {
|
|
11286
|
-
console.log(
|
|
11717
|
+
console.log(import_chalk21.default.green(` constitution: found`));
|
|
11287
11718
|
}
|
|
11288
11719
|
}
|
|
11289
11720
|
});
|
|
@@ -11300,30 +11731,30 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11300
11731
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
11301
11732
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
11302
11733
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11303
|
-
console.log(
|
|
11304
|
-
console.log(
|
|
11734
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11735
|
+
console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
11305
11736
|
const updateRunId = generateRunId();
|
|
11306
11737
|
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11307
11738
|
setActiveSnapshot(updateSnapshot);
|
|
11308
11739
|
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11309
11740
|
setActiveLogger(updateLogger);
|
|
11310
|
-
console.log(
|
|
11741
|
+
console.log(import_chalk21.default.gray(` Run ID: ${updateRunId}`));
|
|
11311
11742
|
let specPath = opts.spec ?? null;
|
|
11312
11743
|
if (!specPath) {
|
|
11313
|
-
const specsDir =
|
|
11744
|
+
const specsDir = path23.join(currentDir, "specs");
|
|
11314
11745
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11315
11746
|
if (!latest) {
|
|
11316
|
-
console.error(
|
|
11747
|
+
console.error(import_chalk21.default.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11317
11748
|
process.exit(1);
|
|
11318
11749
|
}
|
|
11319
11750
|
specPath = latest.filePath;
|
|
11320
|
-
console.log(
|
|
11751
|
+
console.log(import_chalk21.default.gray(` Using spec: ${path23.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11321
11752
|
}
|
|
11322
|
-
console.log(
|
|
11753
|
+
console.log(import_chalk21.default.gray(" Loading project context..."));
|
|
11323
11754
|
const loader = new ContextLoader(currentDir);
|
|
11324
11755
|
const context = await loader.loadProjectContext();
|
|
11325
11756
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
11326
|
-
console.log(
|
|
11757
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11327
11758
|
}
|
|
11328
11759
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
11329
11760
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -11335,19 +11766,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11335
11766
|
repoType
|
|
11336
11767
|
});
|
|
11337
11768
|
} catch (err) {
|
|
11338
|
-
console.error(
|
|
11769
|
+
console.error(import_chalk21.default.red(` Update failed: ${err.message}`));
|
|
11339
11770
|
process.exit(1);
|
|
11340
11771
|
}
|
|
11341
|
-
console.log(
|
|
11342
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11772
|
+
console.log(import_chalk21.default.green(`
|
|
11773
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path23.relative(currentDir, result.newSpecPath)}`));
|
|
11343
11774
|
if (result.newDslPath) {
|
|
11344
|
-
console.log(
|
|
11775
|
+
console.log(import_chalk21.default.green(` \u2714 DSL updated: ${path23.relative(currentDir, result.newDslPath)}`));
|
|
11345
11776
|
}
|
|
11346
11777
|
if (result.affectedFiles.length > 0) {
|
|
11347
|
-
console.log(
|
|
11778
|
+
console.log(import_chalk21.default.cyan("\n Affected files:"));
|
|
11348
11779
|
for (const f of result.affectedFiles) {
|
|
11349
|
-
const icon = f.action === "create" ?
|
|
11350
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11780
|
+
const icon = f.action === "create" ? import_chalk21.default.green("+") : import_chalk21.default.yellow("~");
|
|
11781
|
+
console.log(` ${icon} ${f.file}: ${import_chalk21.default.gray(f.description)}`);
|
|
11351
11782
|
}
|
|
11352
11783
|
}
|
|
11353
11784
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -11355,9 +11786,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11355
11786
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
11356
11787
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
11357
11788
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11358
|
-
console.log(
|
|
11789
|
+
console.log(import_chalk21.default.blue("\n Regenerating affected files..."));
|
|
11359
11790
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11360
|
-
const specContent = await
|
|
11791
|
+
const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
|
|
11361
11792
|
const constitutionSection = context.constitution ? `
|
|
11362
11793
|
=== Project Constitution (MUST follow) ===
|
|
11363
11794
|
${context.constitution}
|
|
@@ -11368,10 +11799,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
|
11368
11799
|
` : "";
|
|
11369
11800
|
updateLogger.stageStart("update_codegen");
|
|
11370
11801
|
for (const affected of result.affectedFiles) {
|
|
11371
|
-
const fullPath =
|
|
11802
|
+
const fullPath = path23.join(currentDir, affected.file);
|
|
11372
11803
|
let existing = "";
|
|
11373
11804
|
try {
|
|
11374
|
-
existing = await
|
|
11805
|
+
existing = await fs24.readFile(fullPath, "utf-8");
|
|
11375
11806
|
} catch {
|
|
11376
11807
|
}
|
|
11377
11808
|
const codePrompt = `Apply this change to the file.
|
|
@@ -11385,23 +11816,23 @@ ${specContent}
|
|
|
11385
11816
|
${constitutionSection}${dslSection}
|
|
11386
11817
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
11387
11818
|
${existing || "Create from scratch."}`;
|
|
11388
|
-
process.stdout.write(` ${existing ?
|
|
11819
|
+
process.stdout.write(` ${existing ? import_chalk21.default.yellow("~") : import_chalk21.default.green("+")} ${affected.file}... `);
|
|
11389
11820
|
try {
|
|
11390
11821
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11391
11822
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11392
11823
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11393
|
-
await
|
|
11824
|
+
await fs24.ensureDir(path23.dirname(fullPath));
|
|
11394
11825
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11395
|
-
await
|
|
11826
|
+
await fs24.writeFile(fullPath, content, "utf-8");
|
|
11396
11827
|
updateLogger.fileWritten(affected.file);
|
|
11397
|
-
console.log(
|
|
11828
|
+
console.log(import_chalk21.default.green("\u2714"));
|
|
11398
11829
|
} catch (err) {
|
|
11399
11830
|
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11400
|
-
console.log(
|
|
11831
|
+
console.log(import_chalk21.default.red(`\u2718 ${err.message}`));
|
|
11401
11832
|
}
|
|
11402
11833
|
}
|
|
11403
11834
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11404
|
-
const updatedSpecContent = await
|
|
11835
|
+
const updatedSpecContent = await fs24.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11405
11836
|
if (updatedSpecContent) {
|
|
11406
11837
|
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11407
11838
|
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
@@ -11413,13 +11844,13 @@ ${existing || "Create from scratch."}`;
|
|
|
11413
11844
|
updateLogger.finish();
|
|
11414
11845
|
updateLogger.printSummary();
|
|
11415
11846
|
if (updateSnapshot.fileCount > 0) {
|
|
11416
|
-
console.log(
|
|
11847
|
+
console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11417
11848
|
}
|
|
11418
11849
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11419
|
-
console.log(
|
|
11420
|
-
console.log(
|
|
11421
|
-
console.log(
|
|
11422
|
-
console.log(
|
|
11850
|
+
console.log(import_chalk21.default.blue("\n Next steps:"));
|
|
11851
|
+
console.log(import_chalk21.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11852
|
+
console.log(import_chalk21.default.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11853
|
+
console.log(import_chalk21.default.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
11423
11854
|
}
|
|
11424
11855
|
});
|
|
11425
11856
|
program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)").option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)").option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml").option("--output <path>", "Output file path (default: openapi.yaml)").option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").action(async (opts) => {
|
|
@@ -11428,19 +11859,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11428
11859
|
if (!dslPath) {
|
|
11429
11860
|
dslPath = await findLatestDslFile(currentDir);
|
|
11430
11861
|
if (!dslPath) {
|
|
11431
|
-
console.error(
|
|
11862
|
+
console.error(import_chalk21.default.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11432
11863
|
process.exit(1);
|
|
11433
11864
|
}
|
|
11434
|
-
console.log(
|
|
11865
|
+
console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
|
|
11435
11866
|
}
|
|
11436
11867
|
let dsl;
|
|
11437
11868
|
try {
|
|
11438
|
-
dsl = await
|
|
11869
|
+
dsl = await fs24.readJson(dslPath);
|
|
11439
11870
|
} catch (err) {
|
|
11440
|
-
console.error(
|
|
11871
|
+
console.error(import_chalk21.default.red(` Failed to read DSL: ${err.message}`));
|
|
11441
11872
|
process.exit(1);
|
|
11442
11873
|
}
|
|
11443
|
-
console.log(
|
|
11874
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11444
11875
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
11445
11876
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
11446
11877
|
try {
|
|
@@ -11449,31 +11880,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11449
11880
|
serverUrl,
|
|
11450
11881
|
outputPath: opts.output
|
|
11451
11882
|
});
|
|
11452
|
-
const rel =
|
|
11453
|
-
console.log(
|
|
11454
|
-
console.log(
|
|
11455
|
-
console.log(
|
|
11456
|
-
console.log(
|
|
11457
|
-
console.log(
|
|
11458
|
-
console.log(
|
|
11459
|
-
console.log(
|
|
11460
|
-
console.log(
|
|
11883
|
+
const rel = path23.relative(currentDir, outputPath);
|
|
11884
|
+
console.log(import_chalk21.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11885
|
+
console.log(import_chalk21.default.gray(` Feature : ${dsl.feature.title}`));
|
|
11886
|
+
console.log(import_chalk21.default.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11887
|
+
console.log(import_chalk21.default.gray(` Models : ${dsl.models.length}`));
|
|
11888
|
+
console.log(import_chalk21.default.gray(` Server : ${serverUrl}`));
|
|
11889
|
+
console.log(import_chalk21.default.blue("\n Next steps:"));
|
|
11890
|
+
console.log(import_chalk21.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11891
|
+
console.log(import_chalk21.default.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
11461
11892
|
} catch (err) {
|
|
11462
|
-
console.error(
|
|
11893
|
+
console.error(import_chalk21.default.red(` Export failed: ${err.message}`));
|
|
11463
11894
|
process.exit(1);
|
|
11464
11895
|
}
|
|
11465
11896
|
});
|
|
11466
11897
|
program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
|
|
11467
11898
|
const currentDir = process.cwd();
|
|
11468
11899
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11469
|
-
console.log(
|
|
11900
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11470
11901
|
if (opts.restore) {
|
|
11471
|
-
const frontendDir = opts.frontend ?
|
|
11902
|
+
const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
|
|
11472
11903
|
const r = await restoreMockProxy(frontendDir);
|
|
11473
11904
|
if (r.restored) {
|
|
11474
|
-
console.log(
|
|
11905
|
+
console.log(import_chalk21.default.green(" \u2714 Proxy restored and mock server stopped."));
|
|
11475
11906
|
} else {
|
|
11476
|
-
console.log(
|
|
11907
|
+
console.log(import_chalk21.default.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
11477
11908
|
}
|
|
11478
11909
|
return;
|
|
11479
11910
|
}
|
|
@@ -11481,32 +11912,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11481
11912
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11482
11913
|
const workspaceConfig = await workspaceLoader.load();
|
|
11483
11914
|
if (!workspaceConfig) {
|
|
11484
|
-
console.error(
|
|
11915
|
+
console.error(import_chalk21.default.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
11485
11916
|
process.exit(1);
|
|
11486
11917
|
}
|
|
11487
11918
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
11488
11919
|
if (backendRepos.length === 0) {
|
|
11489
|
-
console.log(
|
|
11920
|
+
console.log(import_chalk21.default.yellow(" No backend repos found in workspace."));
|
|
11490
11921
|
return;
|
|
11491
11922
|
}
|
|
11492
11923
|
for (const repo of backendRepos) {
|
|
11493
11924
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
11494
|
-
console.log(
|
|
11925
|
+
console.log(import_chalk21.default.cyan(`
|
|
11495
11926
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
11496
11927
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
11497
11928
|
if (!dslFile) {
|
|
11498
|
-
console.log(
|
|
11929
|
+
console.log(import_chalk21.default.yellow(` No DSL file found \u2014 skipping.`));
|
|
11499
11930
|
continue;
|
|
11500
11931
|
}
|
|
11501
|
-
const dsl2 = await
|
|
11932
|
+
const dsl2 = await fs24.readJson(dslFile);
|
|
11502
11933
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
11503
11934
|
port,
|
|
11504
11935
|
msw: opts.msw,
|
|
11505
11936
|
proxy: opts.proxy
|
|
11506
11937
|
});
|
|
11507
11938
|
for (const f of result2.files) {
|
|
11508
|
-
console.log(
|
|
11509
|
-
console.log(
|
|
11939
|
+
console.log(import_chalk21.default.green(` \u2714 ${f.path}`));
|
|
11940
|
+
console.log(import_chalk21.default.gray(` ${f.description}`));
|
|
11510
11941
|
}
|
|
11511
11942
|
}
|
|
11512
11943
|
return;
|
|
@@ -11516,19 +11947,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11516
11947
|
dslPath = await findLatestDslFile(currentDir);
|
|
11517
11948
|
if (!dslPath) {
|
|
11518
11949
|
console.error(
|
|
11519
|
-
|
|
11950
|
+
import_chalk21.default.red(
|
|
11520
11951
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
11521
11952
|
)
|
|
11522
11953
|
);
|
|
11523
11954
|
process.exit(1);
|
|
11524
11955
|
}
|
|
11525
|
-
console.log(
|
|
11956
|
+
console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
|
|
11526
11957
|
}
|
|
11527
11958
|
let dsl;
|
|
11528
11959
|
try {
|
|
11529
|
-
dsl = await
|
|
11960
|
+
dsl = await fs24.readJson(dslPath);
|
|
11530
11961
|
} catch (err) {
|
|
11531
|
-
console.error(
|
|
11962
|
+
console.error(import_chalk21.default.red(` Failed to read DSL file: ${err.message}`));
|
|
11532
11963
|
process.exit(1);
|
|
11533
11964
|
}
|
|
11534
11965
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11536,58 +11967,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11536
11967
|
msw: opts.msw,
|
|
11537
11968
|
proxy: opts.proxy
|
|
11538
11969
|
});
|
|
11539
|
-
console.log(
|
|
11970
|
+
console.log(import_chalk21.default.green(`
|
|
11540
11971
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11541
11972
|
for (const f of result.files) {
|
|
11542
|
-
console.log(
|
|
11543
|
-
console.log(
|
|
11973
|
+
console.log(import_chalk21.default.green(` ${f.path}`));
|
|
11974
|
+
console.log(import_chalk21.default.gray(` ${f.description}`));
|
|
11544
11975
|
}
|
|
11545
11976
|
if (opts.serve) {
|
|
11546
|
-
const serverJsPath =
|
|
11547
|
-
if (!await
|
|
11548
|
-
console.error(
|
|
11977
|
+
const serverJsPath = path23.join(currentDir, "mock", "server.js");
|
|
11978
|
+
if (!await fs24.pathExists(serverJsPath)) {
|
|
11979
|
+
console.error(import_chalk21.default.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11549
11980
|
process.exit(1);
|
|
11550
11981
|
}
|
|
11551
11982
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11552
|
-
console.log(
|
|
11983
|
+
console.log(import_chalk21.default.green(`
|
|
11553
11984
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11554
11985
|
if (opts.frontend) {
|
|
11555
|
-
const frontendDir =
|
|
11986
|
+
const frontendDir = path23.resolve(opts.frontend);
|
|
11556
11987
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11557
11988
|
await saveMockServerPid(frontendDir, pid);
|
|
11558
11989
|
if (proxyResult.applied) {
|
|
11559
|
-
console.log(
|
|
11560
|
-
console.log(
|
|
11990
|
+
console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11991
|
+
console.log(import_chalk21.default.bold.cyan(`
|
|
11561
11992
|
Ready! Open a new terminal and run:`));
|
|
11562
|
-
console.log(
|
|
11563
|
-
console.log(
|
|
11564
|
-
console.log(
|
|
11993
|
+
console.log(import_chalk21.default.white(` cd ${frontendDir}`));
|
|
11994
|
+
console.log(import_chalk21.default.white(` ${proxyResult.devCommand}`));
|
|
11995
|
+
console.log(import_chalk21.default.gray(`
|
|
11565
11996
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11566
11997
|
} else {
|
|
11567
|
-
console.log(
|
|
11568
|
-
if (proxyResult.note) console.log(
|
|
11998
|
+
console.log(import_chalk21.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11999
|
+
if (proxyResult.note) console.log(import_chalk21.default.gray(` ${proxyResult.note}`));
|
|
11569
12000
|
}
|
|
11570
12001
|
} else {
|
|
11571
|
-
console.log(
|
|
11572
|
-
console.log(
|
|
12002
|
+
console.log(import_chalk21.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
12003
|
+
console.log(import_chalk21.default.gray(` Mock server: http://localhost:${port}`));
|
|
11573
12004
|
}
|
|
11574
12005
|
return;
|
|
11575
12006
|
}
|
|
11576
|
-
console.log(
|
|
11577
|
-
console.log(
|
|
11578
|
-
console.log(
|
|
11579
|
-
console.log(
|
|
11580
|
-
console.log(
|
|
11581
|
-
console.log(
|
|
11582
|
-
console.log(
|
|
11583
|
-
console.log(
|
|
12007
|
+
console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12008
|
+
console.log(import_chalk21.default.white(` 1. Install express (if not already):`));
|
|
12009
|
+
console.log(import_chalk21.default.gray(` npm install --save-dev express`));
|
|
12010
|
+
console.log(import_chalk21.default.white(` 2. Start mock server:`));
|
|
12011
|
+
console.log(import_chalk21.default.gray(` node mock/server.js`));
|
|
12012
|
+
console.log(import_chalk21.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
12013
|
+
console.log(import_chalk21.default.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
12014
|
+
console.log(import_chalk21.default.gray(` http://localhost:${port}`));
|
|
11584
12015
|
if (opts.proxy) {
|
|
11585
|
-
console.log(
|
|
12016
|
+
console.log(import_chalk21.default.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11586
12017
|
}
|
|
11587
12018
|
if (opts.msw) {
|
|
11588
|
-
console.log(
|
|
11589
|
-
console.log(
|
|
11590
|
-
console.log(
|
|
12019
|
+
console.log(import_chalk21.default.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
12020
|
+
console.log(import_chalk21.default.gray(` import { worker } from './mocks/browser';`));
|
|
12021
|
+
console.log(import_chalk21.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11591
12022
|
}
|
|
11592
12023
|
});
|
|
11593
12024
|
program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
@@ -11603,26 +12034,118 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11603
12034
|
}
|
|
11604
12035
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11605
12036
|
if (result.appended) {
|
|
11606
|
-
console.log(
|
|
12037
|
+
console.log(import_chalk21.default.green(`
|
|
11607
12038
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11608
|
-
console.log(
|
|
12039
|
+
console.log(import_chalk21.default.gray(` File: .ai-spec-constitution.md`));
|
|
11609
12040
|
} else {
|
|
11610
|
-
console.log(
|
|
12041
|
+
console.log(import_chalk21.default.yellow(`
|
|
11611
12042
|
\u26A0 Not appended: ${result.reason}`));
|
|
11612
12043
|
}
|
|
11613
12044
|
});
|
|
11614
12045
|
program.command("restore").description("Restore files modified by a previous run").argument("<runId>", "Run ID shown at the end of a create / generate run").action(async (runId) => {
|
|
11615
12046
|
const currentDir = process.cwd();
|
|
11616
12047
|
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11617
|
-
console.log(
|
|
12048
|
+
console.log(import_chalk21.default.blue(`Restoring run: ${runId}...`));
|
|
11618
12049
|
const restored = await snapshot.restore();
|
|
11619
12050
|
if (restored.length === 0) {
|
|
11620
|
-
console.log(
|
|
12051
|
+
console.log(import_chalk21.default.yellow(" No backup found for this run ID."));
|
|
11621
12052
|
} else {
|
|
11622
|
-
restored.forEach((f) => console.log(
|
|
11623
|
-
console.log(
|
|
12053
|
+
restored.forEach((f) => console.log(import_chalk21.default.green(` \u2714 restored: ${f}`)));
|
|
12054
|
+
console.log(import_chalk21.default.bold.green(`
|
|
11624
12055
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11625
12056
|
}
|
|
11626
12057
|
});
|
|
12058
|
+
program.command("trend").description("Show harness score trend across past create runs").option("--last <n>", "Number of recent scored runs to show (default: 15)", "15").option("--prompt <hash>", "Filter to a specific prompt hash (prefix match)").option("--json", "Output raw JSON instead of formatted table").action(async (opts) => {
|
|
12059
|
+
const currentDir = process.cwd();
|
|
12060
|
+
const last = parseInt(opts.last, 10) || 15;
|
|
12061
|
+
const logs = await loadRunLogs(currentDir);
|
|
12062
|
+
if (logs.length === 0) {
|
|
12063
|
+
console.log(import_chalk21.default.yellow(
|
|
12064
|
+
"\n No run logs found. Run `ai-spec create` at least once to start tracking.\n"
|
|
12065
|
+
));
|
|
12066
|
+
return;
|
|
12067
|
+
}
|
|
12068
|
+
const report = buildTrendReport(logs, {
|
|
12069
|
+
last,
|
|
12070
|
+
promptFilter: opts.prompt
|
|
12071
|
+
});
|
|
12072
|
+
if (opts.json) {
|
|
12073
|
+
console.log(JSON.stringify(report, null, 2));
|
|
12074
|
+
return;
|
|
12075
|
+
}
|
|
12076
|
+
printTrendReport(report, currentDir);
|
|
12077
|
+
});
|
|
12078
|
+
program.command("logs").description("List recent run logs with stage timing").argument("[runId]", "Show detailed stage breakdown for a specific run ID").option("--last <n>", "Number of runs to list (default: 10)", "10").action(async (runId, opts) => {
|
|
12079
|
+
const currentDir = process.cwd();
|
|
12080
|
+
const logDir = path23.join(currentDir, ".ai-spec-logs");
|
|
12081
|
+
if (!await fs24.pathExists(logDir)) {
|
|
12082
|
+
console.log(import_chalk21.default.yellow("\n No run logs found (.ai-spec-logs/ does not exist).\n"));
|
|
12083
|
+
return;
|
|
12084
|
+
}
|
|
12085
|
+
if (runId) {
|
|
12086
|
+
const logPath = path23.join(logDir, `${runId}.json`);
|
|
12087
|
+
if (!await fs24.pathExists(logPath)) {
|
|
12088
|
+
console.log(import_chalk21.default.red(`
|
|
12089
|
+
Run not found: ${runId}
|
|
12090
|
+
`));
|
|
12091
|
+
return;
|
|
12092
|
+
}
|
|
12093
|
+
const log = await fs24.readJson(logPath);
|
|
12094
|
+
console.log(import_chalk21.default.cyan(`
|
|
12095
|
+
\u2500\u2500\u2500 Run: ${log.runId} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
12096
|
+
console.log(import_chalk21.default.gray(` Started : ${log.startedAt}`));
|
|
12097
|
+
if (log.endedAt) console.log(import_chalk21.default.gray(` Ended : ${log.endedAt}`));
|
|
12098
|
+
if (log.totalDurationMs !== void 0)
|
|
12099
|
+
console.log(import_chalk21.default.gray(` Duration: ${(log.totalDurationMs / 1e3).toFixed(1)}s`));
|
|
12100
|
+
if (log.provider) console.log(import_chalk21.default.gray(` Provider: ${log.provider} / ${log.model ?? "?"}`));
|
|
12101
|
+
if (log.promptHash) console.log(import_chalk21.default.gray(` Prompt : ${log.promptHash}`));
|
|
12102
|
+
if (log.harnessScore !== void 0)
|
|
12103
|
+
console.log(import_chalk21.default.white(` Score : ${log.harnessScore}/10`));
|
|
12104
|
+
if (log.filesWritten?.length)
|
|
12105
|
+
console.log(import_chalk21.default.gray(` Files : ${log.filesWritten.length} written`));
|
|
12106
|
+
if (log.errors?.length)
|
|
12107
|
+
console.log(import_chalk21.default.yellow(` Errors : ${log.errors.length}`));
|
|
12108
|
+
if (log.entries?.length) {
|
|
12109
|
+
console.log(import_chalk21.default.bold("\n Stages:\n"));
|
|
12110
|
+
const doneEvents = log.entries.filter((e) => e.event.endsWith(":done") || e.event.endsWith(":failed"));
|
|
12111
|
+
for (const entry of doneEvents) {
|
|
12112
|
+
const isOk = entry.event.endsWith(":done");
|
|
12113
|
+
const stage = entry.event.replace(/:done$|:failed$/, "");
|
|
12114
|
+
const dur = entry.data?.durationMs ? import_chalk21.default.gray(` ${(Number(entry.data.durationMs) / 1e3).toFixed(1)}s`) : "";
|
|
12115
|
+
const mark = isOk ? import_chalk21.default.green("\u2714") : import_chalk21.default.red("\u2718");
|
|
12116
|
+
console.log(` ${mark} ${stage.padEnd(20)}${dur}`);
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
|
|
12120
|
+
return;
|
|
12121
|
+
}
|
|
12122
|
+
const logs = await loadRunLogs(currentDir);
|
|
12123
|
+
const last = parseInt(opts.last, 10) || 10;
|
|
12124
|
+
const shown = logs.slice(0, last);
|
|
12125
|
+
if (shown.length === 0) {
|
|
12126
|
+
console.log(import_chalk21.default.yellow("\n No run logs found.\n"));
|
|
12127
|
+
return;
|
|
12128
|
+
}
|
|
12129
|
+
console.log(import_chalk21.default.cyan("\n\u2500\u2500\u2500 Run Logs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12130
|
+
console.log(import_chalk21.default.gray(
|
|
12131
|
+
"\n " + "Run ID ".padEnd(26) + "Date " + "Score ".padStart(6) + " Files Dur\n"
|
|
12132
|
+
));
|
|
12133
|
+
for (const log of shown) {
|
|
12134
|
+
const date = log.startedAt.slice(0, 10);
|
|
12135
|
+
const score = log.harnessScore !== void 0 ? (log.harnessScore >= 8 ? import_chalk21.default.green : log.harnessScore >= 6 ? import_chalk21.default.yellow : import_chalk21.default.red)(
|
|
12136
|
+
log.harnessScore.toFixed(1).padStart(5)
|
|
12137
|
+
) : import_chalk21.default.gray(" \u2014");
|
|
12138
|
+
const files = String(log.filesWritten?.length ?? 0).padStart(5);
|
|
12139
|
+
const dur = log.totalDurationMs !== void 0 ? import_chalk21.default.gray((log.totalDurationMs / 1e3).toFixed(0) + "s") : import_chalk21.default.gray("\u2014");
|
|
12140
|
+
const errMark = (log.errors?.length ?? 0) > 0 ? import_chalk21.default.yellow(` \u26A0${log.errors.length}`) : "";
|
|
12141
|
+
console.log(` ${import_chalk21.default.white(log.runId.padEnd(25))} ${import_chalk21.default.gray(date)} ${score} ${import_chalk21.default.gray(files)} ${dur}${errMark}`);
|
|
12142
|
+
}
|
|
12143
|
+
console.log(import_chalk21.default.gray(`
|
|
12144
|
+
Showing ${shown.length} of ${logs.length} run(s) \xB7 logs: .ai-spec-logs/`));
|
|
12145
|
+
console.log(import_chalk21.default.cyan("\u2500".repeat(63)));
|
|
12146
|
+
console.log(import_chalk21.default.gray(` Tip: ai-spec logs <runId> to see stage breakdown`));
|
|
12147
|
+
console.log(import_chalk21.default.gray(` ai-spec trend to see score trend by prompt version
|
|
12148
|
+
`));
|
|
12149
|
+
});
|
|
11627
12150
|
program.parse();
|
|
11628
12151
|
//# sourceMappingURL=index.js.map
|