ai-spec-dev 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/RELEASE_LOG.md +33 -0
- package/cli/index.ts +25 -12
- package/core/prompt-hasher.ts +42 -0
- package/core/run-logger.ts +21 -0
- package/core/self-evaluator.ts +172 -0
- package/dist/cli/index.js +344 -333
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +344 -333
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/purpose.md +189 -2
- package/cli/welcome.ts +0 -151
package/dist/cli/index.mjs
CHANGED
|
@@ -402,8 +402,8 @@ var init_workspace_loader = __esm({
|
|
|
402
402
|
const repos = [];
|
|
403
403
|
for (const entry of entries) {
|
|
404
404
|
const absPath = path17.join(this.workspaceRoot, entry);
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
405
|
+
const stat3 = await fs18.stat(absPath).catch(() => null);
|
|
406
|
+
if (!stat3 || !stat3.isDirectory()) continue;
|
|
407
407
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
408
408
|
if (names && !names.includes(entry)) continue;
|
|
409
409
|
const hasManifest = await fs18.pathExists(path17.join(absPath, "package.json")) || await fs18.pathExists(path17.join(absPath, "go.mod")) || await fs18.pathExists(path17.join(absPath, "Cargo.toml")) || await fs18.pathExists(path17.join(absPath, "pom.xml")) || await fs18.pathExists(path17.join(absPath, "build.gradle")) || await fs18.pathExists(path17.join(absPath, "requirements.txt")) || await fs18.pathExists(path17.join(absPath, "pyproject.toml")) || await fs18.pathExists(path17.join(absPath, "composer.json"));
|
|
@@ -467,8 +467,8 @@ var init_workspace_loader = __esm({
|
|
|
467
467
|
|
|
468
468
|
// cli/index.ts
|
|
469
469
|
import { Command } from "commander";
|
|
470
|
-
import * as
|
|
471
|
-
import * as
|
|
470
|
+
import * as path22 from "path";
|
|
471
|
+
import * as fs23 from "fs-extra";
|
|
472
472
|
import chalk19 from "chalk";
|
|
473
473
|
import * as dotenv from "dotenv";
|
|
474
474
|
import { input, confirm as confirm2, select as select3 } from "@inquirer/prompts";
|
|
@@ -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, path23, errors) {
|
|
4853
4853
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4854
|
-
errors.push({ path:
|
|
4854
|
+
errors.push({ path: path23, 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"], `${path23}.id`, errors);
|
|
4859
|
+
requireNonEmptyString(f["title"], `${path23}.title`, errors);
|
|
4860
|
+
requireNonEmptyString(f["description"], `${path23}.description`, errors);
|
|
4861
4861
|
}
|
|
4862
|
-
function validateModel(raw,
|
|
4862
|
+
function validateModel(raw, path23, errors) {
|
|
4863
4863
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4864
|
-
errors.push({ path:
|
|
4864
|
+
errors.push({ path: path23, 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"], `${path23}.name`, errors);
|
|
4869
4869
|
if (!Array.isArray(m["fields"])) {
|
|
4870
|
-
errors.push({ path: `${
|
|
4870
|
+
errors.push({ path: `${path23}.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: `${path23}.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], `${path23}.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: `${path23}.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: `${path23}.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, path23, errors) {
|
|
4894
4894
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4895
|
-
errors.push({ path:
|
|
4895
|
+
errors.push({ path: path23, 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"], `${path23}.name`, errors);
|
|
4900
|
+
requireNonEmptyString(f["type"], `${path23}.type`, errors);
|
|
4901
4901
|
if (typeof f["required"] !== "boolean") {
|
|
4902
|
-
errors.push({ path: `${
|
|
4902
|
+
errors.push({ path: `${path23}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4903
4903
|
}
|
|
4904
4904
|
}
|
|
4905
|
-
function validateEndpoint(raw,
|
|
4905
|
+
function validateEndpoint(raw, path23, errors) {
|
|
4906
4906
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4907
|
-
errors.push({ path:
|
|
4907
|
+
errors.push({ path: path23, 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"], `${path23}.id`, errors);
|
|
4912
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4913
4913
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4914
4914
|
errors.push({
|
|
4915
|
-
path: `${
|
|
4915
|
+
path: `${path23}.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: `${path23}.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: `${path23}.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: `${path23}.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"], `${path23}.successDescription`, errors);
|
|
4935
4935
|
if (e["request"] !== void 0) {
|
|
4936
|
-
validateRequestSchema(e["request"], `${
|
|
4936
|
+
validateRequestSchema(e["request"], `${path23}.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: `${path23}.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: `${path23}.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], `${path23}.errors[${j2}]`, errors);
|
|
4948
4948
|
}
|
|
4949
4949
|
}
|
|
4950
4950
|
}
|
|
4951
4951
|
}
|
|
4952
|
-
function validateRequestSchema(raw,
|
|
4952
|
+
function validateRequestSchema(raw, path23, errors) {
|
|
4953
4953
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4954
|
-
errors.push({ path:
|
|
4954
|
+
errors.push({ path: path23, 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], `${path23}.${key}`, errors);
|
|
4961
4961
|
}
|
|
4962
4962
|
}
|
|
4963
4963
|
}
|
|
4964
|
-
function validateFieldMap(raw,
|
|
4964
|
+
function validateFieldMap(raw, path23, errors) {
|
|
4965
4965
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4966
|
-
errors.push({ path:
|
|
4966
|
+
errors.push({ path: path23, 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: `${path23}.${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, path23, errors) {
|
|
4977
4977
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4978
|
-
errors.push({ path:
|
|
4978
|
+
errors.push({ path: path23, 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: `${path23}.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"], `${path23}.code`, errors);
|
|
4986
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4987
4987
|
}
|
|
4988
|
-
function validateBehavior(raw,
|
|
4988
|
+
function validateBehavior(raw, path23, errors) {
|
|
4989
4989
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4990
|
-
errors.push({ path:
|
|
4990
|
+
errors.push({ path: path23, 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"], `${path23}.id`, errors);
|
|
4995
|
+
requireNonEmptyString(b["description"], `${path23}.description`, errors);
|
|
4996
4996
|
if (b["constraints"] !== void 0) {
|
|
4997
4997
|
if (!Array.isArray(b["constraints"])) {
|
|
4998
|
-
errors.push({ path: `${
|
|
4998
|
+
errors.push({ path: `${path23}.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: `${path23}.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, path23, errors) {
|
|
5010
5010
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5011
|
-
errors.push({ path:
|
|
5011
|
+
errors.push({ path: path23, 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"], `${path23}.id`, errors);
|
|
5016
|
+
requireNonEmptyString(c["name"], `${path23}.name`, errors);
|
|
5017
|
+
requireNonEmptyString(c["description"], `${path23}.description`, errors);
|
|
5018
5018
|
if (c["props"] !== void 0) {
|
|
5019
5019
|
if (!Array.isArray(c["props"])) {
|
|
5020
|
-
errors.push({ path: `${
|
|
5020
|
+
errors.push({ path: `${path23}.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: `${path23}.props[${j2}]`, message: "Must be an object" });
|
|
5027
5027
|
continue;
|
|
5028
5028
|
}
|
|
5029
|
-
requireNonEmptyString(p["name"], `${
|
|
5030
|
-
requireNonEmptyString(p["type"], `${
|
|
5029
|
+
requireNonEmptyString(p["name"], `${path23}.props[${j2}].name`, errors);
|
|
5030
|
+
requireNonEmptyString(p["type"], `${path23}.props[${j2}].type`, errors);
|
|
5031
5031
|
if (typeof p["required"] !== "boolean") {
|
|
5032
|
-
errors.push({ path: `${
|
|
5032
|
+
errors.push({ path: `${path23}.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: `${path23}.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: `${path23}.events[${j2}]`, message: "Must be an object" });
|
|
5046
5046
|
continue;
|
|
5047
5047
|
}
|
|
5048
|
-
requireNonEmptyString(e["name"], `${
|
|
5048
|
+
requireNonEmptyString(e["name"], `${path23}.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: `${path23}.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: `${path23}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5060
5060
|
}
|
|
5061
5061
|
}
|
|
5062
5062
|
}
|
|
5063
|
-
function requireNonEmptyString(v2,
|
|
5063
|
+
function requireNonEmptyString(v2, path23, errors) {
|
|
5064
5064
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5065
5065
|
errors.push({
|
|
5066
|
-
path:
|
|
5066
|
+
path: path23,
|
|
5067
5067
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5068
5068
|
});
|
|
5069
5069
|
}
|
|
@@ -6106,6 +6106,16 @@ var RunLogger = class {
|
|
|
6106
6106
|
this.log.errors.push(`[${event}] ${error}`);
|
|
6107
6107
|
this.flush();
|
|
6108
6108
|
}
|
|
6109
|
+
/** Record the prompt hash for this run (call once at run start). */
|
|
6110
|
+
setPromptHash(hash) {
|
|
6111
|
+
this.log.promptHash = hash;
|
|
6112
|
+
this.flush();
|
|
6113
|
+
}
|
|
6114
|
+
/** Record the harness self-eval score (call once at run end). */
|
|
6115
|
+
setHarnessScore(score) {
|
|
6116
|
+
this.log.harnessScore = score;
|
|
6117
|
+
this.flush();
|
|
6118
|
+
}
|
|
6109
6119
|
fileWritten(filePath) {
|
|
6110
6120
|
if (!this.log.filesWritten.includes(filePath)) {
|
|
6111
6121
|
this.log.filesWritten.push(filePath);
|
|
@@ -8318,108 +8328,6 @@ async function clearKey(provider) {
|
|
|
8318
8328
|
await writeStore(store);
|
|
8319
8329
|
}
|
|
8320
8330
|
|
|
8321
|
-
// cli/welcome.ts
|
|
8322
|
-
import chalk17 from "chalk";
|
|
8323
|
-
import * as os4 from "os";
|
|
8324
|
-
import * as path19 from "path";
|
|
8325
|
-
import * as fs20 from "fs-extra";
|
|
8326
|
-
var VERSION = "0.14.1";
|
|
8327
|
-
var TOTAL_W = 76;
|
|
8328
|
-
var L_WIDTH = 44;
|
|
8329
|
-
var R_WIDTH = TOTAL_W - L_WIDTH - 4;
|
|
8330
|
-
var ROBOT_COLOR = chalk17.hex("#E8885A");
|
|
8331
|
-
var ROBOT = [
|
|
8332
|
-
ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
|
|
8333
|
-
ROBOT_COLOR(" \u2502") + chalk17.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + chalk17.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
|
|
8334
|
-
ROBOT_COLOR(" \u2502") + chalk17.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
|
|
8335
|
-
ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
|
|
8336
|
-
ROBOT_COLOR(" \u2502"),
|
|
8337
|
-
ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
|
|
8338
|
-
];
|
|
8339
|
-
function visLen(s) {
|
|
8340
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
8341
|
-
}
|
|
8342
|
-
function padR(s, width) {
|
|
8343
|
-
const vl = visLen(s);
|
|
8344
|
-
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
8345
|
-
}
|
|
8346
|
-
function row(left, right) {
|
|
8347
|
-
return padR(left, L_WIDTH) + " " + chalk17.gray("\u2502") + " " + padR(right, R_WIDTH);
|
|
8348
|
-
}
|
|
8349
|
-
function center(s, width) {
|
|
8350
|
-
const vl = visLen(s);
|
|
8351
|
-
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
8352
|
-
return " ".repeat(pad) + s;
|
|
8353
|
-
}
|
|
8354
|
-
async function getRecentSpecs(dir) {
|
|
8355
|
-
const specsDir = path19.join(dir, "specs");
|
|
8356
|
-
if (!await fs20.pathExists(specsDir)) return [];
|
|
8357
|
-
try {
|
|
8358
|
-
const files = await fs20.readdir(specsDir);
|
|
8359
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
8360
|
-
const withStats = await Promise.all(
|
|
8361
|
-
mdFiles.map(async (f) => {
|
|
8362
|
-
const stat4 = await fs20.stat(path19.join(specsDir, f));
|
|
8363
|
-
return { name: f, mtime: stat4.mtime.getTime() };
|
|
8364
|
-
})
|
|
8365
|
-
);
|
|
8366
|
-
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
8367
|
-
return withStats.slice(0, 3).map(({ name, mtime }) => {
|
|
8368
|
-
const ms2 = Date.now() - mtime;
|
|
8369
|
-
const hours = Math.floor(ms2 / 36e5);
|
|
8370
|
-
const days = Math.floor(hours / 24);
|
|
8371
|
-
const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
|
|
8372
|
-
const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
|
|
8373
|
-
return chalk17.white(slug) + chalk17.dim(" " + age);
|
|
8374
|
-
});
|
|
8375
|
-
} catch {
|
|
8376
|
-
return [];
|
|
8377
|
-
}
|
|
8378
|
-
}
|
|
8379
|
-
async function printWelcome(currentDir, config2) {
|
|
8380
|
-
const username = os4.userInfo().username;
|
|
8381
|
-
const homeDir = os4.homedir();
|
|
8382
|
-
const shortDir = currentDir.startsWith(homeDir) ? "~" + currentDir.slice(homeDir.length) : currentDir;
|
|
8383
|
-
const recentSpecs = await getRecentSpecs(currentDir);
|
|
8384
|
-
const providerBit = config2?.provider ? config2.provider + (config2.model ? " \xB7 " + config2.model : "") : "";
|
|
8385
|
-
const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
|
|
8386
|
-
const maxInfoLen = L_WIDTH - 2;
|
|
8387
|
-
const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
|
|
8388
|
-
const bottomLine = " " + chalk17.dim(bottomTruncated);
|
|
8389
|
-
const titleInner = `ai-spec v${VERSION} `;
|
|
8390
|
-
const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
|
|
8391
|
-
console.log(
|
|
8392
|
-
"\n" + chalk17.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
|
|
8393
|
-
);
|
|
8394
|
-
const welcomeText = "Welcome back, " + chalk17.bold.white(username) + "!";
|
|
8395
|
-
const leftLines = [
|
|
8396
|
-
"",
|
|
8397
|
-
center(welcomeText, L_WIDTH),
|
|
8398
|
-
"",
|
|
8399
|
-
...ROBOT,
|
|
8400
|
-
"",
|
|
8401
|
-
bottomLine,
|
|
8402
|
-
""
|
|
8403
|
-
];
|
|
8404
|
-
const rightLines = [
|
|
8405
|
-
chalk17.hex("#FF8C00").bold("Tips for getting started"),
|
|
8406
|
-
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8407
|
-
chalk17.white('ai-spec create "feature"'),
|
|
8408
|
-
chalk17.gray("ai-spec workspace run"),
|
|
8409
|
-
chalk17.gray('ai-spec update "change"'),
|
|
8410
|
-
"",
|
|
8411
|
-
chalk17.hex("#FF8C00").bold("Recent activity"),
|
|
8412
|
-
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8413
|
-
...recentSpecs.length > 0 ? recentSpecs : [chalk17.dim("No recent activity")]
|
|
8414
|
-
];
|
|
8415
|
-
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
8416
|
-
for (let i = 0; i < maxLines; i++) {
|
|
8417
|
-
console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
|
|
8418
|
-
}
|
|
8419
|
-
console.log(chalk17.gray("\u2500".repeat(TOTAL_W)));
|
|
8420
|
-
console.log();
|
|
8421
|
-
}
|
|
8422
|
-
|
|
8423
8331
|
// prompts/global-constitution.prompt.ts
|
|
8424
8332
|
var globalConstitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided multi-project context and generate a "Global Constitution" \u2014 a team-level baseline document that captures cross-project rules, shared conventions, and universal constraints that every repository in this workspace must follow.
|
|
8425
8333
|
|
|
@@ -8929,8 +8837,8 @@ Existing API/service files:`);
|
|
|
8929
8837
|
}
|
|
8930
8838
|
|
|
8931
8839
|
// core/mock-server-generator.ts
|
|
8932
|
-
import * as
|
|
8933
|
-
import * as
|
|
8840
|
+
import * as path19 from "path";
|
|
8841
|
+
import * as fs20 from "fs-extra";
|
|
8934
8842
|
import { spawn } from "child_process";
|
|
8935
8843
|
function typeToFixture(fieldName, typeDesc) {
|
|
8936
8844
|
const t = typeDesc.toLowerCase();
|
|
@@ -9066,22 +8974,22 @@ function generateMockServerJs(dsl, port) {
|
|
|
9066
8974
|
}
|
|
9067
8975
|
function detectFrontendFramework(projectDir) {
|
|
9068
8976
|
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
9069
|
-
if (
|
|
8977
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "vite";
|
|
9070
8978
|
}
|
|
9071
8979
|
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
9072
|
-
if (
|
|
8980
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "next";
|
|
9073
8981
|
}
|
|
9074
|
-
const pkgPath =
|
|
9075
|
-
if (
|
|
8982
|
+
const pkgPath = path19.join(projectDir, "package.json");
|
|
8983
|
+
if (fs20.existsSync(pkgPath)) {
|
|
9076
8984
|
try {
|
|
9077
|
-
const pkg = JSON.parse(
|
|
8985
|
+
const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
|
|
9078
8986
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9079
8987
|
if (deps["react-scripts"]) return "cra";
|
|
9080
8988
|
} catch {
|
|
9081
8989
|
}
|
|
9082
8990
|
}
|
|
9083
8991
|
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
9084
|
-
if (
|
|
8992
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "webpack";
|
|
9085
8993
|
}
|
|
9086
8994
|
return "unknown";
|
|
9087
8995
|
}
|
|
@@ -9261,7 +9169,7 @@ export const worker = setupWorker(...handlers);
|
|
|
9261
9169
|
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
9262
9170
|
function findViteConfigFile(projectDir) {
|
|
9263
9171
|
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
9264
|
-
if (
|
|
9172
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return f;
|
|
9265
9173
|
}
|
|
9266
9174
|
return null;
|
|
9267
9175
|
}
|
|
@@ -9307,73 +9215,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
|
9307
9215
|
if (framework === "vite") {
|
|
9308
9216
|
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
9309
9217
|
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
9310
|
-
const mockConfigPath =
|
|
9311
|
-
await
|
|
9218
|
+
const mockConfigPath = path19.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
9219
|
+
await fs20.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
9312
9220
|
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
9313
|
-
const pkgPath =
|
|
9314
|
-
if (await
|
|
9315
|
-
const pkg = await
|
|
9221
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9222
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9223
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9316
9224
|
pkg.scripts = pkg.scripts ?? {};
|
|
9317
9225
|
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
9318
9226
|
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
9319
|
-
await
|
|
9227
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9320
9228
|
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
9321
9229
|
}
|
|
9322
9230
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9323
|
-
await
|
|
9231
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9324
9232
|
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
9325
9233
|
}
|
|
9326
9234
|
if (framework === "cra") {
|
|
9327
|
-
const pkgPath =
|
|
9328
|
-
if (await
|
|
9329
|
-
const pkg = await
|
|
9235
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9236
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9237
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9330
9238
|
const originalProxy = pkg.proxy ?? null;
|
|
9331
9239
|
pkg.proxy = `http://localhost:${mockPort}`;
|
|
9332
|
-
await
|
|
9240
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9333
9241
|
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
9334
9242
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9335
|
-
await
|
|
9243
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9336
9244
|
return { framework, applied: true, devCommand: "npm start" };
|
|
9337
9245
|
}
|
|
9338
9246
|
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
9339
9247
|
}
|
|
9340
9248
|
const lock = { framework, mockPort, frontendDir, actions };
|
|
9341
|
-
await
|
|
9249
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
9342
9250
|
const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
|
|
9343
9251
|
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
9344
9252
|
}
|
|
9345
9253
|
async function restoreMockProxy(frontendDir) {
|
|
9346
|
-
const lockPath =
|
|
9347
|
-
if (!await
|
|
9254
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9255
|
+
if (!await fs20.pathExists(lockPath)) {
|
|
9348
9256
|
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
9349
9257
|
}
|
|
9350
|
-
const lock = await
|
|
9258
|
+
const lock = await fs20.readJson(lockPath);
|
|
9351
9259
|
for (const action of lock.actions) {
|
|
9352
9260
|
if (action.type === "wrote-file") {
|
|
9353
|
-
const fp =
|
|
9354
|
-
if (await
|
|
9261
|
+
const fp = path19.join(frontendDir, action.filePath);
|
|
9262
|
+
if (await fs20.pathExists(fp)) await fs20.remove(fp);
|
|
9355
9263
|
} else if (action.type === "added-pkg-script") {
|
|
9356
|
-
const pkgPath =
|
|
9357
|
-
if (await
|
|
9358
|
-
const pkg = await
|
|
9264
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9265
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9266
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9359
9267
|
if (action.originalValue == null) {
|
|
9360
9268
|
delete pkg.scripts?.[action.key];
|
|
9361
9269
|
} else {
|
|
9362
9270
|
pkg.scripts = pkg.scripts ?? {};
|
|
9363
9271
|
pkg.scripts[action.key] = action.originalValue;
|
|
9364
9272
|
}
|
|
9365
|
-
await
|
|
9273
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9366
9274
|
}
|
|
9367
9275
|
} else if (action.type === "patched-pkg-proxy") {
|
|
9368
|
-
const pkgPath =
|
|
9369
|
-
if (await
|
|
9370
|
-
const pkg = await
|
|
9276
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9277
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9278
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9371
9279
|
if (action.originalProxy == null) {
|
|
9372
9280
|
delete pkg.proxy;
|
|
9373
9281
|
} else {
|
|
9374
9282
|
pkg.proxy = action.originalProxy;
|
|
9375
9283
|
}
|
|
9376
|
-
await
|
|
9284
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9377
9285
|
}
|
|
9378
9286
|
}
|
|
9379
9287
|
}
|
|
@@ -9383,7 +9291,7 @@ async function restoreMockProxy(frontendDir) {
|
|
|
9383
9291
|
} catch {
|
|
9384
9292
|
}
|
|
9385
9293
|
}
|
|
9386
|
-
await
|
|
9294
|
+
await fs20.remove(lockPath);
|
|
9387
9295
|
return { restored: true };
|
|
9388
9296
|
}
|
|
9389
9297
|
function startMockServerBackground(serverJsPath, port) {
|
|
@@ -9396,23 +9304,23 @@ function startMockServerBackground(serverJsPath, port) {
|
|
|
9396
9304
|
return child.pid;
|
|
9397
9305
|
}
|
|
9398
9306
|
async function saveMockServerPid(frontendDir, pid) {
|
|
9399
|
-
const lockPath =
|
|
9400
|
-
if (await
|
|
9401
|
-
const lock = await
|
|
9307
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9308
|
+
if (await fs20.pathExists(lockPath)) {
|
|
9309
|
+
const lock = await fs20.readJson(lockPath);
|
|
9402
9310
|
lock.mockServerPid = pid;
|
|
9403
|
-
await
|
|
9311
|
+
await fs20.writeJson(lockPath, lock, { spaces: 2 });
|
|
9404
9312
|
}
|
|
9405
9313
|
}
|
|
9406
9314
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
9407
9315
|
const port = opts.port ?? 3001;
|
|
9408
|
-
const outputDir =
|
|
9316
|
+
const outputDir = path19.join(projectDir, opts.outputDir ?? "mock");
|
|
9409
9317
|
const result = { files: [] };
|
|
9410
|
-
await
|
|
9318
|
+
await fs20.ensureDir(outputDir);
|
|
9411
9319
|
const serverJs = generateMockServerJs(dsl, port);
|
|
9412
|
-
const serverPath =
|
|
9413
|
-
await
|
|
9320
|
+
const serverPath = path19.join(outputDir, "server.js");
|
|
9321
|
+
await fs20.writeFile(serverPath, serverJs, "utf-8");
|
|
9414
9322
|
result.files.push({
|
|
9415
|
-
path:
|
|
9323
|
+
path: path19.relative(projectDir, serverPath),
|
|
9416
9324
|
description: `Express mock server \u2014 run with: node mock/server.js`
|
|
9417
9325
|
});
|
|
9418
9326
|
const mockReadme = `# Mock Server
|
|
@@ -9442,52 +9350,52 @@ ${dsl.endpoints.map(
|
|
|
9442
9350
|
|
|
9443
9351
|
Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
|
|
9444
9352
|
`;
|
|
9445
|
-
const readmePath =
|
|
9446
|
-
await
|
|
9353
|
+
const readmePath = path19.join(outputDir, "README.md");
|
|
9354
|
+
await fs20.writeFile(readmePath, mockReadme, "utf-8");
|
|
9447
9355
|
result.files.push({
|
|
9448
|
-
path:
|
|
9356
|
+
path: path19.relative(projectDir, readmePath),
|
|
9449
9357
|
description: "Mock server usage guide"
|
|
9450
9358
|
});
|
|
9451
9359
|
if (opts.proxy) {
|
|
9452
9360
|
const { content, filename } = generateProxyConfig(dsl, port, projectDir);
|
|
9453
|
-
const proxyPath =
|
|
9454
|
-
await
|
|
9455
|
-
await
|
|
9361
|
+
const proxyPath = path19.join(projectDir, filename);
|
|
9362
|
+
await fs20.ensureDir(path19.dirname(proxyPath));
|
|
9363
|
+
await fs20.writeFile(proxyPath, content, "utf-8");
|
|
9456
9364
|
result.files.push({
|
|
9457
9365
|
path: filename,
|
|
9458
9366
|
description: "Proxy config snippet \u2014 copy instructions into your framework config"
|
|
9459
9367
|
});
|
|
9460
9368
|
}
|
|
9461
9369
|
if (opts.msw) {
|
|
9462
|
-
const mswDir =
|
|
9463
|
-
await
|
|
9370
|
+
const mswDir = path19.join(projectDir, "src", "mocks");
|
|
9371
|
+
await fs20.ensureDir(mswDir);
|
|
9464
9372
|
const handlersContent = generateMswHandlers(dsl);
|
|
9465
|
-
const handlersPath =
|
|
9466
|
-
await
|
|
9373
|
+
const handlersPath = path19.join(mswDir, "handlers.ts");
|
|
9374
|
+
await fs20.writeFile(handlersPath, handlersContent, "utf-8");
|
|
9467
9375
|
result.files.push({
|
|
9468
|
-
path:
|
|
9376
|
+
path: path19.relative(projectDir, handlersPath),
|
|
9469
9377
|
description: "MSW request handlers"
|
|
9470
9378
|
});
|
|
9471
9379
|
const browserContent = generateMswBrowser();
|
|
9472
|
-
const browserPath =
|
|
9473
|
-
await
|
|
9380
|
+
const browserPath = path19.join(mswDir, "browser.ts");
|
|
9381
|
+
await fs20.writeFile(browserPath, browserContent, "utf-8");
|
|
9474
9382
|
result.files.push({
|
|
9475
|
-
path:
|
|
9383
|
+
path: path19.relative(projectDir, browserPath),
|
|
9476
9384
|
description: "MSW browser worker setup"
|
|
9477
9385
|
});
|
|
9478
9386
|
}
|
|
9479
9387
|
return result;
|
|
9480
9388
|
}
|
|
9481
9389
|
async function findLatestDslFile(projectDir) {
|
|
9482
|
-
const specDir =
|
|
9483
|
-
if (!await
|
|
9390
|
+
const specDir = path19.join(projectDir, ".ai-spec");
|
|
9391
|
+
if (!await fs20.pathExists(specDir)) return null;
|
|
9484
9392
|
const allFiles = [];
|
|
9485
9393
|
async function scan(dir) {
|
|
9486
|
-
const entries = await
|
|
9394
|
+
const entries = await fs20.readdir(dir);
|
|
9487
9395
|
for (const entry of entries) {
|
|
9488
|
-
const abs =
|
|
9489
|
-
const
|
|
9490
|
-
if (
|
|
9396
|
+
const abs = path19.join(dir, entry);
|
|
9397
|
+
const stat3 = await fs20.stat(abs);
|
|
9398
|
+
if (stat3.isDirectory()) {
|
|
9491
9399
|
await scan(abs);
|
|
9492
9400
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9493
9401
|
allFiles.push(abs);
|
|
@@ -9497,16 +9405,16 @@ async function findLatestDslFile(projectDir) {
|
|
|
9497
9405
|
await scan(specDir);
|
|
9498
9406
|
if (allFiles.length === 0) return null;
|
|
9499
9407
|
const withMtimes = await Promise.all(
|
|
9500
|
-
allFiles.map(async (f) => ({ f, mtime: (await
|
|
9408
|
+
allFiles.map(async (f) => ({ f, mtime: (await fs20.stat(f)).mtime }))
|
|
9501
9409
|
);
|
|
9502
9410
|
withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
9503
9411
|
return withMtimes[0].f;
|
|
9504
9412
|
}
|
|
9505
9413
|
|
|
9506
9414
|
// core/spec-updater.ts
|
|
9507
|
-
import
|
|
9508
|
-
import * as
|
|
9509
|
-
import * as
|
|
9415
|
+
import chalk17 from "chalk";
|
|
9416
|
+
import * as path20 from "path";
|
|
9417
|
+
import * as fs21 from "fs-extra";
|
|
9510
9418
|
|
|
9511
9419
|
// prompts/update.prompt.ts
|
|
9512
9420
|
var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
|
|
@@ -9652,8 +9560,8 @@ var SpecUpdater = class {
|
|
|
9652
9560
|
* Returns all .md spec files sorted newest-first.
|
|
9653
9561
|
*/
|
|
9654
9562
|
static async findLatestSpec(specsDir) {
|
|
9655
|
-
if (!await
|
|
9656
|
-
const files = await
|
|
9563
|
+
if (!await fs21.pathExists(specsDir)) return null;
|
|
9564
|
+
const files = await fs21.readdir(specsDir);
|
|
9657
9565
|
const pattern = /^feature-(.+)-v(\d+)\.md$/;
|
|
9658
9566
|
let latest = null;
|
|
9659
9567
|
for (const file of files) {
|
|
@@ -9661,8 +9569,8 @@ var SpecUpdater = class {
|
|
|
9661
9569
|
if (!m) continue;
|
|
9662
9570
|
const version = parseInt(m[2], 10);
|
|
9663
9571
|
if (!latest || version > latest.version) {
|
|
9664
|
-
const filePath =
|
|
9665
|
-
const content = await
|
|
9572
|
+
const filePath = path20.join(specsDir, file);
|
|
9573
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
9666
9574
|
latest = { filePath, version, slug: m[1], content };
|
|
9667
9575
|
}
|
|
9668
9576
|
}
|
|
@@ -9673,16 +9581,16 @@ var SpecUpdater = class {
|
|
|
9673
9581
|
* Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
|
|
9674
9582
|
*/
|
|
9675
9583
|
async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
|
|
9676
|
-
const existingSpec = await
|
|
9584
|
+
const existingSpec = await fs21.readFile(existingSpecPath, "utf-8");
|
|
9677
9585
|
let existingDsl = null;
|
|
9678
9586
|
const dslFile = await findLatestDslFile(projectDir);
|
|
9679
9587
|
if (dslFile) {
|
|
9680
9588
|
try {
|
|
9681
|
-
existingDsl = await
|
|
9589
|
+
existingDsl = await fs21.readJson(dslFile);
|
|
9682
9590
|
} catch {
|
|
9683
9591
|
}
|
|
9684
9592
|
}
|
|
9685
|
-
console.log(
|
|
9593
|
+
console.log(chalk17.blue(" [1/3] Generating updated spec..."));
|
|
9686
9594
|
const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
|
|
9687
9595
|
let updatedSpecContent;
|
|
9688
9596
|
try {
|
|
@@ -9691,15 +9599,15 @@ var SpecUpdater = class {
|
|
|
9691
9599
|
} catch (err) {
|
|
9692
9600
|
throw new Error(`Spec update generation failed: ${err.message}`);
|
|
9693
9601
|
}
|
|
9694
|
-
const specBasename =
|
|
9602
|
+
const specBasename = path20.basename(existingSpecPath);
|
|
9695
9603
|
const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
|
|
9696
9604
|
const slug = slugMatch ? slugMatch[1] : "feature";
|
|
9697
|
-
const specsDir =
|
|
9605
|
+
const specsDir = path20.dirname(existingSpecPath);
|
|
9698
9606
|
const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
|
|
9699
|
-
await
|
|
9700
|
-
await
|
|
9701
|
-
console.log(
|
|
9702
|
-
console.log(
|
|
9607
|
+
await fs21.ensureDir(specsDir);
|
|
9608
|
+
await fs21.writeFile(newSpecPath, updatedSpecContent, "utf-8");
|
|
9609
|
+
console.log(chalk17.green(` \u2714 New spec written: ${path20.relative(projectDir, newSpecPath)}`));
|
|
9610
|
+
console.log(chalk17.blue(" [2/3] Updating DSL..."));
|
|
9703
9611
|
let updatedDsl = null;
|
|
9704
9612
|
let newDslPath = null;
|
|
9705
9613
|
if (existingDsl) {
|
|
@@ -9711,7 +9619,7 @@ var SpecUpdater = class {
|
|
|
9711
9619
|
updatedDsl = parsed;
|
|
9712
9620
|
}
|
|
9713
9621
|
} catch {
|
|
9714
|
-
console.log(
|
|
9622
|
+
console.log(chalk17.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
|
|
9715
9623
|
}
|
|
9716
9624
|
}
|
|
9717
9625
|
if (!updatedDsl) {
|
|
@@ -9720,15 +9628,15 @@ var SpecUpdater = class {
|
|
|
9720
9628
|
}
|
|
9721
9629
|
if (updatedDsl) {
|
|
9722
9630
|
const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
|
|
9723
|
-
await
|
|
9631
|
+
await fs21.writeJson(dslPath, updatedDsl, { spaces: 2 });
|
|
9724
9632
|
newDslPath = dslPath;
|
|
9725
|
-
console.log(
|
|
9633
|
+
console.log(chalk17.green(` \u2714 DSL updated: ${path20.relative(projectDir, dslPath)}`));
|
|
9726
9634
|
} else {
|
|
9727
|
-
console.log(
|
|
9635
|
+
console.log(chalk17.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
|
|
9728
9636
|
}
|
|
9729
9637
|
let affectedFiles = [];
|
|
9730
9638
|
if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
|
|
9731
|
-
console.log(
|
|
9639
|
+
console.log(chalk17.blue(" [3/3] Identifying affected files..."));
|
|
9732
9640
|
const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
|
|
9733
9641
|
const affectedPrompt = buildAffectedFilesPrompt(
|
|
9734
9642
|
changeRequest,
|
|
@@ -9739,9 +9647,9 @@ var SpecUpdater = class {
|
|
|
9739
9647
|
try {
|
|
9740
9648
|
const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
|
|
9741
9649
|
affectedFiles = parseAffectedFiles(affectedRaw);
|
|
9742
|
-
console.log(
|
|
9650
|
+
console.log(chalk17.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
|
|
9743
9651
|
} catch {
|
|
9744
|
-
console.log(
|
|
9652
|
+
console.log(chalk17.gray(" Could not identify affected files \u2014 use manual selection."));
|
|
9745
9653
|
}
|
|
9746
9654
|
}
|
|
9747
9655
|
return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
|
|
@@ -9749,8 +9657,8 @@ var SpecUpdater = class {
|
|
|
9749
9657
|
};
|
|
9750
9658
|
|
|
9751
9659
|
// core/openapi-exporter.ts
|
|
9752
|
-
import * as
|
|
9753
|
-
import * as
|
|
9660
|
+
import * as path21 from "path";
|
|
9661
|
+
import * as fs22 from "fs-extra";
|
|
9754
9662
|
function dslTypeToOASchema(typeDesc, fieldName = "") {
|
|
9755
9663
|
const t = typeDesc.toLowerCase();
|
|
9756
9664
|
if (t === "string" || t.includes("string")) {
|
|
@@ -9979,7 +9887,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9979
9887
|
const format = opts.format ?? "yaml";
|
|
9980
9888
|
const serverUrl = opts.serverUrl ?? "http://localhost:3000";
|
|
9981
9889
|
const defaultName = `openapi.${format}`;
|
|
9982
|
-
const outputPath = opts.outputPath ?
|
|
9890
|
+
const outputPath = opts.outputPath ? path21.isAbsolute(opts.outputPath) ? opts.outputPath : path21.join(projectDir, opts.outputPath) : path21.join(projectDir, defaultName);
|
|
9983
9891
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
9984
9892
|
let content;
|
|
9985
9893
|
if (format === "json") {
|
|
@@ -9987,18 +9895,115 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9987
9895
|
} else {
|
|
9988
9896
|
content = buildYamlDoc(doc);
|
|
9989
9897
|
}
|
|
9990
|
-
await
|
|
9991
|
-
await
|
|
9898
|
+
await fs22.ensureDir(path21.dirname(outputPath));
|
|
9899
|
+
await fs22.writeFile(outputPath, content, "utf-8");
|
|
9992
9900
|
return outputPath;
|
|
9993
9901
|
}
|
|
9994
9902
|
|
|
9903
|
+
// core/prompt-hasher.ts
|
|
9904
|
+
init_codegen_prompt();
|
|
9905
|
+
init_codegen_prompt();
|
|
9906
|
+
import { createHash } from "crypto";
|
|
9907
|
+
function computePromptHash() {
|
|
9908
|
+
const segments = [
|
|
9909
|
+
codeGenSystemPrompt,
|
|
9910
|
+
dslSystemPrompt,
|
|
9911
|
+
specPrompt,
|
|
9912
|
+
reviewArchitectureSystemPrompt,
|
|
9913
|
+
reviewImplementationSystemPrompt,
|
|
9914
|
+
reviewImpactComplexitySystemPrompt
|
|
9915
|
+
];
|
|
9916
|
+
return createHash("sha256").update(segments.join("\0")).digest("hex").slice(0, 8);
|
|
9917
|
+
}
|
|
9918
|
+
|
|
9919
|
+
// core/self-evaluator.ts
|
|
9920
|
+
import chalk18 from "chalk";
|
|
9921
|
+
var ENDPOINT_LAYER_PATTERNS = [
|
|
9922
|
+
/src\/api/,
|
|
9923
|
+
/src\/routes?/,
|
|
9924
|
+
/src\/controller/,
|
|
9925
|
+
/src\/handler/,
|
|
9926
|
+
/src\/endpoints?/
|
|
9927
|
+
];
|
|
9928
|
+
var MODEL_LAYER_PATTERNS = [
|
|
9929
|
+
/src\/model/,
|
|
9930
|
+
/src\/schema/,
|
|
9931
|
+
/src\/entit/,
|
|
9932
|
+
/src\/db/,
|
|
9933
|
+
/prisma/,
|
|
9934
|
+
/src\/data/,
|
|
9935
|
+
/src\/domain/
|
|
9936
|
+
];
|
|
9937
|
+
function extractReviewScore(reviewText) {
|
|
9938
|
+
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
9939
|
+
return match ? parseFloat(match[1]) : null;
|
|
9940
|
+
}
|
|
9941
|
+
function runSelfEval(opts) {
|
|
9942
|
+
const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
|
|
9943
|
+
const endpointsTotal = dsl?.endpoints?.length ?? 0;
|
|
9944
|
+
const modelsTotal = dsl?.models?.length ?? 0;
|
|
9945
|
+
const endpointLayerCovered = generatedFiles.some(
|
|
9946
|
+
(f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9947
|
+
);
|
|
9948
|
+
const modelLayerCovered = generatedFiles.some(
|
|
9949
|
+
(f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9950
|
+
);
|
|
9951
|
+
let dslCoverageScore = 10;
|
|
9952
|
+
if (generatedFiles.length === 0) {
|
|
9953
|
+
dslCoverageScore = 0;
|
|
9954
|
+
} else {
|
|
9955
|
+
if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
|
|
9956
|
+
if (modelsTotal > 0 && !modelLayerCovered) dslCoverageScore -= 3;
|
|
9957
|
+
}
|
|
9958
|
+
const compileScore = compilePassed ? 10 : 5;
|
|
9959
|
+
const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
|
|
9960
|
+
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;
|
|
9961
|
+
const result = {
|
|
9962
|
+
dslCoverageScore,
|
|
9963
|
+
compileScore,
|
|
9964
|
+
reviewScore,
|
|
9965
|
+
harnessScore,
|
|
9966
|
+
promptHash,
|
|
9967
|
+
detail: {
|
|
9968
|
+
endpointsTotal,
|
|
9969
|
+
endpointLayerCovered,
|
|
9970
|
+
modelsTotal,
|
|
9971
|
+
modelLayerCovered,
|
|
9972
|
+
filesWritten: generatedFiles.length
|
|
9973
|
+
}
|
|
9974
|
+
};
|
|
9975
|
+
logger.setHarnessScore(harnessScore);
|
|
9976
|
+
logger.stageEnd("self_eval", {
|
|
9977
|
+
harnessScore,
|
|
9978
|
+
dslCoverageScore,
|
|
9979
|
+
compileScore,
|
|
9980
|
+
reviewScore: reviewScore ?? void 0,
|
|
9981
|
+
promptHash
|
|
9982
|
+
});
|
|
9983
|
+
return result;
|
|
9984
|
+
}
|
|
9985
|
+
function printSelfEval(result) {
|
|
9986
|
+
const scoreColor = result.harnessScore >= 8 ? chalk18.green : result.harnessScore >= 6 ? chalk18.yellow : chalk18.red;
|
|
9987
|
+
const filled = Math.round(result.harnessScore);
|
|
9988
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
9989
|
+
const compileTag = result.compileScore === 10 ? chalk18.green("pass") : chalk18.yellow("partial");
|
|
9990
|
+
const reviewTag = result.reviewScore !== null ? `Review: ${result.reviewScore}/10` : chalk18.gray("Review: skipped");
|
|
9991
|
+
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 : ${scoreColor(`[${bar}] ${result.harnessScore}/10`)}`);
|
|
9993
|
+
console.log(
|
|
9994
|
+
` DSL : ${scoreColor(result.dslCoverageScore + "/10")} Compile: ${compileTag} ${reviewTag}`
|
|
9995
|
+
);
|
|
9996
|
+
console.log(chalk18.gray(` Prompt : ${result.promptHash}`));
|
|
9997
|
+
console.log(chalk18.gray("\u2500".repeat(49)));
|
|
9998
|
+
}
|
|
9999
|
+
|
|
9995
10000
|
// cli/index.ts
|
|
9996
10001
|
dotenv.config();
|
|
9997
10002
|
var CONFIG_FILE = ".ai-spec.json";
|
|
9998
10003
|
async function loadConfig(dir) {
|
|
9999
|
-
const p =
|
|
10000
|
-
if (await
|
|
10001
|
-
return
|
|
10004
|
+
const p = path22.join(dir, CONFIG_FILE);
|
|
10005
|
+
if (await fs23.pathExists(p)) {
|
|
10006
|
+
return fs23.readJson(p);
|
|
10002
10007
|
}
|
|
10003
10008
|
return {};
|
|
10004
10009
|
}
|
|
@@ -10076,7 +10081,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10076
10081
|
} else {
|
|
10077
10082
|
const mockPort = 3001;
|
|
10078
10083
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
10079
|
-
const serverJsPath =
|
|
10084
|
+
const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
10080
10085
|
console.log(chalk19.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
10081
10086
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
10082
10087
|
console.log(chalk19.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
@@ -10127,6 +10132,8 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10127
10132
|
model: specModelName
|
|
10128
10133
|
});
|
|
10129
10134
|
setActiveLogger(runLogger);
|
|
10135
|
+
const promptHash = computePromptHash();
|
|
10136
|
+
runLogger.setPromptHash(promptHash);
|
|
10130
10137
|
console.log(chalk19.blue("[1/6] Loading project context..."));
|
|
10131
10138
|
runLogger.stageStart("context_load");
|
|
10132
10139
|
const loader = new ContextLoader(currentDir);
|
|
@@ -10240,7 +10247,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10240
10247
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10241
10248
|
console.log(chalk19.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10242
10249
|
if (taskCountHint) console.log(chalk19.gray(taskCountHint));
|
|
10243
|
-
const previewSpecsDir =
|
|
10250
|
+
const previewSpecsDir = path22.join(currentDir, "specs");
|
|
10244
10251
|
const slug = featureSlug;
|
|
10245
10252
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10246
10253
|
if (prevVersion) {
|
|
@@ -10318,10 +10325,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10318
10325
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10319
10326
|
console.log(chalk19.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10320
10327
|
}
|
|
10321
|
-
const specsDir =
|
|
10322
|
-
await
|
|
10328
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10329
|
+
await fs23.ensureDir(specsDir);
|
|
10323
10330
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10324
|
-
await
|
|
10331
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10325
10332
|
console.log(chalk19.green(`
|
|
10326
10333
|
[5/6] \u2714 Spec saved: ${specFile}`) + chalk19.gray(` (v${specVersion})`));
|
|
10327
10334
|
let savedDslFile = null;
|
|
@@ -10383,14 +10390,16 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10383
10390
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10384
10391
|
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10385
10392
|
}
|
|
10393
|
+
let compilePassed = false;
|
|
10386
10394
|
if (opts.skipErrorFeedback) {
|
|
10387
10395
|
console.log(chalk19.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10396
|
+
compilePassed = true;
|
|
10388
10397
|
} else {
|
|
10389
10398
|
if (opts.tdd) {
|
|
10390
10399
|
console.log(chalk19.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10391
10400
|
}
|
|
10392
10401
|
runLogger.stageStart("error_feedback");
|
|
10393
|
-
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10402
|
+
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10394
10403
|
maxCycles: opts.tdd ? 3 : 2
|
|
10395
10404
|
// TDD gets one extra cycle
|
|
10396
10405
|
});
|
|
@@ -10401,7 +10410,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10401
10410
|
console.log(chalk19.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10402
10411
|
runLogger.stageStart("review");
|
|
10403
10412
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10404
|
-
const savedSpec = await
|
|
10413
|
+
const savedSpec = await fs23.readFile(specFile, "utf-8");
|
|
10405
10414
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
10406
10415
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
10407
10416
|
} else {
|
|
@@ -10416,6 +10425,16 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10416
10425
|
runLogger.stageEnd("review");
|
|
10417
10426
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10418
10427
|
}
|
|
10428
|
+
runLogger.stageStart("self_eval");
|
|
10429
|
+
const selfEvalResult = runSelfEval({
|
|
10430
|
+
dsl: extractedDsl,
|
|
10431
|
+
generatedFiles,
|
|
10432
|
+
compilePassed,
|
|
10433
|
+
reviewText: reviewResult,
|
|
10434
|
+
promptHash,
|
|
10435
|
+
logger: runLogger
|
|
10436
|
+
});
|
|
10437
|
+
printSelfEval(selfEvalResult);
|
|
10419
10438
|
runLogger.finish();
|
|
10420
10439
|
console.log(chalk19.bold.green("\n\u2714 All done!"));
|
|
10421
10440
|
console.log(chalk19.gray(` Spec : ${specFile}`));
|
|
@@ -10446,17 +10465,17 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10446
10465
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
10447
10466
|
let specContent = "";
|
|
10448
10467
|
let resolvedSpecFile;
|
|
10449
|
-
if (specFile && await
|
|
10450
|
-
specContent = await
|
|
10468
|
+
if (specFile && await fs23.pathExists(specFile)) {
|
|
10469
|
+
specContent = await fs23.readFile(specFile, "utf-8");
|
|
10451
10470
|
resolvedSpecFile = specFile;
|
|
10452
10471
|
console.log(chalk19.gray(`Using spec: ${specFile}`));
|
|
10453
10472
|
} else {
|
|
10454
|
-
const specsDir =
|
|
10455
|
-
if (await
|
|
10456
|
-
const files = (await
|
|
10473
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
10474
|
+
if (await fs23.pathExists(specsDir)) {
|
|
10475
|
+
const files = (await fs23.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
10457
10476
|
if (files.length > 0) {
|
|
10458
|
-
const latest =
|
|
10459
|
-
specContent = await
|
|
10477
|
+
const latest = path22.join(specsDir, files[0]);
|
|
10478
|
+
specContent = await fs23.readFile(latest, "utf-8");
|
|
10460
10479
|
resolvedSpecFile = latest;
|
|
10461
10480
|
console.log(chalk19.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10462
10481
|
}
|
|
@@ -10494,7 +10513,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10494
10513
|
console.log(chalk19.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10495
10514
|
console.log(chalk19.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10496
10515
|
if (result.backupPath) {
|
|
10497
|
-
console.log(chalk19.gray(` Backup: ${
|
|
10516
|
+
console.log(chalk19.gray(` Backup: ${path22.basename(result.backupPath)}`));
|
|
10498
10517
|
}
|
|
10499
10518
|
}
|
|
10500
10519
|
} catch (err) {
|
|
@@ -10520,7 +10539,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10520
10539
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10521
10540
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10522
10541
|
].join("\n");
|
|
10523
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10542
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
|
|
10524
10543
|
let globalConstitution;
|
|
10525
10544
|
try {
|
|
10526
10545
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
@@ -10540,8 +10559,8 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10540
10559
|
}
|
|
10541
10560
|
return;
|
|
10542
10561
|
}
|
|
10543
|
-
const constitutionPath =
|
|
10544
|
-
if (!opts.force && await
|
|
10562
|
+
const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
|
|
10563
|
+
if (!opts.force && await fs23.pathExists(constitutionPath)) {
|
|
10545
10564
|
console.log(chalk19.yellow(`
|
|
10546
10565
|
${CONSTITUTION_FILE} already exists.`));
|
|
10547
10566
|
console.log(chalk19.gray(" Use --force to overwrite it."));
|
|
@@ -10560,7 +10579,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10560
10579
|
process.exit(1);
|
|
10561
10580
|
}
|
|
10562
10581
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10563
|
-
const globalResult = await loadGlobalConstitution([
|
|
10582
|
+
const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
|
|
10564
10583
|
if (globalResult) {
|
|
10565
10584
|
console.log(chalk19.cyan(`
|
|
10566
10585
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
@@ -10582,7 +10601,7 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10582
10601
|
"Default code generation mode (claude-code|api|plan)"
|
|
10583
10602
|
).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) => {
|
|
10584
10603
|
const currentDir = process.cwd();
|
|
10585
|
-
const configPath =
|
|
10604
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10586
10605
|
if (opts.clearKeys) {
|
|
10587
10606
|
await clearAllKeys();
|
|
10588
10607
|
console.log(chalk19.green(`\u2714 All saved API keys cleared.`));
|
|
@@ -10594,7 +10613,7 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10594
10613
|
return;
|
|
10595
10614
|
}
|
|
10596
10615
|
if (opts.listKeys) {
|
|
10597
|
-
const store = await
|
|
10616
|
+
const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10598
10617
|
const providers = Object.keys(store);
|
|
10599
10618
|
if (providers.length === 0) {
|
|
10600
10619
|
console.log(chalk19.gray("No saved API keys."));
|
|
@@ -10610,7 +10629,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10610
10629
|
return;
|
|
10611
10630
|
}
|
|
10612
10631
|
if (opts.reset) {
|
|
10613
|
-
await
|
|
10632
|
+
await fs23.writeJson(configPath, {}, { spaces: 2 });
|
|
10614
10633
|
console.log(chalk19.green(`\u2714 Config reset: ${configPath}`));
|
|
10615
10634
|
return;
|
|
10616
10635
|
}
|
|
@@ -10638,13 +10657,13 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10638
10657
|
}
|
|
10639
10658
|
updated.minSpecScore = score;
|
|
10640
10659
|
}
|
|
10641
|
-
await
|
|
10660
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10642
10661
|
console.log(chalk19.green(`\u2714 Config saved to ${configPath}`));
|
|
10643
10662
|
console.log(JSON.stringify(updated, null, 2));
|
|
10644
10663
|
});
|
|
10645
10664
|
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) => {
|
|
10646
10665
|
const currentDir = process.cwd();
|
|
10647
|
-
const configPath =
|
|
10666
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10648
10667
|
if (opts.list) {
|
|
10649
10668
|
console.log(chalk19.bold("\nAvailable providers & models:\n"));
|
|
10650
10669
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
@@ -10748,7 +10767,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10748
10767
|
console.log(chalk19.gray(" Cancelled."));
|
|
10749
10768
|
return;
|
|
10750
10769
|
}
|
|
10751
|
-
await
|
|
10770
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10752
10771
|
console.log(chalk19.green(`
|
|
10753
10772
|
\u2714 Saved to ${configPath}`));
|
|
10754
10773
|
const providerToCheck = updated.provider ?? "gemini";
|
|
@@ -10843,17 +10862,17 @@ ${contractContextSection}`;
|
|
|
10843
10862
|
} else {
|
|
10844
10863
|
console.log(chalk19.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10845
10864
|
}
|
|
10846
|
-
const specsDir =
|
|
10847
|
-
await
|
|
10865
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10866
|
+
await fs23.ensureDir(specsDir);
|
|
10848
10867
|
const featureSlug = slugify(idea);
|
|
10849
10868
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10850
|
-
await
|
|
10851
|
-
console.log(chalk19.green(` Spec saved: ${
|
|
10869
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10870
|
+
console.log(chalk19.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
|
|
10852
10871
|
let savedDslFile = null;
|
|
10853
10872
|
if (extractedDsl) {
|
|
10854
10873
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10855
10874
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10856
|
-
console.log(chalk19.green(` DSL saved: ${
|
|
10875
|
+
console.log(chalk19.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
|
|
10857
10876
|
}
|
|
10858
10877
|
console.log(chalk19.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10859
10878
|
try {
|
|
@@ -11083,8 +11102,8 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11083
11102
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
11084
11103
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
11085
11104
|
const currentDir = process.cwd();
|
|
11086
|
-
const configPath =
|
|
11087
|
-
if (await
|
|
11105
|
+
const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
11106
|
+
if (await fs23.pathExists(configPath)) {
|
|
11088
11107
|
const overwrite = await confirm2({
|
|
11089
11108
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
11090
11109
|
default: false
|
|
@@ -11165,10 +11184,10 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11165
11184
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
11166
11185
|
default: `./${repoName}`
|
|
11167
11186
|
});
|
|
11168
|
-
const absPath =
|
|
11187
|
+
const absPath = path22.resolve(currentDir, repoPath);
|
|
11169
11188
|
let detectedType = "unknown";
|
|
11170
11189
|
let detectedRole = "shared";
|
|
11171
|
-
if (await
|
|
11190
|
+
if (await fs23.pathExists(absPath)) {
|
|
11172
11191
|
const { type, role } = await detectRepoType(absPath);
|
|
11173
11192
|
detectedType = type;
|
|
11174
11193
|
detectedRole = role;
|
|
@@ -11231,12 +11250,12 @@ workspaceCmd.command("status").description("Show current workspace configuration
|
|
|
11231
11250
|
}
|
|
11232
11251
|
console.log(chalk19.bold(`
|
|
11233
11252
|
Workspace: ${config2.name}`));
|
|
11234
|
-
console.log(chalk19.gray(` Config: ${
|
|
11253
|
+
console.log(chalk19.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11235
11254
|
console.log(chalk19.gray(` Repos (${config2.repos.length}):
|
|
11236
11255
|
`));
|
|
11237
11256
|
for (const repo of config2.repos) {
|
|
11238
11257
|
const absPath = loader.resolveAbsPath(repo);
|
|
11239
|
-
const exists = await
|
|
11258
|
+
const exists = await fs23.pathExists(absPath);
|
|
11240
11259
|
const status = exists ? chalk19.green("found") : chalk19.red("not found");
|
|
11241
11260
|
console.log(
|
|
11242
11261
|
` ${chalk19.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
@@ -11270,14 +11289,14 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11270
11289
|
console.log(chalk19.gray(` Run ID: ${updateRunId}`));
|
|
11271
11290
|
let specPath = opts.spec ?? null;
|
|
11272
11291
|
if (!specPath) {
|
|
11273
|
-
const specsDir =
|
|
11292
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
11274
11293
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11275
11294
|
if (!latest) {
|
|
11276
11295
|
console.error(chalk19.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11277
11296
|
process.exit(1);
|
|
11278
11297
|
}
|
|
11279
11298
|
specPath = latest.filePath;
|
|
11280
|
-
console.log(chalk19.gray(` Using spec: ${
|
|
11299
|
+
console.log(chalk19.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11281
11300
|
}
|
|
11282
11301
|
console.log(chalk19.gray(" Loading project context..."));
|
|
11283
11302
|
const loader = new ContextLoader(currentDir);
|
|
@@ -11299,9 +11318,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11299
11318
|
process.exit(1);
|
|
11300
11319
|
}
|
|
11301
11320
|
console.log(chalk19.green(`
|
|
11302
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11321
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
|
|
11303
11322
|
if (result.newDslPath) {
|
|
11304
|
-
console.log(chalk19.green(` \u2714 DSL updated: ${
|
|
11323
|
+
console.log(chalk19.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
|
|
11305
11324
|
}
|
|
11306
11325
|
if (result.affectedFiles.length > 0) {
|
|
11307
11326
|
console.log(chalk19.cyan("\n Affected files:"));
|
|
@@ -11317,7 +11336,7 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11317
11336
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11318
11337
|
console.log(chalk19.blue("\n Regenerating affected files..."));
|
|
11319
11338
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11320
|
-
const specContent = await
|
|
11339
|
+
const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
|
|
11321
11340
|
const constitutionSection = context.constitution ? `
|
|
11322
11341
|
=== Project Constitution (MUST follow) ===
|
|
11323
11342
|
${context.constitution}
|
|
@@ -11328,10 +11347,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
|
11328
11347
|
` : "";
|
|
11329
11348
|
updateLogger.stageStart("update_codegen");
|
|
11330
11349
|
for (const affected of result.affectedFiles) {
|
|
11331
|
-
const fullPath =
|
|
11350
|
+
const fullPath = path22.join(currentDir, affected.file);
|
|
11332
11351
|
let existing = "";
|
|
11333
11352
|
try {
|
|
11334
|
-
existing = await
|
|
11353
|
+
existing = await fs23.readFile(fullPath, "utf-8");
|
|
11335
11354
|
} catch {
|
|
11336
11355
|
}
|
|
11337
11356
|
const codePrompt = `Apply this change to the file.
|
|
@@ -11350,9 +11369,9 @@ ${existing || "Create from scratch."}`;
|
|
|
11350
11369
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11351
11370
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11352
11371
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11353
|
-
await
|
|
11372
|
+
await fs23.ensureDir(path22.dirname(fullPath));
|
|
11354
11373
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11355
|
-
await
|
|
11374
|
+
await fs23.writeFile(fullPath, content, "utf-8");
|
|
11356
11375
|
updateLogger.fileWritten(affected.file);
|
|
11357
11376
|
console.log(chalk19.green("\u2714"));
|
|
11358
11377
|
} catch (err) {
|
|
@@ -11361,7 +11380,7 @@ ${existing || "Create from scratch."}`;
|
|
|
11361
11380
|
}
|
|
11362
11381
|
}
|
|
11363
11382
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11364
|
-
const updatedSpecContent = await
|
|
11383
|
+
const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11365
11384
|
if (updatedSpecContent) {
|
|
11366
11385
|
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11367
11386
|
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
@@ -11391,11 +11410,11 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11391
11410
|
console.error(chalk19.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11392
11411
|
process.exit(1);
|
|
11393
11412
|
}
|
|
11394
|
-
console.log(chalk19.gray(` Using DSL: ${
|
|
11413
|
+
console.log(chalk19.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11395
11414
|
}
|
|
11396
11415
|
let dsl;
|
|
11397
11416
|
try {
|
|
11398
|
-
dsl = await
|
|
11417
|
+
dsl = await fs23.readJson(dslPath);
|
|
11399
11418
|
} catch (err) {
|
|
11400
11419
|
console.error(chalk19.red(` Failed to read DSL: ${err.message}`));
|
|
11401
11420
|
process.exit(1);
|
|
@@ -11409,7 +11428,7 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11409
11428
|
serverUrl,
|
|
11410
11429
|
outputPath: opts.output
|
|
11411
11430
|
});
|
|
11412
|
-
const rel =
|
|
11431
|
+
const rel = path22.relative(currentDir, outputPath);
|
|
11413
11432
|
console.log(chalk19.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11414
11433
|
console.log(chalk19.gray(` Feature : ${dsl.feature.title}`));
|
|
11415
11434
|
console.log(chalk19.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
@@ -11428,7 +11447,7 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11428
11447
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11429
11448
|
console.log(chalk19.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"));
|
|
11430
11449
|
if (opts.restore) {
|
|
11431
|
-
const frontendDir = opts.frontend ?
|
|
11450
|
+
const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
|
|
11432
11451
|
const r = await restoreMockProxy(frontendDir);
|
|
11433
11452
|
if (r.restored) {
|
|
11434
11453
|
console.log(chalk19.green(" \u2714 Proxy restored and mock server stopped."));
|
|
@@ -11458,7 +11477,7 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11458
11477
|
console.log(chalk19.yellow(` No DSL file found \u2014 skipping.`));
|
|
11459
11478
|
continue;
|
|
11460
11479
|
}
|
|
11461
|
-
const dsl2 = await
|
|
11480
|
+
const dsl2 = await fs23.readJson(dslFile);
|
|
11462
11481
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
11463
11482
|
port,
|
|
11464
11483
|
msw: opts.msw,
|
|
@@ -11482,11 +11501,11 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11482
11501
|
);
|
|
11483
11502
|
process.exit(1);
|
|
11484
11503
|
}
|
|
11485
|
-
console.log(chalk19.gray(` Using DSL: ${
|
|
11504
|
+
console.log(chalk19.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11486
11505
|
}
|
|
11487
11506
|
let dsl;
|
|
11488
11507
|
try {
|
|
11489
|
-
dsl = await
|
|
11508
|
+
dsl = await fs23.readJson(dslPath);
|
|
11490
11509
|
} catch (err) {
|
|
11491
11510
|
console.error(chalk19.red(` Failed to read DSL file: ${err.message}`));
|
|
11492
11511
|
process.exit(1);
|
|
@@ -11503,8 +11522,8 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11503
11522
|
console.log(chalk19.gray(` ${f.description}`));
|
|
11504
11523
|
}
|
|
11505
11524
|
if (opts.serve) {
|
|
11506
|
-
const serverJsPath =
|
|
11507
|
-
if (!await
|
|
11525
|
+
const serverJsPath = path22.join(currentDir, "mock", "server.js");
|
|
11526
|
+
if (!await fs23.pathExists(serverJsPath)) {
|
|
11508
11527
|
console.error(chalk19.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11509
11528
|
process.exit(1);
|
|
11510
11529
|
}
|
|
@@ -11512,7 +11531,7 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11512
11531
|
console.log(chalk19.green(`
|
|
11513
11532
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11514
11533
|
if (opts.frontend) {
|
|
11515
|
-
const frontendDir =
|
|
11534
|
+
const frontendDir = path22.resolve(opts.frontend);
|
|
11516
11535
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11517
11536
|
await saveMockServerPid(frontendDir, pid);
|
|
11518
11537
|
if (proxyResult.applied) {
|
|
@@ -11584,13 +11603,5 @@ program.command("restore").description("Restore files modified by a previous run
|
|
|
11584
11603
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11585
11604
|
}
|
|
11586
11605
|
});
|
|
11587
|
-
|
|
11588
|
-
(async () => {
|
|
11589
|
-
const currentDir = process.cwd();
|
|
11590
|
-
const config2 = await loadConfig(currentDir);
|
|
11591
|
-
await printWelcome(currentDir, config2);
|
|
11592
|
-
})();
|
|
11593
|
-
} else {
|
|
11594
|
-
program.parse();
|
|
11595
|
-
}
|
|
11606
|
+
program.parse();
|
|
11596
11607
|
//# sourceMappingURL=index.mjs.map
|