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