ai-spec-dev 0.29.1 → 0.30.1
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 +4 -3
- package/RELEASE_LOG.md +24 -0
- package/cli/index.ts +1 -11
- package/core/error-feedback.ts +102 -2
- package/core/frontend-context-loader.ts +117 -20
- package/dist/cli/index.js +649 -655
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +649 -655
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js +57 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/purpose.md +29 -4
- package/cli/welcome.ts +0 -151
package/dist/cli/index.js
CHANGED
|
@@ -423,8 +423,8 @@ var init_workspace_loader = __esm({
|
|
|
423
423
|
const repos = [];
|
|
424
424
|
for (const entry of entries) {
|
|
425
425
|
const absPath = path17.join(this.workspaceRoot, entry);
|
|
426
|
-
const
|
|
427
|
-
if (!
|
|
426
|
+
const stat3 = await fs18.stat(absPath).catch(() => null);
|
|
427
|
+
if (!stat3 || !stat3.isDirectory()) continue;
|
|
428
428
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
429
429
|
if (names && !names.includes(entry)) continue;
|
|
430
430
|
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"));
|
|
@@ -488,9 +488,9 @@ var init_workspace_loader = __esm({
|
|
|
488
488
|
|
|
489
489
|
// cli/index.ts
|
|
490
490
|
var import_commander = require("commander");
|
|
491
|
-
var
|
|
492
|
-
var
|
|
493
|
-
var
|
|
491
|
+
var path22 = __toESM(require("path"));
|
|
492
|
+
var fs23 = __toESM(require("fs-extra"));
|
|
493
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
494
494
|
var dotenv = __toESM(require("dotenv"));
|
|
495
495
|
var import_prompts3 = require("@inquirer/prompts");
|
|
496
496
|
|
|
@@ -4870,221 +4870,221 @@ function validateDsl(raw) {
|
|
|
4870
4870
|
}
|
|
4871
4871
|
return { valid: true, dsl: raw };
|
|
4872
4872
|
}
|
|
4873
|
-
function validateFeature(raw,
|
|
4873
|
+
function validateFeature(raw, path23, errors) {
|
|
4874
4874
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4875
|
-
errors.push({ path:
|
|
4875
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4876
4876
|
return;
|
|
4877
4877
|
}
|
|
4878
4878
|
const f = raw;
|
|
4879
|
-
requireNonEmptyString(f["id"], `${
|
|
4880
|
-
requireNonEmptyString(f["title"], `${
|
|
4881
|
-
requireNonEmptyString(f["description"], `${
|
|
4879
|
+
requireNonEmptyString(f["id"], `${path23}.id`, errors);
|
|
4880
|
+
requireNonEmptyString(f["title"], `${path23}.title`, errors);
|
|
4881
|
+
requireNonEmptyString(f["description"], `${path23}.description`, errors);
|
|
4882
4882
|
}
|
|
4883
|
-
function validateModel(raw,
|
|
4883
|
+
function validateModel(raw, path23, errors) {
|
|
4884
4884
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4885
|
-
errors.push({ path:
|
|
4885
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4886
4886
|
return;
|
|
4887
4887
|
}
|
|
4888
4888
|
const m = raw;
|
|
4889
|
-
requireNonEmptyString(m["name"], `${
|
|
4889
|
+
requireNonEmptyString(m["name"], `${path23}.name`, errors);
|
|
4890
4890
|
if (!Array.isArray(m["fields"])) {
|
|
4891
|
-
errors.push({ path: `${
|
|
4891
|
+
errors.push({ path: `${path23}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4892
4892
|
} else {
|
|
4893
4893
|
const fields = m["fields"];
|
|
4894
4894
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4895
|
-
errors.push({ path: `${
|
|
4895
|
+
errors.push({ path: `${path23}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4896
4896
|
}
|
|
4897
4897
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4898
|
-
validateModelField(fields[j2], `${
|
|
4898
|
+
validateModelField(fields[j2], `${path23}.fields[${j2}]`, errors);
|
|
4899
4899
|
}
|
|
4900
4900
|
}
|
|
4901
4901
|
if (m["relations"] !== void 0) {
|
|
4902
4902
|
if (!Array.isArray(m["relations"])) {
|
|
4903
|
-
errors.push({ path: `${
|
|
4903
|
+
errors.push({ path: `${path23}.relations`, message: "Must be an array of strings if present" });
|
|
4904
4904
|
} else {
|
|
4905
4905
|
const rels = m["relations"];
|
|
4906
4906
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4907
4907
|
if (typeof rels[j2] !== "string") {
|
|
4908
|
-
errors.push({ path: `${
|
|
4908
|
+
errors.push({ path: `${path23}.relations[${j2}]`, message: "Must be a string" });
|
|
4909
4909
|
}
|
|
4910
4910
|
}
|
|
4911
4911
|
}
|
|
4912
4912
|
}
|
|
4913
4913
|
}
|
|
4914
|
-
function validateModelField(raw,
|
|
4914
|
+
function validateModelField(raw, path23, errors) {
|
|
4915
4915
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4916
|
-
errors.push({ path:
|
|
4916
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4917
4917
|
return;
|
|
4918
4918
|
}
|
|
4919
4919
|
const f = raw;
|
|
4920
|
-
requireNonEmptyString(f["name"], `${
|
|
4921
|
-
requireNonEmptyString(f["type"], `${
|
|
4920
|
+
requireNonEmptyString(f["name"], `${path23}.name`, errors);
|
|
4921
|
+
requireNonEmptyString(f["type"], `${path23}.type`, errors);
|
|
4922
4922
|
if (typeof f["required"] !== "boolean") {
|
|
4923
|
-
errors.push({ path: `${
|
|
4923
|
+
errors.push({ path: `${path23}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4924
4924
|
}
|
|
4925
4925
|
}
|
|
4926
|
-
function validateEndpoint(raw,
|
|
4926
|
+
function validateEndpoint(raw, path23, errors) {
|
|
4927
4927
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4928
|
-
errors.push({ path:
|
|
4928
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4929
4929
|
return;
|
|
4930
4930
|
}
|
|
4931
4931
|
const e = raw;
|
|
4932
|
-
requireNonEmptyString(e["id"], `${
|
|
4933
|
-
requireNonEmptyString(e["description"], `${
|
|
4932
|
+
requireNonEmptyString(e["id"], `${path23}.id`, errors);
|
|
4933
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4934
4934
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4935
4935
|
errors.push({
|
|
4936
|
-
path: `${
|
|
4936
|
+
path: `${path23}.method`,
|
|
4937
4937
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4938
4938
|
});
|
|
4939
4939
|
}
|
|
4940
4940
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4941
4941
|
errors.push({
|
|
4942
|
-
path: `${
|
|
4942
|
+
path: `${path23}.path`,
|
|
4943
4943
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4944
4944
|
});
|
|
4945
4945
|
}
|
|
4946
4946
|
if (typeof e["auth"] !== "boolean") {
|
|
4947
|
-
errors.push({ path: `${
|
|
4947
|
+
errors.push({ path: `${path23}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4948
4948
|
}
|
|
4949
4949
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4950
4950
|
errors.push({
|
|
4951
|
-
path: `${
|
|
4951
|
+
path: `${path23}.successStatus`,
|
|
4952
4952
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4953
4953
|
});
|
|
4954
4954
|
}
|
|
4955
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4955
|
+
requireNonEmptyString(e["successDescription"], `${path23}.successDescription`, errors);
|
|
4956
4956
|
if (e["request"] !== void 0) {
|
|
4957
|
-
validateRequestSchema(e["request"], `${
|
|
4957
|
+
validateRequestSchema(e["request"], `${path23}.request`, errors);
|
|
4958
4958
|
}
|
|
4959
4959
|
if (e["errors"] !== void 0) {
|
|
4960
4960
|
if (!Array.isArray(e["errors"])) {
|
|
4961
|
-
errors.push({ path: `${
|
|
4961
|
+
errors.push({ path: `${path23}.errors`, message: "Must be an array if present" });
|
|
4962
4962
|
} else {
|
|
4963
4963
|
const errs = e["errors"];
|
|
4964
4964
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4965
|
-
errors.push({ path: `${
|
|
4965
|
+
errors.push({ path: `${path23}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4966
4966
|
}
|
|
4967
4967
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4968
|
-
validateResponseError(errs[j2], `${
|
|
4968
|
+
validateResponseError(errs[j2], `${path23}.errors[${j2}]`, errors);
|
|
4969
4969
|
}
|
|
4970
4970
|
}
|
|
4971
4971
|
}
|
|
4972
4972
|
}
|
|
4973
|
-
function validateRequestSchema(raw,
|
|
4973
|
+
function validateRequestSchema(raw, path23, errors) {
|
|
4974
4974
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4975
|
-
errors.push({ path:
|
|
4975
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4976
4976
|
return;
|
|
4977
4977
|
}
|
|
4978
4978
|
const r = raw;
|
|
4979
4979
|
for (const key of ["body", "query", "params"]) {
|
|
4980
4980
|
if (r[key] !== void 0) {
|
|
4981
|
-
validateFieldMap(r[key], `${
|
|
4981
|
+
validateFieldMap(r[key], `${path23}.${key}`, errors);
|
|
4982
4982
|
}
|
|
4983
4983
|
}
|
|
4984
4984
|
}
|
|
4985
|
-
function validateFieldMap(raw,
|
|
4985
|
+
function validateFieldMap(raw, path23, errors) {
|
|
4986
4986
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4987
|
-
errors.push({ path:
|
|
4987
|
+
errors.push({ path: path23, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4988
4988
|
return;
|
|
4989
4989
|
}
|
|
4990
4990
|
const map = raw;
|
|
4991
4991
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4992
4992
|
if (typeof v2 !== "string") {
|
|
4993
|
-
errors.push({ path: `${
|
|
4993
|
+
errors.push({ path: `${path23}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4994
4994
|
}
|
|
4995
4995
|
}
|
|
4996
4996
|
}
|
|
4997
|
-
function validateResponseError(raw,
|
|
4997
|
+
function validateResponseError(raw, path23, errors) {
|
|
4998
4998
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4999
|
-
errors.push({ path:
|
|
4999
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5000
5000
|
return;
|
|
5001
5001
|
}
|
|
5002
5002
|
const e = raw;
|
|
5003
5003
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5004
|
-
errors.push({ path: `${
|
|
5004
|
+
errors.push({ path: `${path23}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5005
5005
|
}
|
|
5006
|
-
requireNonEmptyString(e["code"], `${
|
|
5007
|
-
requireNonEmptyString(e["description"], `${
|
|
5006
|
+
requireNonEmptyString(e["code"], `${path23}.code`, errors);
|
|
5007
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
5008
5008
|
}
|
|
5009
|
-
function validateBehavior(raw,
|
|
5009
|
+
function validateBehavior(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 b = raw;
|
|
5015
|
-
requireNonEmptyString(b["id"], `${
|
|
5016
|
-
requireNonEmptyString(b["description"], `${
|
|
5015
|
+
requireNonEmptyString(b["id"], `${path23}.id`, errors);
|
|
5016
|
+
requireNonEmptyString(b["description"], `${path23}.description`, errors);
|
|
5017
5017
|
if (b["constraints"] !== void 0) {
|
|
5018
5018
|
if (!Array.isArray(b["constraints"])) {
|
|
5019
|
-
errors.push({ path: `${
|
|
5019
|
+
errors.push({ path: `${path23}.constraints`, message: "Must be an array of strings if present" });
|
|
5020
5020
|
} else {
|
|
5021
5021
|
const cs2 = b["constraints"];
|
|
5022
5022
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5023
5023
|
if (typeof cs2[j2] !== "string") {
|
|
5024
|
-
errors.push({ path: `${
|
|
5024
|
+
errors.push({ path: `${path23}.constraints[${j2}]`, message: "Must be a string" });
|
|
5025
5025
|
}
|
|
5026
5026
|
}
|
|
5027
5027
|
}
|
|
5028
5028
|
}
|
|
5029
5029
|
}
|
|
5030
|
-
function validateComponent(raw,
|
|
5030
|
+
function validateComponent(raw, path23, errors) {
|
|
5031
5031
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5032
|
-
errors.push({ path:
|
|
5032
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5033
5033
|
return;
|
|
5034
5034
|
}
|
|
5035
5035
|
const c = raw;
|
|
5036
|
-
requireNonEmptyString(c["id"], `${
|
|
5037
|
-
requireNonEmptyString(c["name"], `${
|
|
5038
|
-
requireNonEmptyString(c["description"], `${
|
|
5036
|
+
requireNonEmptyString(c["id"], `${path23}.id`, errors);
|
|
5037
|
+
requireNonEmptyString(c["name"], `${path23}.name`, errors);
|
|
5038
|
+
requireNonEmptyString(c["description"], `${path23}.description`, errors);
|
|
5039
5039
|
if (c["props"] !== void 0) {
|
|
5040
5040
|
if (!Array.isArray(c["props"])) {
|
|
5041
|
-
errors.push({ path: `${
|
|
5041
|
+
errors.push({ path: `${path23}.props`, message: "Must be an array if present" });
|
|
5042
5042
|
} else {
|
|
5043
5043
|
const props = c["props"];
|
|
5044
5044
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5045
5045
|
const p = props[j2];
|
|
5046
5046
|
if (typeof p !== "object" || p === null) {
|
|
5047
|
-
errors.push({ path: `${
|
|
5047
|
+
errors.push({ path: `${path23}.props[${j2}]`, message: "Must be an object" });
|
|
5048
5048
|
continue;
|
|
5049
5049
|
}
|
|
5050
|
-
requireNonEmptyString(p["name"], `${
|
|
5051
|
-
requireNonEmptyString(p["type"], `${
|
|
5050
|
+
requireNonEmptyString(p["name"], `${path23}.props[${j2}].name`, errors);
|
|
5051
|
+
requireNonEmptyString(p["type"], `${path23}.props[${j2}].type`, errors);
|
|
5052
5052
|
if (typeof p["required"] !== "boolean") {
|
|
5053
|
-
errors.push({ path: `${
|
|
5053
|
+
errors.push({ path: `${path23}.props[${j2}].required`, message: "Must be boolean" });
|
|
5054
5054
|
}
|
|
5055
5055
|
}
|
|
5056
5056
|
}
|
|
5057
5057
|
}
|
|
5058
5058
|
if (c["events"] !== void 0) {
|
|
5059
5059
|
if (!Array.isArray(c["events"])) {
|
|
5060
|
-
errors.push({ path: `${
|
|
5060
|
+
errors.push({ path: `${path23}.events`, message: "Must be an array if present" });
|
|
5061
5061
|
} else {
|
|
5062
5062
|
const events = c["events"];
|
|
5063
5063
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5064
5064
|
const e = events[j2];
|
|
5065
5065
|
if (typeof e !== "object" || e === null) {
|
|
5066
|
-
errors.push({ path: `${
|
|
5066
|
+
errors.push({ path: `${path23}.events[${j2}]`, message: "Must be an object" });
|
|
5067
5067
|
continue;
|
|
5068
5068
|
}
|
|
5069
|
-
requireNonEmptyString(e["name"], `${
|
|
5069
|
+
requireNonEmptyString(e["name"], `${path23}.events[${j2}].name`, errors);
|
|
5070
5070
|
}
|
|
5071
5071
|
}
|
|
5072
5072
|
}
|
|
5073
5073
|
if (c["state"] !== void 0) {
|
|
5074
5074
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5075
|
-
errors.push({ path: `${
|
|
5075
|
+
errors.push({ path: `${path23}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5076
5076
|
}
|
|
5077
5077
|
}
|
|
5078
5078
|
if (c["apiCalls"] !== void 0) {
|
|
5079
5079
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5080
|
-
errors.push({ path: `${
|
|
5080
|
+
errors.push({ path: `${path23}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5081
5081
|
}
|
|
5082
5082
|
}
|
|
5083
5083
|
}
|
|
5084
|
-
function requireNonEmptyString(v2,
|
|
5084
|
+
function requireNonEmptyString(v2, path23, errors) {
|
|
5085
5085
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5086
5086
|
errors.push({
|
|
5087
|
-
path:
|
|
5087
|
+
path: path23,
|
|
5088
5088
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5089
5089
|
});
|
|
5090
5090
|
}
|
|
@@ -5518,6 +5518,41 @@ async function loadDslForSpec(specFilePath) {
|
|
|
5518
5518
|
// core/frontend-context-loader.ts
|
|
5519
5519
|
var fs7 = __toESM(require("fs-extra"));
|
|
5520
5520
|
var path6 = __toESM(require("path"));
|
|
5521
|
+
function parseImportStatements(content) {
|
|
5522
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, (m) => "\n".repeat(m.split("\n").length - 1));
|
|
5523
|
+
const collapsed = stripped.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
5524
|
+
const results = [];
|
|
5525
|
+
for (const rawLine of collapsed.split("\n")) {
|
|
5526
|
+
const line = rawLine.trim();
|
|
5527
|
+
if (!line) continue;
|
|
5528
|
+
if (/^import\s+type\b/.test(line)) continue;
|
|
5529
|
+
if (!line.startsWith("import")) continue;
|
|
5530
|
+
const match = line.match(/^(import\s+([\s\S]+?)\s+from\s+['"]([^'"]+)['"])/);
|
|
5531
|
+
if (!match) continue;
|
|
5532
|
+
results.push({
|
|
5533
|
+
line: match[1],
|
|
5534
|
+
modulePath: match[3],
|
|
5535
|
+
specifiers: match[2]
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5538
|
+
return results;
|
|
5539
|
+
}
|
|
5540
|
+
var HTTP_MODULE_PATTERNS = [
|
|
5541
|
+
// Project path aliases (@/, @@/, ~/, #/) — catches '@/utils/request', '~/lib/http', etc.
|
|
5542
|
+
/^(?:@{1,2}|~|#)[/\\]/,
|
|
5543
|
+
// Well-known HTTP libraries (exact name match)
|
|
5544
|
+
/^(?:axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)$/,
|
|
5545
|
+
// Relative imports whose path contains an HTTP-utility keyword
|
|
5546
|
+
/\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*/
|
|
5547
|
+
];
|
|
5548
|
+
function findHttpClientImport(content) {
|
|
5549
|
+
for (const stmt of parseImportStatements(content)) {
|
|
5550
|
+
if (HTTP_MODULE_PATTERNS.some((p) => p.test(stmt.modulePath))) {
|
|
5551
|
+
return stmt.line;
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
return void 0;
|
|
5555
|
+
}
|
|
5521
5556
|
var STATE_MANAGEMENT_LIBS = [
|
|
5522
5557
|
"zustand",
|
|
5523
5558
|
"redux",
|
|
@@ -5706,13 +5741,12 @@ ${preview}`);
|
|
|
5706
5741
|
} catch {
|
|
5707
5742
|
}
|
|
5708
5743
|
}
|
|
5709
|
-
const httpImportRegex = /^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
|
|
5710
5744
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
5711
5745
|
try {
|
|
5712
5746
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5713
|
-
const
|
|
5714
|
-
if (
|
|
5715
|
-
ctx.httpClientImport =
|
|
5747
|
+
const found = findHttpClientImport(content);
|
|
5748
|
+
if (found) {
|
|
5749
|
+
ctx.httpClientImport = found;
|
|
5716
5750
|
break;
|
|
5717
5751
|
}
|
|
5718
5752
|
} catch {
|
|
@@ -5842,13 +5876,28 @@ async function extractRouteModuleContext(projectRoot, ctx) {
|
|
|
5842
5876
|
moduleFiles.push(...files);
|
|
5843
5877
|
}
|
|
5844
5878
|
if (moduleFiles.length === 0) return;
|
|
5845
|
-
const
|
|
5879
|
+
const dynamicLayoutRegex = /const\s+Layout\s*=\s*(?:defineAsyncComponent\s*\(\s*)?(?:\(\s*\))?\s*(?:=>|function[^(]*\()\s*(?:[^)]*\))?\s*(?:=>)?\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
5846
5880
|
for (const relPath of moduleFiles) {
|
|
5847
5881
|
try {
|
|
5848
5882
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5849
|
-
const
|
|
5850
|
-
|
|
5851
|
-
|
|
5883
|
+
const stmts = parseImportStatements(content);
|
|
5884
|
+
const staticLayout = stmts.find(
|
|
5885
|
+
(s) => /\bLayout\b/.test(s.specifiers) && /layout/i.test(s.modulePath)
|
|
5886
|
+
);
|
|
5887
|
+
if (staticLayout) {
|
|
5888
|
+
ctx.layoutImport = staticLayout.line;
|
|
5889
|
+
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5890
|
+
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5891
|
+
break;
|
|
5892
|
+
}
|
|
5893
|
+
const singleLine = content.replace(
|
|
5894
|
+
/const\s+Layout\s*=[\s\S]*?import\s*\([^)]+\)/gm,
|
|
5895
|
+
(m) => m.replace(/\n\s*/g, " ")
|
|
5896
|
+
);
|
|
5897
|
+
const dynMatch = singleLine.match(dynamicLayoutRegex);
|
|
5898
|
+
if (dynMatch) {
|
|
5899
|
+
const constMatch = content.match(/^const\s+Layout\s*=.+/m);
|
|
5900
|
+
ctx.layoutImport = constMatch ? constMatch[0].trim() : dynMatch[0].trim();
|
|
5852
5901
|
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5853
5902
|
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5854
5903
|
break;
|
|
@@ -7833,6 +7882,60 @@ function parseErrors(output, source) {
|
|
|
7833
7882
|
}
|
|
7834
7883
|
return errors;
|
|
7835
7884
|
}
|
|
7885
|
+
function parseRelativeImports(content, fromFileRel) {
|
|
7886
|
+
const relDir = path15.dirname(fromFileRel);
|
|
7887
|
+
const results = [];
|
|
7888
|
+
const normalized = content.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
7889
|
+
for (const line of normalized.split("\n")) {
|
|
7890
|
+
const trimmed = line.trim();
|
|
7891
|
+
if (/^import\s+type\b/.test(trimmed)) continue;
|
|
7892
|
+
const match = trimmed.match(/^import\b[^'"]*from\s+['"](\.\.?\/[^'"]+)['"]/);
|
|
7893
|
+
if (!match) continue;
|
|
7894
|
+
const resolved = path15.normalize(path15.join(relDir, match[1]));
|
|
7895
|
+
results.push(resolved);
|
|
7896
|
+
}
|
|
7897
|
+
return results;
|
|
7898
|
+
}
|
|
7899
|
+
async function buildRepairOrder(errorsByFile, workingDir) {
|
|
7900
|
+
const files = Array.from(errorsByFile.keys());
|
|
7901
|
+
if (files.length <= 1) return Array.from(errorsByFile.entries());
|
|
7902
|
+
const deps = new Map(files.map((f) => [f, []]));
|
|
7903
|
+
for (const file of files) {
|
|
7904
|
+
try {
|
|
7905
|
+
const content = await fs16.readFile(path15.join(workingDir, file), "utf-8");
|
|
7906
|
+
const importedPaths = parseRelativeImports(content, file);
|
|
7907
|
+
for (const importedPath of importedPaths) {
|
|
7908
|
+
const matched = files.find((f) => {
|
|
7909
|
+
if (f === file) return false;
|
|
7910
|
+
const fNoExt = f.replace(/\.[^.]+$/, "");
|
|
7911
|
+
return importedPath === fNoExt || importedPath === f || `${importedPath}.ts` === f || `${importedPath}.tsx` === f || `${importedPath}.js` === f || `${importedPath}.jsx` === f;
|
|
7912
|
+
});
|
|
7913
|
+
if (matched) deps.get(file).push(matched);
|
|
7914
|
+
}
|
|
7915
|
+
} catch {
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
const dependents = new Map(files.map((f) => [f, []]));
|
|
7919
|
+
for (const [file, fileDeps] of deps) {
|
|
7920
|
+
for (const dep of fileDeps) dependents.get(dep).push(file);
|
|
7921
|
+
}
|
|
7922
|
+
const inDegree = new Map(files.map((f) => [f, deps.get(f).length]));
|
|
7923
|
+
const queue = files.filter((f) => inDegree.get(f) === 0);
|
|
7924
|
+
const sorted = [];
|
|
7925
|
+
while (queue.length > 0) {
|
|
7926
|
+
const file = queue.shift();
|
|
7927
|
+
sorted.push(file);
|
|
7928
|
+
for (const dependent of dependents.get(file) ?? []) {
|
|
7929
|
+
const degree = (inDegree.get(dependent) ?? 1) - 1;
|
|
7930
|
+
inDegree.set(dependent, degree);
|
|
7931
|
+
if (degree === 0) queue.push(dependent);
|
|
7932
|
+
}
|
|
7933
|
+
}
|
|
7934
|
+
for (const f of files) {
|
|
7935
|
+
if (!sorted.includes(f)) sorted.push(f);
|
|
7936
|
+
}
|
|
7937
|
+
return sorted.map((f) => [f, errorsByFile.get(f)]);
|
|
7938
|
+
}
|
|
7836
7939
|
async function attemptFix(provider, errors, workingDir, dsl) {
|
|
7837
7940
|
const results = [];
|
|
7838
7941
|
const errorsByFile = /* @__PURE__ */ new Map();
|
|
@@ -7841,7 +7944,8 @@ async function attemptFix(provider, errors, workingDir, dsl) {
|
|
|
7841
7944
|
if (!errorsByFile.has(file)) errorsByFile.set(file, []);
|
|
7842
7945
|
errorsByFile.get(file).push(err);
|
|
7843
7946
|
}
|
|
7844
|
-
|
|
7947
|
+
const sortedEntries = await buildRepairOrder(errorsByFile, workingDir);
|
|
7948
|
+
for (const [file, fileErrors] of sortedEntries) {
|
|
7845
7949
|
const fullPath = path15.join(workingDir, file);
|
|
7846
7950
|
let existingContent = "";
|
|
7847
7951
|
try {
|
|
@@ -8235,108 +8339,6 @@ async function clearKey(provider) {
|
|
|
8235
8339
|
await writeStore(store);
|
|
8236
8340
|
}
|
|
8237
8341
|
|
|
8238
|
-
// cli/welcome.ts
|
|
8239
|
-
var import_chalk17 = __toESM(require("chalk"));
|
|
8240
|
-
var os4 = __toESM(require("os"));
|
|
8241
|
-
var path19 = __toESM(require("path"));
|
|
8242
|
-
var fs20 = __toESM(require("fs-extra"));
|
|
8243
|
-
var VERSION = "0.14.1";
|
|
8244
|
-
var TOTAL_W = 76;
|
|
8245
|
-
var L_WIDTH = 44;
|
|
8246
|
-
var R_WIDTH = TOTAL_W - L_WIDTH - 4;
|
|
8247
|
-
var ROBOT_COLOR = import_chalk17.default.hex("#E8885A");
|
|
8248
|
-
var ROBOT = [
|
|
8249
|
-
ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
|
|
8250
|
-
ROBOT_COLOR(" \u2502") + import_chalk17.default.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + import_chalk17.default.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
|
|
8251
|
-
ROBOT_COLOR(" \u2502") + import_chalk17.default.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
|
|
8252
|
-
ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
|
|
8253
|
-
ROBOT_COLOR(" \u2502"),
|
|
8254
|
-
ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
|
|
8255
|
-
];
|
|
8256
|
-
function visLen(s) {
|
|
8257
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
8258
|
-
}
|
|
8259
|
-
function padR(s, width) {
|
|
8260
|
-
const vl = visLen(s);
|
|
8261
|
-
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
8262
|
-
}
|
|
8263
|
-
function row(left, right) {
|
|
8264
|
-
return padR(left, L_WIDTH) + " " + import_chalk17.default.gray("\u2502") + " " + padR(right, R_WIDTH);
|
|
8265
|
-
}
|
|
8266
|
-
function center(s, width) {
|
|
8267
|
-
const vl = visLen(s);
|
|
8268
|
-
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
8269
|
-
return " ".repeat(pad) + s;
|
|
8270
|
-
}
|
|
8271
|
-
async function getRecentSpecs(dir) {
|
|
8272
|
-
const specsDir = path19.join(dir, "specs");
|
|
8273
|
-
if (!await fs20.pathExists(specsDir)) return [];
|
|
8274
|
-
try {
|
|
8275
|
-
const files = await fs20.readdir(specsDir);
|
|
8276
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
8277
|
-
const withStats = await Promise.all(
|
|
8278
|
-
mdFiles.map(async (f) => {
|
|
8279
|
-
const stat4 = await fs20.stat(path19.join(specsDir, f));
|
|
8280
|
-
return { name: f, mtime: stat4.mtime.getTime() };
|
|
8281
|
-
})
|
|
8282
|
-
);
|
|
8283
|
-
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
8284
|
-
return withStats.slice(0, 3).map(({ name, mtime }) => {
|
|
8285
|
-
const ms2 = Date.now() - mtime;
|
|
8286
|
-
const hours = Math.floor(ms2 / 36e5);
|
|
8287
|
-
const days = Math.floor(hours / 24);
|
|
8288
|
-
const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
|
|
8289
|
-
const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
|
|
8290
|
-
return import_chalk17.default.white(slug) + import_chalk17.default.dim(" " + age);
|
|
8291
|
-
});
|
|
8292
|
-
} catch {
|
|
8293
|
-
return [];
|
|
8294
|
-
}
|
|
8295
|
-
}
|
|
8296
|
-
async function printWelcome(currentDir, config2) {
|
|
8297
|
-
const username = os4.userInfo().username;
|
|
8298
|
-
const homeDir = os4.homedir();
|
|
8299
|
-
const shortDir = currentDir.startsWith(homeDir) ? "~" + currentDir.slice(homeDir.length) : currentDir;
|
|
8300
|
-
const recentSpecs = await getRecentSpecs(currentDir);
|
|
8301
|
-
const providerBit = config2?.provider ? config2.provider + (config2.model ? " \xB7 " + config2.model : "") : "";
|
|
8302
|
-
const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
|
|
8303
|
-
const maxInfoLen = L_WIDTH - 2;
|
|
8304
|
-
const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
|
|
8305
|
-
const bottomLine = " " + import_chalk17.default.dim(bottomTruncated);
|
|
8306
|
-
const titleInner = `ai-spec v${VERSION} `;
|
|
8307
|
-
const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
|
|
8308
|
-
console.log(
|
|
8309
|
-
"\n" + import_chalk17.default.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
|
|
8310
|
-
);
|
|
8311
|
-
const welcomeText = "Welcome back, " + import_chalk17.default.bold.white(username) + "!";
|
|
8312
|
-
const leftLines = [
|
|
8313
|
-
"",
|
|
8314
|
-
center(welcomeText, L_WIDTH),
|
|
8315
|
-
"",
|
|
8316
|
-
...ROBOT,
|
|
8317
|
-
"",
|
|
8318
|
-
bottomLine,
|
|
8319
|
-
""
|
|
8320
|
-
];
|
|
8321
|
-
const rightLines = [
|
|
8322
|
-
import_chalk17.default.hex("#FF8C00").bold("Tips for getting started"),
|
|
8323
|
-
import_chalk17.default.gray("\u2500".repeat(R_WIDTH)),
|
|
8324
|
-
import_chalk17.default.white('ai-spec create "feature"'),
|
|
8325
|
-
import_chalk17.default.gray("ai-spec workspace run"),
|
|
8326
|
-
import_chalk17.default.gray('ai-spec update "change"'),
|
|
8327
|
-
"",
|
|
8328
|
-
import_chalk17.default.hex("#FF8C00").bold("Recent activity"),
|
|
8329
|
-
import_chalk17.default.gray("\u2500".repeat(R_WIDTH)),
|
|
8330
|
-
...recentSpecs.length > 0 ? recentSpecs : [import_chalk17.default.dim("No recent activity")]
|
|
8331
|
-
];
|
|
8332
|
-
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
8333
|
-
for (let i = 0; i < maxLines; i++) {
|
|
8334
|
-
console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
|
|
8335
|
-
}
|
|
8336
|
-
console.log(import_chalk17.default.gray("\u2500".repeat(TOTAL_W)));
|
|
8337
|
-
console.log();
|
|
8338
|
-
}
|
|
8339
|
-
|
|
8340
8342
|
// prompts/global-constitution.prompt.ts
|
|
8341
8343
|
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.
|
|
8342
8344
|
|
|
@@ -8846,8 +8848,8 @@ Existing API/service files:`);
|
|
|
8846
8848
|
}
|
|
8847
8849
|
|
|
8848
8850
|
// core/mock-server-generator.ts
|
|
8849
|
-
var
|
|
8850
|
-
var
|
|
8851
|
+
var path19 = __toESM(require("path"));
|
|
8852
|
+
var fs20 = __toESM(require("fs-extra"));
|
|
8851
8853
|
var import_child_process5 = require("child_process");
|
|
8852
8854
|
function typeToFixture(fieldName, typeDesc) {
|
|
8853
8855
|
const t = typeDesc.toLowerCase();
|
|
@@ -8983,22 +8985,22 @@ function generateMockServerJs(dsl, port) {
|
|
|
8983
8985
|
}
|
|
8984
8986
|
function detectFrontendFramework(projectDir) {
|
|
8985
8987
|
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
8986
|
-
if (
|
|
8988
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "vite";
|
|
8987
8989
|
}
|
|
8988
8990
|
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
8989
|
-
if (
|
|
8991
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "next";
|
|
8990
8992
|
}
|
|
8991
|
-
const pkgPath =
|
|
8992
|
-
if (
|
|
8993
|
+
const pkgPath = path19.join(projectDir, "package.json");
|
|
8994
|
+
if (fs20.existsSync(pkgPath)) {
|
|
8993
8995
|
try {
|
|
8994
|
-
const pkg = JSON.parse(
|
|
8996
|
+
const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
|
|
8995
8997
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
8996
8998
|
if (deps["react-scripts"]) return "cra";
|
|
8997
8999
|
} catch {
|
|
8998
9000
|
}
|
|
8999
9001
|
}
|
|
9000
9002
|
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
9001
|
-
if (
|
|
9003
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "webpack";
|
|
9002
9004
|
}
|
|
9003
9005
|
return "unknown";
|
|
9004
9006
|
}
|
|
@@ -9178,7 +9180,7 @@ export const worker = setupWorker(...handlers);
|
|
|
9178
9180
|
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
9179
9181
|
function findViteConfigFile(projectDir) {
|
|
9180
9182
|
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
9181
|
-
if (
|
|
9183
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return f;
|
|
9182
9184
|
}
|
|
9183
9185
|
return null;
|
|
9184
9186
|
}
|
|
@@ -9224,73 +9226,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
|
9224
9226
|
if (framework === "vite") {
|
|
9225
9227
|
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
9226
9228
|
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
9227
|
-
const mockConfigPath =
|
|
9228
|
-
await
|
|
9229
|
+
const mockConfigPath = path19.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
9230
|
+
await fs20.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
9229
9231
|
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
9230
|
-
const pkgPath =
|
|
9231
|
-
if (await
|
|
9232
|
-
const pkg = await
|
|
9232
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9233
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9234
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9233
9235
|
pkg.scripts = pkg.scripts ?? {};
|
|
9234
9236
|
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
9235
9237
|
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
9236
|
-
await
|
|
9238
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9237
9239
|
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
9238
9240
|
}
|
|
9239
9241
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9240
|
-
await
|
|
9242
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9241
9243
|
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
9242
9244
|
}
|
|
9243
9245
|
if (framework === "cra") {
|
|
9244
|
-
const pkgPath =
|
|
9245
|
-
if (await
|
|
9246
|
-
const pkg = await
|
|
9246
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9247
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9248
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9247
9249
|
const originalProxy = pkg.proxy ?? null;
|
|
9248
9250
|
pkg.proxy = `http://localhost:${mockPort}`;
|
|
9249
|
-
await
|
|
9251
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9250
9252
|
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
9251
9253
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9252
|
-
await
|
|
9254
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9253
9255
|
return { framework, applied: true, devCommand: "npm start" };
|
|
9254
9256
|
}
|
|
9255
9257
|
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
9256
9258
|
}
|
|
9257
9259
|
const lock = { framework, mockPort, frontendDir, actions };
|
|
9258
|
-
await
|
|
9260
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
9259
9261
|
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}`;
|
|
9260
9262
|
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
9261
9263
|
}
|
|
9262
9264
|
async function restoreMockProxy(frontendDir) {
|
|
9263
|
-
const lockPath =
|
|
9264
|
-
if (!await
|
|
9265
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9266
|
+
if (!await fs20.pathExists(lockPath)) {
|
|
9265
9267
|
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
9266
9268
|
}
|
|
9267
|
-
const lock = await
|
|
9269
|
+
const lock = await fs20.readJson(lockPath);
|
|
9268
9270
|
for (const action of lock.actions) {
|
|
9269
9271
|
if (action.type === "wrote-file") {
|
|
9270
|
-
const fp =
|
|
9271
|
-
if (await
|
|
9272
|
+
const fp = path19.join(frontendDir, action.filePath);
|
|
9273
|
+
if (await fs20.pathExists(fp)) await fs20.remove(fp);
|
|
9272
9274
|
} else if (action.type === "added-pkg-script") {
|
|
9273
|
-
const pkgPath =
|
|
9274
|
-
if (await
|
|
9275
|
-
const pkg = await
|
|
9275
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9276
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9277
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9276
9278
|
if (action.originalValue == null) {
|
|
9277
9279
|
delete pkg.scripts?.[action.key];
|
|
9278
9280
|
} else {
|
|
9279
9281
|
pkg.scripts = pkg.scripts ?? {};
|
|
9280
9282
|
pkg.scripts[action.key] = action.originalValue;
|
|
9281
9283
|
}
|
|
9282
|
-
await
|
|
9284
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9283
9285
|
}
|
|
9284
9286
|
} else if (action.type === "patched-pkg-proxy") {
|
|
9285
|
-
const pkgPath =
|
|
9286
|
-
if (await
|
|
9287
|
-
const pkg = await
|
|
9287
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9288
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9289
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9288
9290
|
if (action.originalProxy == null) {
|
|
9289
9291
|
delete pkg.proxy;
|
|
9290
9292
|
} else {
|
|
9291
9293
|
pkg.proxy = action.originalProxy;
|
|
9292
9294
|
}
|
|
9293
|
-
await
|
|
9295
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9294
9296
|
}
|
|
9295
9297
|
}
|
|
9296
9298
|
}
|
|
@@ -9300,7 +9302,7 @@ async function restoreMockProxy(frontendDir) {
|
|
|
9300
9302
|
} catch {
|
|
9301
9303
|
}
|
|
9302
9304
|
}
|
|
9303
|
-
await
|
|
9305
|
+
await fs20.remove(lockPath);
|
|
9304
9306
|
return { restored: true };
|
|
9305
9307
|
}
|
|
9306
9308
|
function startMockServerBackground(serverJsPath, port) {
|
|
@@ -9313,23 +9315,23 @@ function startMockServerBackground(serverJsPath, port) {
|
|
|
9313
9315
|
return child.pid;
|
|
9314
9316
|
}
|
|
9315
9317
|
async function saveMockServerPid(frontendDir, pid) {
|
|
9316
|
-
const lockPath =
|
|
9317
|
-
if (await
|
|
9318
|
-
const lock = await
|
|
9318
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9319
|
+
if (await fs20.pathExists(lockPath)) {
|
|
9320
|
+
const lock = await fs20.readJson(lockPath);
|
|
9319
9321
|
lock.mockServerPid = pid;
|
|
9320
|
-
await
|
|
9322
|
+
await fs20.writeJson(lockPath, lock, { spaces: 2 });
|
|
9321
9323
|
}
|
|
9322
9324
|
}
|
|
9323
9325
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
9324
9326
|
const port = opts.port ?? 3001;
|
|
9325
|
-
const outputDir =
|
|
9327
|
+
const outputDir = path19.join(projectDir, opts.outputDir ?? "mock");
|
|
9326
9328
|
const result = { files: [] };
|
|
9327
|
-
await
|
|
9329
|
+
await fs20.ensureDir(outputDir);
|
|
9328
9330
|
const serverJs = generateMockServerJs(dsl, port);
|
|
9329
|
-
const serverPath =
|
|
9330
|
-
await
|
|
9331
|
+
const serverPath = path19.join(outputDir, "server.js");
|
|
9332
|
+
await fs20.writeFile(serverPath, serverJs, "utf-8");
|
|
9331
9333
|
result.files.push({
|
|
9332
|
-
path:
|
|
9334
|
+
path: path19.relative(projectDir, serverPath),
|
|
9333
9335
|
description: `Express mock server \u2014 run with: node mock/server.js`
|
|
9334
9336
|
});
|
|
9335
9337
|
const mockReadme = `# Mock Server
|
|
@@ -9359,52 +9361,52 @@ ${dsl.endpoints.map(
|
|
|
9359
9361
|
|
|
9360
9362
|
Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
|
|
9361
9363
|
`;
|
|
9362
|
-
const readmePath =
|
|
9363
|
-
await
|
|
9364
|
+
const readmePath = path19.join(outputDir, "README.md");
|
|
9365
|
+
await fs20.writeFile(readmePath, mockReadme, "utf-8");
|
|
9364
9366
|
result.files.push({
|
|
9365
|
-
path:
|
|
9367
|
+
path: path19.relative(projectDir, readmePath),
|
|
9366
9368
|
description: "Mock server usage guide"
|
|
9367
9369
|
});
|
|
9368
9370
|
if (opts.proxy) {
|
|
9369
9371
|
const { content, filename } = generateProxyConfig(dsl, port, projectDir);
|
|
9370
|
-
const proxyPath =
|
|
9371
|
-
await
|
|
9372
|
-
await
|
|
9372
|
+
const proxyPath = path19.join(projectDir, filename);
|
|
9373
|
+
await fs20.ensureDir(path19.dirname(proxyPath));
|
|
9374
|
+
await fs20.writeFile(proxyPath, content, "utf-8");
|
|
9373
9375
|
result.files.push({
|
|
9374
9376
|
path: filename,
|
|
9375
9377
|
description: "Proxy config snippet \u2014 copy instructions into your framework config"
|
|
9376
9378
|
});
|
|
9377
9379
|
}
|
|
9378
9380
|
if (opts.msw) {
|
|
9379
|
-
const mswDir =
|
|
9380
|
-
await
|
|
9381
|
+
const mswDir = path19.join(projectDir, "src", "mocks");
|
|
9382
|
+
await fs20.ensureDir(mswDir);
|
|
9381
9383
|
const handlersContent = generateMswHandlers(dsl);
|
|
9382
|
-
const handlersPath =
|
|
9383
|
-
await
|
|
9384
|
+
const handlersPath = path19.join(mswDir, "handlers.ts");
|
|
9385
|
+
await fs20.writeFile(handlersPath, handlersContent, "utf-8");
|
|
9384
9386
|
result.files.push({
|
|
9385
|
-
path:
|
|
9387
|
+
path: path19.relative(projectDir, handlersPath),
|
|
9386
9388
|
description: "MSW request handlers"
|
|
9387
9389
|
});
|
|
9388
9390
|
const browserContent = generateMswBrowser();
|
|
9389
|
-
const browserPath =
|
|
9390
|
-
await
|
|
9391
|
+
const browserPath = path19.join(mswDir, "browser.ts");
|
|
9392
|
+
await fs20.writeFile(browserPath, browserContent, "utf-8");
|
|
9391
9393
|
result.files.push({
|
|
9392
|
-
path:
|
|
9394
|
+
path: path19.relative(projectDir, browserPath),
|
|
9393
9395
|
description: "MSW browser worker setup"
|
|
9394
9396
|
});
|
|
9395
9397
|
}
|
|
9396
9398
|
return result;
|
|
9397
9399
|
}
|
|
9398
9400
|
async function findLatestDslFile(projectDir) {
|
|
9399
|
-
const specDir =
|
|
9400
|
-
if (!await
|
|
9401
|
+
const specDir = path19.join(projectDir, ".ai-spec");
|
|
9402
|
+
if (!await fs20.pathExists(specDir)) return null;
|
|
9401
9403
|
const allFiles = [];
|
|
9402
9404
|
async function scan(dir) {
|
|
9403
|
-
const entries = await
|
|
9405
|
+
const entries = await fs20.readdir(dir);
|
|
9404
9406
|
for (const entry of entries) {
|
|
9405
|
-
const abs =
|
|
9406
|
-
const
|
|
9407
|
-
if (
|
|
9407
|
+
const abs = path19.join(dir, entry);
|
|
9408
|
+
const stat3 = await fs20.stat(abs);
|
|
9409
|
+
if (stat3.isDirectory()) {
|
|
9408
9410
|
await scan(abs);
|
|
9409
9411
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9410
9412
|
allFiles.push(abs);
|
|
@@ -9414,16 +9416,16 @@ async function findLatestDslFile(projectDir) {
|
|
|
9414
9416
|
await scan(specDir);
|
|
9415
9417
|
if (allFiles.length === 0) return null;
|
|
9416
9418
|
const withMtimes = await Promise.all(
|
|
9417
|
-
allFiles.map(async (f) => ({ f, mtime: (await
|
|
9419
|
+
allFiles.map(async (f) => ({ f, mtime: (await fs20.stat(f)).mtime }))
|
|
9418
9420
|
);
|
|
9419
9421
|
withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
9420
9422
|
return withMtimes[0].f;
|
|
9421
9423
|
}
|
|
9422
9424
|
|
|
9423
9425
|
// core/spec-updater.ts
|
|
9424
|
-
var
|
|
9425
|
-
var
|
|
9426
|
-
var
|
|
9426
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
9427
|
+
var path20 = __toESM(require("path"));
|
|
9428
|
+
var fs21 = __toESM(require("fs-extra"));
|
|
9427
9429
|
|
|
9428
9430
|
// prompts/update.prompt.ts
|
|
9429
9431
|
var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
|
|
@@ -9569,8 +9571,8 @@ var SpecUpdater = class {
|
|
|
9569
9571
|
* Returns all .md spec files sorted newest-first.
|
|
9570
9572
|
*/
|
|
9571
9573
|
static async findLatestSpec(specsDir) {
|
|
9572
|
-
if (!await
|
|
9573
|
-
const files = await
|
|
9574
|
+
if (!await fs21.pathExists(specsDir)) return null;
|
|
9575
|
+
const files = await fs21.readdir(specsDir);
|
|
9574
9576
|
const pattern = /^feature-(.+)-v(\d+)\.md$/;
|
|
9575
9577
|
let latest = null;
|
|
9576
9578
|
for (const file of files) {
|
|
@@ -9578,8 +9580,8 @@ var SpecUpdater = class {
|
|
|
9578
9580
|
if (!m) continue;
|
|
9579
9581
|
const version = parseInt(m[2], 10);
|
|
9580
9582
|
if (!latest || version > latest.version) {
|
|
9581
|
-
const filePath =
|
|
9582
|
-
const content = await
|
|
9583
|
+
const filePath = path20.join(specsDir, file);
|
|
9584
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
9583
9585
|
latest = { filePath, version, slug: m[1], content };
|
|
9584
9586
|
}
|
|
9585
9587
|
}
|
|
@@ -9590,16 +9592,16 @@ var SpecUpdater = class {
|
|
|
9590
9592
|
* Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
|
|
9591
9593
|
*/
|
|
9592
9594
|
async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
|
|
9593
|
-
const existingSpec = await
|
|
9595
|
+
const existingSpec = await fs21.readFile(existingSpecPath, "utf-8");
|
|
9594
9596
|
let existingDsl = null;
|
|
9595
9597
|
const dslFile = await findLatestDslFile(projectDir);
|
|
9596
9598
|
if (dslFile) {
|
|
9597
9599
|
try {
|
|
9598
|
-
existingDsl = await
|
|
9600
|
+
existingDsl = await fs21.readJson(dslFile);
|
|
9599
9601
|
} catch {
|
|
9600
9602
|
}
|
|
9601
9603
|
}
|
|
9602
|
-
console.log(
|
|
9604
|
+
console.log(import_chalk17.default.blue(" [1/3] Generating updated spec..."));
|
|
9603
9605
|
const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
|
|
9604
9606
|
let updatedSpecContent;
|
|
9605
9607
|
try {
|
|
@@ -9608,15 +9610,15 @@ var SpecUpdater = class {
|
|
|
9608
9610
|
} catch (err) {
|
|
9609
9611
|
throw new Error(`Spec update generation failed: ${err.message}`);
|
|
9610
9612
|
}
|
|
9611
|
-
const specBasename =
|
|
9613
|
+
const specBasename = path20.basename(existingSpecPath);
|
|
9612
9614
|
const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
|
|
9613
9615
|
const slug = slugMatch ? slugMatch[1] : "feature";
|
|
9614
|
-
const specsDir =
|
|
9616
|
+
const specsDir = path20.dirname(existingSpecPath);
|
|
9615
9617
|
const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
|
|
9616
|
-
await
|
|
9617
|
-
await
|
|
9618
|
-
console.log(
|
|
9619
|
-
console.log(
|
|
9618
|
+
await fs21.ensureDir(specsDir);
|
|
9619
|
+
await fs21.writeFile(newSpecPath, updatedSpecContent, "utf-8");
|
|
9620
|
+
console.log(import_chalk17.default.green(` \u2714 New spec written: ${path20.relative(projectDir, newSpecPath)}`));
|
|
9621
|
+
console.log(import_chalk17.default.blue(" [2/3] Updating DSL..."));
|
|
9620
9622
|
let updatedDsl = null;
|
|
9621
9623
|
let newDslPath = null;
|
|
9622
9624
|
if (existingDsl) {
|
|
@@ -9628,7 +9630,7 @@ var SpecUpdater = class {
|
|
|
9628
9630
|
updatedDsl = parsed;
|
|
9629
9631
|
}
|
|
9630
9632
|
} catch {
|
|
9631
|
-
console.log(
|
|
9633
|
+
console.log(import_chalk17.default.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
|
|
9632
9634
|
}
|
|
9633
9635
|
}
|
|
9634
9636
|
if (!updatedDsl) {
|
|
@@ -9637,15 +9639,15 @@ var SpecUpdater = class {
|
|
|
9637
9639
|
}
|
|
9638
9640
|
if (updatedDsl) {
|
|
9639
9641
|
const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
|
|
9640
|
-
await
|
|
9642
|
+
await fs21.writeJson(dslPath, updatedDsl, { spaces: 2 });
|
|
9641
9643
|
newDslPath = dslPath;
|
|
9642
|
-
console.log(
|
|
9644
|
+
console.log(import_chalk17.default.green(` \u2714 DSL updated: ${path20.relative(projectDir, dslPath)}`));
|
|
9643
9645
|
} else {
|
|
9644
|
-
console.log(
|
|
9646
|
+
console.log(import_chalk17.default.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
|
|
9645
9647
|
}
|
|
9646
9648
|
let affectedFiles = [];
|
|
9647
9649
|
if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
|
|
9648
|
-
console.log(
|
|
9650
|
+
console.log(import_chalk17.default.blue(" [3/3] Identifying affected files..."));
|
|
9649
9651
|
const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
|
|
9650
9652
|
const affectedPrompt = buildAffectedFilesPrompt(
|
|
9651
9653
|
changeRequest,
|
|
@@ -9656,9 +9658,9 @@ var SpecUpdater = class {
|
|
|
9656
9658
|
try {
|
|
9657
9659
|
const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
|
|
9658
9660
|
affectedFiles = parseAffectedFiles(affectedRaw);
|
|
9659
|
-
console.log(
|
|
9661
|
+
console.log(import_chalk17.default.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
|
|
9660
9662
|
} catch {
|
|
9661
|
-
console.log(
|
|
9663
|
+
console.log(import_chalk17.default.gray(" Could not identify affected files \u2014 use manual selection."));
|
|
9662
9664
|
}
|
|
9663
9665
|
}
|
|
9664
9666
|
return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
|
|
@@ -9666,8 +9668,8 @@ var SpecUpdater = class {
|
|
|
9666
9668
|
};
|
|
9667
9669
|
|
|
9668
9670
|
// core/openapi-exporter.ts
|
|
9669
|
-
var
|
|
9670
|
-
var
|
|
9671
|
+
var path21 = __toESM(require("path"));
|
|
9672
|
+
var fs22 = __toESM(require("fs-extra"));
|
|
9671
9673
|
function dslTypeToOASchema(typeDesc, fieldName = "") {
|
|
9672
9674
|
const t = typeDesc.toLowerCase();
|
|
9673
9675
|
if (t === "string" || t.includes("string")) {
|
|
@@ -9896,7 +9898,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9896
9898
|
const format = opts.format ?? "yaml";
|
|
9897
9899
|
const serverUrl = opts.serverUrl ?? "http://localhost:3000";
|
|
9898
9900
|
const defaultName = `openapi.${format}`;
|
|
9899
|
-
const outputPath = opts.outputPath ?
|
|
9901
|
+
const outputPath = opts.outputPath ? path21.isAbsolute(opts.outputPath) ? opts.outputPath : path21.join(projectDir, opts.outputPath) : path21.join(projectDir, defaultName);
|
|
9900
9902
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
9901
9903
|
let content;
|
|
9902
9904
|
if (format === "json") {
|
|
@@ -9904,8 +9906,8 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9904
9906
|
} else {
|
|
9905
9907
|
content = buildYamlDoc(doc);
|
|
9906
9908
|
}
|
|
9907
|
-
await
|
|
9908
|
-
await
|
|
9909
|
+
await fs22.ensureDir(path21.dirname(outputPath));
|
|
9910
|
+
await fs22.writeFile(outputPath, content, "utf-8");
|
|
9909
9911
|
return outputPath;
|
|
9910
9912
|
}
|
|
9911
9913
|
|
|
@@ -9913,9 +9915,9 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9913
9915
|
dotenv.config();
|
|
9914
9916
|
var CONFIG_FILE = ".ai-spec.json";
|
|
9915
9917
|
async function loadConfig(dir) {
|
|
9916
|
-
const p =
|
|
9917
|
-
if (await
|
|
9918
|
-
return
|
|
9918
|
+
const p = path22.join(dir, CONFIG_FILE);
|
|
9919
|
+
if (await fs23.pathExists(p)) {
|
|
9920
|
+
return fs23.readJson(p);
|
|
9919
9921
|
}
|
|
9920
9922
|
return {};
|
|
9921
9923
|
}
|
|
@@ -9940,20 +9942,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
9940
9942
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
9941
9943
|
});
|
|
9942
9944
|
await saveKey(providerName, newKey.trim());
|
|
9943
|
-
console.log(
|
|
9945
|
+
console.log(import_chalk18.default.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
9944
9946
|
return newKey.trim();
|
|
9945
9947
|
}
|
|
9946
9948
|
function printBanner(opts) {
|
|
9947
|
-
console.log(
|
|
9948
|
-
console.log(
|
|
9949
|
-
console.log(
|
|
9950
|
-
console.log(
|
|
9949
|
+
console.log(import_chalk18.default.blue("\n" + "\u2500".repeat(52)));
|
|
9950
|
+
console.log(import_chalk18.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
9951
|
+
console.log(import_chalk18.default.blue("\u2500".repeat(52)));
|
|
9952
|
+
console.log(import_chalk18.default.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
9951
9953
|
console.log(
|
|
9952
|
-
|
|
9954
|
+
import_chalk18.default.gray(
|
|
9953
9955
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
9954
9956
|
)
|
|
9955
9957
|
);
|
|
9956
|
-
console.log(
|
|
9958
|
+
console.log(import_chalk18.default.blue("\u2500".repeat(52) + "\n"));
|
|
9957
9959
|
}
|
|
9958
9960
|
var program = new import_commander.Command();
|
|
9959
9961
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -9980,42 +9982,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9980
9982
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
9981
9983
|
const workspaceConfig = await workspaceLoader.load();
|
|
9982
9984
|
if (workspaceConfig) {
|
|
9983
|
-
console.log(
|
|
9985
|
+
console.log(import_chalk18.default.cyan(`
|
|
9984
9986
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
9985
|
-
console.log(
|
|
9987
|
+
console.log(import_chalk18.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
9986
9988
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9987
9989
|
if (opts.serve) {
|
|
9988
|
-
console.log(
|
|
9990
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9989
9991
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
9990
9992
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
9991
9993
|
if (!backendResult) {
|
|
9992
|
-
console.log(
|
|
9994
|
+
console.log(import_chalk18.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
9993
9995
|
} else {
|
|
9994
9996
|
const mockPort = 3001;
|
|
9995
9997
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
9996
|
-
const serverJsPath =
|
|
9997
|
-
console.log(
|
|
9998
|
+
const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9999
|
+
console.log(import_chalk18.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
9998
10000
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
9999
|
-
console.log(
|
|
10001
|
+
console.log(import_chalk18.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
10000
10002
|
if (frontendResult) {
|
|
10001
10003
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
10002
10004
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
10003
10005
|
if (proxyResult.applied) {
|
|
10004
|
-
console.log(
|
|
10005
|
-
console.log(
|
|
10006
|
+
console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
10007
|
+
console.log(import_chalk18.default.bold.cyan(`
|
|
10006
10008
|
Ready! Run your frontend dev server:`));
|
|
10007
|
-
console.log(
|
|
10008
|
-
console.log(
|
|
10009
|
-
console.log(
|
|
10009
|
+
console.log(import_chalk18.default.white(` cd ${frontendResult.repoAbsPath}`));
|
|
10010
|
+
console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
|
|
10011
|
+
console.log(import_chalk18.default.gray(`
|
|
10010
10012
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
10011
10013
|
} else {
|
|
10012
|
-
console.log(
|
|
10013
|
-
if (proxyResult.note) console.log(
|
|
10014
|
-
console.log(
|
|
10014
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
10015
|
+
if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
|
|
10016
|
+
console.log(import_chalk18.default.gray(` Mock server: http://localhost:${mockPort}`));
|
|
10015
10017
|
}
|
|
10016
10018
|
} else {
|
|
10017
|
-
console.log(
|
|
10018
|
-
console.log(
|
|
10019
|
+
console.log(import_chalk18.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
10020
|
+
console.log(import_chalk18.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
10019
10021
|
}
|
|
10020
10022
|
}
|
|
10021
10023
|
}
|
|
@@ -10036,7 +10038,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10036
10038
|
codegenModel: codegenModelName
|
|
10037
10039
|
});
|
|
10038
10040
|
const runId = generateRunId();
|
|
10039
|
-
console.log(
|
|
10041
|
+
console.log(import_chalk18.default.gray(` Run ID: ${runId}`));
|
|
10040
10042
|
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10041
10043
|
setActiveSnapshot(runSnapshot);
|
|
10042
10044
|
const runLogger = new RunLogger(currentDir, runId, {
|
|
@@ -10044,25 +10046,25 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10044
10046
|
model: specModelName
|
|
10045
10047
|
});
|
|
10046
10048
|
setActiveLogger(runLogger);
|
|
10047
|
-
console.log(
|
|
10049
|
+
console.log(import_chalk18.default.blue("[1/6] Loading project context..."));
|
|
10048
10050
|
runLogger.stageStart("context_load");
|
|
10049
10051
|
const loader = new ContextLoader(currentDir);
|
|
10050
10052
|
const context = await loader.loadProjectContext();
|
|
10051
10053
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10052
10054
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10053
|
-
console.log(
|
|
10054
|
-
console.log(
|
|
10055
|
-
console.log(
|
|
10055
|
+
console.log(import_chalk18.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10056
|
+
console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10057
|
+
console.log(import_chalk18.default.gray(` API files : ${context.apiStructure.length} files`));
|
|
10056
10058
|
if (context.schema) {
|
|
10057
|
-
console.log(
|
|
10059
|
+
console.log(import_chalk18.default.gray(` Prisma schema: found`));
|
|
10058
10060
|
}
|
|
10059
10061
|
if (context.constitution) {
|
|
10060
|
-
console.log(
|
|
10062
|
+
console.log(import_chalk18.default.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
10061
10063
|
if (context.constitution.length > 6e3) {
|
|
10062
|
-
console.log(
|
|
10064
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10063
10065
|
}
|
|
10064
10066
|
} else {
|
|
10065
|
-
console.log(
|
|
10067
|
+
console.log(import_chalk18.default.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
10066
10068
|
try {
|
|
10067
10069
|
const constitutionGen = new ConstitutionGenerator(
|
|
10068
10070
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -10070,12 +10072,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10070
10072
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
10071
10073
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
10072
10074
|
context.constitution = constitutionContent;
|
|
10073
|
-
console.log(
|
|
10075
|
+
console.log(import_chalk18.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
10074
10076
|
} catch (err) {
|
|
10075
|
-
console.log(
|
|
10077
|
+
console.log(import_chalk18.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
10076
10078
|
}
|
|
10077
10079
|
}
|
|
10078
|
-
console.log(
|
|
10080
|
+
console.log(import_chalk18.default.blue(`
|
|
10079
10081
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
10080
10082
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10081
10083
|
let initialSpec;
|
|
@@ -10085,30 +10087,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10085
10087
|
if (opts.skipTasks) {
|
|
10086
10088
|
const generator = new SpecGenerator(specProvider);
|
|
10087
10089
|
initialSpec = await generator.generateSpec(idea, context);
|
|
10088
|
-
console.log(
|
|
10090
|
+
console.log(import_chalk18.default.green(" \u2714 Spec generated."));
|
|
10089
10091
|
} else {
|
|
10090
10092
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
10091
10093
|
initialSpec = result.spec;
|
|
10092
10094
|
initialTasks = result.tasks;
|
|
10093
|
-
console.log(
|
|
10095
|
+
console.log(import_chalk18.default.green(` \u2714 Spec generated.`));
|
|
10094
10096
|
if (initialTasks.length > 0) {
|
|
10095
|
-
console.log(
|
|
10097
|
+
console.log(import_chalk18.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
10096
10098
|
} else {
|
|
10097
|
-
console.log(
|
|
10099
|
+
console.log(import_chalk18.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10098
10100
|
}
|
|
10099
10101
|
}
|
|
10100
10102
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10101
10103
|
} catch (err) {
|
|
10102
10104
|
runLogger.stageFail("spec_gen", err.message);
|
|
10103
|
-
console.error(
|
|
10105
|
+
console.error(import_chalk18.default.red(" \u2718 Spec generation failed:"), err);
|
|
10104
10106
|
process.exit(1);
|
|
10105
10107
|
}
|
|
10106
10108
|
let finalSpec;
|
|
10107
10109
|
if (opts.fast) {
|
|
10108
|
-
console.log(
|
|
10110
|
+
console.log(import_chalk18.default.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
10109
10111
|
finalSpec = initialSpec;
|
|
10110
10112
|
} else {
|
|
10111
|
-
console.log(
|
|
10113
|
+
console.log(import_chalk18.default.blue("\n[3/6] Interactive spec refinement..."));
|
|
10112
10114
|
runLogger.stageStart("spec_refine");
|
|
10113
10115
|
const refiner = new SpecRefiner(specProvider);
|
|
10114
10116
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
@@ -10119,7 +10121,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10119
10121
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
10120
10122
|
if (shouldRunAssessment) {
|
|
10121
10123
|
if (!opts.auto) {
|
|
10122
|
-
console.log(
|
|
10124
|
+
console.log(import_chalk18.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10123
10125
|
}
|
|
10124
10126
|
runLogger.stageStart("spec_assess");
|
|
10125
10127
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
@@ -10128,45 +10130,45 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10128
10130
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10129
10131
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10130
10132
|
if (opts.force) {
|
|
10131
|
-
console.log(
|
|
10133
|
+
console.log(import_chalk18.default.yellow(`
|
|
10132
10134
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10133
10135
|
} else {
|
|
10134
10136
|
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10135
|
-
console.log(
|
|
10137
|
+
console.log(import_chalk18.default.red(`
|
|
10136
10138
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10137
10139
|
if (!opts.auto) {
|
|
10138
|
-
console.log(
|
|
10140
|
+
console.log(import_chalk18.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
10139
10141
|
} else {
|
|
10140
|
-
console.log(
|
|
10142
|
+
console.log(import_chalk18.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
10141
10143
|
}
|
|
10142
|
-
console.log(
|
|
10144
|
+
console.log(import_chalk18.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
10143
10145
|
process.exit(1);
|
|
10144
10146
|
}
|
|
10145
10147
|
}
|
|
10146
10148
|
} else {
|
|
10147
10149
|
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10148
10150
|
if (!opts.auto) {
|
|
10149
|
-
console.log(
|
|
10151
|
+
console.log(import_chalk18.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10150
10152
|
}
|
|
10151
10153
|
}
|
|
10152
10154
|
}
|
|
10153
10155
|
if (!opts.auto) {
|
|
10154
|
-
console.log(
|
|
10156
|
+
console.log(import_chalk18.default.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
10155
10157
|
const specLines = finalSpec.split("\n").length;
|
|
10156
10158
|
const specWords = finalSpec.split(/\s+/).length;
|
|
10157
10159
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10158
|
-
console.log(
|
|
10159
|
-
if (taskCountHint) console.log(
|
|
10160
|
-
const previewSpecsDir =
|
|
10160
|
+
console.log(import_chalk18.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10161
|
+
if (taskCountHint) console.log(import_chalk18.default.gray(taskCountHint));
|
|
10162
|
+
const previewSpecsDir = path22.join(currentDir, "specs");
|
|
10161
10163
|
const slug = featureSlug;
|
|
10162
10164
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10163
10165
|
if (prevVersion) {
|
|
10164
|
-
console.log(
|
|
10166
|
+
console.log(import_chalk18.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
10165
10167
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
10166
|
-
console.log(
|
|
10168
|
+
console.log(import_chalk18.default.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10167
10169
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
10168
10170
|
printDiff(diff);
|
|
10169
|
-
console.log(
|
|
10171
|
+
console.log(import_chalk18.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10170
10172
|
}
|
|
10171
10173
|
const gate = await (0, import_prompts3.select)({
|
|
10172
10174
|
message: "Ready to proceed to code generation?",
|
|
@@ -10177,9 +10179,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10177
10179
|
]
|
|
10178
10180
|
});
|
|
10179
10181
|
if (gate === "view") {
|
|
10180
|
-
console.log(
|
|
10182
|
+
console.log(import_chalk18.default.cyan("\n" + "\u2500".repeat(52)));
|
|
10181
10183
|
console.log(finalSpec);
|
|
10182
|
-
console.log(
|
|
10184
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52) + "\n"));
|
|
10183
10185
|
const confirm22 = await (0, import_prompts3.select)({
|
|
10184
10186
|
message: "Proceed to code generation?",
|
|
10185
10187
|
choices: [
|
|
@@ -10188,92 +10190,92 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10188
10190
|
]
|
|
10189
10191
|
});
|
|
10190
10192
|
if (confirm22 === "abort") {
|
|
10191
|
-
console.log(
|
|
10193
|
+
console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10192
10194
|
process.exit(0);
|
|
10193
10195
|
}
|
|
10194
10196
|
} else if (gate === "abort") {
|
|
10195
|
-
console.log(
|
|
10197
|
+
console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10196
10198
|
process.exit(0);
|
|
10197
10199
|
}
|
|
10198
|
-
console.log(
|
|
10200
|
+
console.log(import_chalk18.default.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
10199
10201
|
} else {
|
|
10200
|
-
console.log(
|
|
10202
|
+
console.log(import_chalk18.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
10201
10203
|
}
|
|
10202
10204
|
let extractedDsl = null;
|
|
10203
10205
|
if (opts.skipDsl) {
|
|
10204
|
-
console.log(
|
|
10206
|
+
console.log(import_chalk18.default.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
10205
10207
|
} else {
|
|
10206
|
-
console.log(
|
|
10207
|
-
console.log(
|
|
10208
|
+
console.log(import_chalk18.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10209
|
+
console.log(import_chalk18.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10208
10210
|
runLogger.stageStart("dsl_extract");
|
|
10209
10211
|
try {
|
|
10210
10212
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10211
|
-
if (isFrontend) console.log(
|
|
10213
|
+
if (isFrontend) console.log(import_chalk18.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10212
10214
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10213
10215
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10214
10216
|
if (extractedDsl) {
|
|
10215
10217
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10216
|
-
console.log(
|
|
10218
|
+
console.log(import_chalk18.default.green(" \u2714 DSL extracted and validated."));
|
|
10217
10219
|
} else {
|
|
10218
10220
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10219
|
-
console.log(
|
|
10221
|
+
console.log(import_chalk18.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10220
10222
|
}
|
|
10221
10223
|
} catch (err) {
|
|
10222
10224
|
runLogger.stageFail("dsl_extract", err.message);
|
|
10223
|
-
console.log(
|
|
10225
|
+
console.log(import_chalk18.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10224
10226
|
}
|
|
10225
10227
|
}
|
|
10226
10228
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
10227
10229
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
10228
10230
|
let workingDir = currentDir;
|
|
10229
10231
|
if (!skipWorktree) {
|
|
10230
|
-
console.log(
|
|
10232
|
+
console.log(import_chalk18.default.blue("\n[4/6] Setting up git worktree..."));
|
|
10231
10233
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
10232
10234
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10233
10235
|
if (worktreePath) workingDir = worktreePath;
|
|
10234
10236
|
} else {
|
|
10235
10237
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10236
|
-
console.log(
|
|
10238
|
+
console.log(import_chalk18.default.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10237
10239
|
}
|
|
10238
|
-
const specsDir =
|
|
10239
|
-
await
|
|
10240
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10241
|
+
await fs23.ensureDir(specsDir);
|
|
10240
10242
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10241
|
-
await
|
|
10242
|
-
console.log(
|
|
10243
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10243
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10244
|
+
console.log(import_chalk18.default.green(`
|
|
10245
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + import_chalk18.default.gray(` (v${specVersion})`));
|
|
10244
10246
|
let savedDslFile = null;
|
|
10245
10247
|
if (extractedDsl) {
|
|
10246
10248
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10247
10249
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
10248
|
-
console.log(
|
|
10250
|
+
console.log(import_chalk18.default.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
10249
10251
|
}
|
|
10250
10252
|
if (!opts.skipTasks) {
|
|
10251
10253
|
const taskGen = new TaskGenerator(specProvider);
|
|
10252
10254
|
let tasksToSave = initialTasks;
|
|
10253
10255
|
if (tasksToSave.length === 0) {
|
|
10254
|
-
console.log(
|
|
10256
|
+
console.log(import_chalk18.default.blue(`
|
|
10255
10257
|
Generating tasks (separate call)...`));
|
|
10256
10258
|
try {
|
|
10257
10259
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
10258
10260
|
} catch (err) {
|
|
10259
|
-
console.log(
|
|
10261
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
10260
10262
|
}
|
|
10261
10263
|
}
|
|
10262
10264
|
if (tasksToSave.length > 0) {
|
|
10263
10265
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
10264
10266
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
10265
10267
|
printTasks(sorted);
|
|
10266
|
-
console.log(
|
|
10268
|
+
console.log(import_chalk18.default.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
10267
10269
|
} else {
|
|
10268
|
-
console.log(
|
|
10270
|
+
console.log(import_chalk18.default.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
10269
10271
|
}
|
|
10270
10272
|
}
|
|
10271
|
-
console.log(
|
|
10273
|
+
console.log(import_chalk18.default.blue(`
|
|
10272
10274
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
10273
10275
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10274
10276
|
let generatedTestFiles = [];
|
|
10275
10277
|
if (opts.tdd && extractedDsl) {
|
|
10276
|
-
console.log(
|
|
10278
|
+
console.log(import_chalk18.default.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
10277
10279
|
const testGen = new TestGenerator(codegenProvider);
|
|
10278
10280
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10279
10281
|
}
|
|
@@ -10287,13 +10289,13 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10287
10289
|
});
|
|
10288
10290
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10289
10291
|
if (opts.tdd) {
|
|
10290
|
-
console.log(
|
|
10292
|
+
console.log(import_chalk18.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10291
10293
|
} else if (opts.skipTests) {
|
|
10292
|
-
console.log(
|
|
10294
|
+
console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
10293
10295
|
} else if (!extractedDsl) {
|
|
10294
|
-
console.log(
|
|
10296
|
+
console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
10295
10297
|
} else {
|
|
10296
|
-
console.log(
|
|
10298
|
+
console.log(import_chalk18.default.blue(`
|
|
10297
10299
|
[7/9] Test skeleton generation...`));
|
|
10298
10300
|
runLogger.stageStart("test_gen");
|
|
10299
10301
|
const testGen = new TestGenerator(codegenProvider);
|
|
@@ -10301,10 +10303,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10301
10303
|
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10302
10304
|
}
|
|
10303
10305
|
if (opts.skipErrorFeedback) {
|
|
10304
|
-
console.log(
|
|
10306
|
+
console.log(import_chalk18.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10305
10307
|
} else {
|
|
10306
10308
|
if (opts.tdd) {
|
|
10307
|
-
console.log(
|
|
10309
|
+
console.log(import_chalk18.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10308
10310
|
}
|
|
10309
10311
|
runLogger.stageStart("error_feedback");
|
|
10310
10312
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
@@ -10315,10 +10317,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10315
10317
|
}
|
|
10316
10318
|
let reviewResult = "";
|
|
10317
10319
|
if (!opts.skipReview) {
|
|
10318
|
-
console.log(
|
|
10320
|
+
console.log(import_chalk18.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10319
10321
|
runLogger.stageStart("review");
|
|
10320
10322
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10321
|
-
const savedSpec = await
|
|
10323
|
+
const savedSpec = await fs23.readFile(specFile, "utf-8");
|
|
10322
10324
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
10323
10325
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
10324
10326
|
} else {
|
|
@@ -10334,19 +10336,19 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10334
10336
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10335
10337
|
}
|
|
10336
10338
|
runLogger.finish();
|
|
10337
|
-
console.log(
|
|
10338
|
-
console.log(
|
|
10339
|
-
if (savedDslFile) console.log(
|
|
10339
|
+
console.log(import_chalk18.default.bold.green("\n\u2714 All done!"));
|
|
10340
|
+
console.log(import_chalk18.default.gray(` Spec : ${specFile}`));
|
|
10341
|
+
if (savedDslFile) console.log(import_chalk18.default.gray(` DSL : ${savedDslFile}`));
|
|
10340
10342
|
if (generatedTestFiles.length > 0) {
|
|
10341
|
-
console.log(
|
|
10343
|
+
console.log(import_chalk18.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
10342
10344
|
}
|
|
10343
|
-
console.log(
|
|
10345
|
+
console.log(import_chalk18.default.gray(` Working dir : ${workingDir}`));
|
|
10344
10346
|
if (workingDir !== currentDir) {
|
|
10345
|
-
console.log(
|
|
10347
|
+
console.log(import_chalk18.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10346
10348
|
}
|
|
10347
10349
|
runLogger.printSummary();
|
|
10348
10350
|
if (runSnapshot.fileCount > 0) {
|
|
10349
|
-
console.log(
|
|
10351
|
+
console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
10350
10352
|
}
|
|
10351
10353
|
});
|
|
10352
10354
|
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(
|
|
@@ -10363,24 +10365,24 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10363
10365
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
10364
10366
|
let specContent = "";
|
|
10365
10367
|
let resolvedSpecFile;
|
|
10366
|
-
if (specFile && await
|
|
10367
|
-
specContent = await
|
|
10368
|
+
if (specFile && await fs23.pathExists(specFile)) {
|
|
10369
|
+
specContent = await fs23.readFile(specFile, "utf-8");
|
|
10368
10370
|
resolvedSpecFile = specFile;
|
|
10369
|
-
console.log(
|
|
10371
|
+
console.log(import_chalk18.default.gray(`Using spec: ${specFile}`));
|
|
10370
10372
|
} else {
|
|
10371
|
-
const specsDir =
|
|
10372
|
-
if (await
|
|
10373
|
-
const files = (await
|
|
10373
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
10374
|
+
if (await fs23.pathExists(specsDir)) {
|
|
10375
|
+
const files = (await fs23.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
10374
10376
|
if (files.length > 0) {
|
|
10375
|
-
const latest =
|
|
10376
|
-
specContent = await
|
|
10377
|
+
const latest = path22.join(specsDir, files[0]);
|
|
10378
|
+
specContent = await fs23.readFile(latest, "utf-8");
|
|
10377
10379
|
resolvedSpecFile = latest;
|
|
10378
|
-
console.log(
|
|
10380
|
+
console.log(import_chalk18.default.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10379
10381
|
}
|
|
10380
10382
|
}
|
|
10381
10383
|
}
|
|
10382
10384
|
if (!specContent) {
|
|
10383
|
-
console.log(
|
|
10385
|
+
console.log(import_chalk18.default.yellow("No spec file found. Running review without spec context."));
|
|
10384
10386
|
}
|
|
10385
10387
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10386
10388
|
await reviewer.printScoreTrend();
|
|
@@ -10407,15 +10409,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10407
10409
|
auto: opts.auto
|
|
10408
10410
|
});
|
|
10409
10411
|
if (result.written) {
|
|
10410
|
-
console.log(
|
|
10411
|
-
console.log(
|
|
10412
|
-
console.log(
|
|
10412
|
+
console.log(import_chalk18.default.blue("\n Summary:"));
|
|
10413
|
+
console.log(import_chalk18.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10414
|
+
console.log(import_chalk18.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10413
10415
|
if (result.backupPath) {
|
|
10414
|
-
console.log(
|
|
10416
|
+
console.log(import_chalk18.default.gray(` Backup: ${path22.basename(result.backupPath)}`));
|
|
10415
10417
|
}
|
|
10416
10418
|
}
|
|
10417
10419
|
} catch (err) {
|
|
10418
|
-
console.error(
|
|
10420
|
+
console.error(import_chalk18.default.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10419
10421
|
process.exit(1);
|
|
10420
10422
|
}
|
|
10421
10423
|
return;
|
|
@@ -10423,75 +10425,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10423
10425
|
if (opts.global) {
|
|
10424
10426
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10425
10427
|
if (existing && !opts.force) {
|
|
10426
|
-
console.log(
|
|
10428
|
+
console.log(import_chalk18.default.yellow(`
|
|
10427
10429
|
Global constitution already exists at: ${existing.source}`));
|
|
10428
|
-
console.log(
|
|
10430
|
+
console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
|
|
10429
10431
|
return;
|
|
10430
10432
|
}
|
|
10431
|
-
console.log(
|
|
10432
|
-
console.log(
|
|
10433
|
-
console.log(
|
|
10433
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10434
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
10435
|
+
console.log(import_chalk18.default.gray(" Scanning repos in workspace..."));
|
|
10434
10436
|
const loader = new ContextLoader(currentDir);
|
|
10435
10437
|
const ctx = await loader.loadProjectContext();
|
|
10436
10438
|
const summary = [
|
|
10437
10439
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10438
10440
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10439
10441
|
].join("\n");
|
|
10440
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10442
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
|
|
10441
10443
|
let globalConstitution;
|
|
10442
10444
|
try {
|
|
10443
10445
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10444
10446
|
} catch (err) {
|
|
10445
|
-
console.error(
|
|
10447
|
+
console.error(import_chalk18.default.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10446
10448
|
process.exit(1);
|
|
10447
10449
|
}
|
|
10448
10450
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10449
|
-
console.log(
|
|
10451
|
+
console.log(import_chalk18.default.green(`
|
|
10450
10452
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10451
|
-
console.log(
|
|
10452
|
-
console.log(
|
|
10453
|
-
console.log(
|
|
10454
|
-
console.log(
|
|
10453
|
+
console.log(import_chalk18.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
10454
|
+
console.log(import_chalk18.default.gray(" Project-level rules always override global rules.\n"));
|
|
10455
|
+
console.log(import_chalk18.default.bold(" Preview:"));
|
|
10456
|
+
console.log(import_chalk18.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10455
10457
|
if (globalConstitution.split("\n").length > 12) {
|
|
10456
|
-
console.log(
|
|
10458
|
+
console.log(import_chalk18.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10457
10459
|
}
|
|
10458
10460
|
return;
|
|
10459
10461
|
}
|
|
10460
|
-
const constitutionPath =
|
|
10461
|
-
if (!opts.force && await
|
|
10462
|
-
console.log(
|
|
10462
|
+
const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
|
|
10463
|
+
if (!opts.force && await fs23.pathExists(constitutionPath)) {
|
|
10464
|
+
console.log(import_chalk18.default.yellow(`
|
|
10463
10465
|
${CONSTITUTION_FILE} already exists.`));
|
|
10464
|
-
console.log(
|
|
10465
|
-
console.log(
|
|
10466
|
+
console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
|
|
10467
|
+
console.log(import_chalk18.default.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10466
10468
|
return;
|
|
10467
10469
|
}
|
|
10468
|
-
console.log(
|
|
10469
|
-
console.log(
|
|
10470
|
-
console.log(
|
|
10470
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10471
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
10472
|
+
console.log(import_chalk18.default.gray(" Analyzing codebase..."));
|
|
10471
10473
|
const generator = new ConstitutionGenerator(provider);
|
|
10472
10474
|
let constitution;
|
|
10473
10475
|
try {
|
|
10474
10476
|
constitution = await generator.generate(currentDir);
|
|
10475
10477
|
} catch (err) {
|
|
10476
|
-
console.error(
|
|
10478
|
+
console.error(import_chalk18.default.red(" \u2718 Failed to generate constitution:"), err);
|
|
10477
10479
|
process.exit(1);
|
|
10478
10480
|
}
|
|
10479
10481
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10480
|
-
const globalResult = await loadGlobalConstitution([
|
|
10482
|
+
const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
|
|
10481
10483
|
if (globalResult) {
|
|
10482
|
-
console.log(
|
|
10484
|
+
console.log(import_chalk18.default.cyan(`
|
|
10483
10485
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10484
|
-
console.log(
|
|
10485
|
-
console.log(
|
|
10486
|
+
console.log(import_chalk18.default.gray(" It will be merged with this project constitution at runtime."));
|
|
10487
|
+
console.log(import_chalk18.default.gray(" Project rules take priority over global rules."));
|
|
10486
10488
|
}
|
|
10487
|
-
console.log(
|
|
10489
|
+
console.log(import_chalk18.default.green(`
|
|
10488
10490
|
\u2714 Constitution saved: ${saved}`));
|
|
10489
|
-
console.log(
|
|
10490
|
-
console.log(
|
|
10491
|
-
console.log(
|
|
10492
|
-
console.log(
|
|
10491
|
+
console.log(import_chalk18.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
10492
|
+
console.log(import_chalk18.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
10493
|
+
console.log(import_chalk18.default.bold(" Preview:"));
|
|
10494
|
+
console.log(import_chalk18.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10493
10495
|
if (constitution.split("\n").length > 15) {
|
|
10494
|
-
console.log(
|
|
10496
|
+
console.log(import_chalk18.default.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10495
10497
|
}
|
|
10496
10498
|
});
|
|
10497
10499
|
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(
|
|
@@ -10499,44 +10501,44 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10499
10501
|
"Default code generation mode (claude-code|api|plan)"
|
|
10500
10502
|
).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) => {
|
|
10501
10503
|
const currentDir = process.cwd();
|
|
10502
|
-
const configPath =
|
|
10504
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10503
10505
|
if (opts.clearKeys) {
|
|
10504
10506
|
await clearAllKeys();
|
|
10505
|
-
console.log(
|
|
10507
|
+
console.log(import_chalk18.default.green(`\u2714 All saved API keys cleared.`));
|
|
10506
10508
|
return;
|
|
10507
10509
|
}
|
|
10508
10510
|
if (opts.clearKey) {
|
|
10509
10511
|
await clearKey(opts.clearKey);
|
|
10510
|
-
console.log(
|
|
10512
|
+
console.log(import_chalk18.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10511
10513
|
return;
|
|
10512
10514
|
}
|
|
10513
10515
|
if (opts.listKeys) {
|
|
10514
|
-
const store = await
|
|
10516
|
+
const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10515
10517
|
const providers = Object.keys(store);
|
|
10516
10518
|
if (providers.length === 0) {
|
|
10517
|
-
console.log(
|
|
10519
|
+
console.log(import_chalk18.default.gray("No saved API keys."));
|
|
10518
10520
|
} else {
|
|
10519
|
-
console.log(
|
|
10521
|
+
console.log(import_chalk18.default.bold("Saved API keys:"));
|
|
10520
10522
|
for (const p of providers) {
|
|
10521
10523
|
const k2 = store[p];
|
|
10522
|
-
console.log(
|
|
10524
|
+
console.log(import_chalk18.default.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10523
10525
|
}
|
|
10524
|
-
console.log(
|
|
10526
|
+
console.log(import_chalk18.default.gray(`
|
|
10525
10527
|
File: ${KEY_STORE_FILE}`));
|
|
10526
10528
|
}
|
|
10527
10529
|
return;
|
|
10528
10530
|
}
|
|
10529
10531
|
if (opts.reset) {
|
|
10530
|
-
await
|
|
10531
|
-
console.log(
|
|
10532
|
+
await fs23.writeJson(configPath, {}, { spaces: 2 });
|
|
10533
|
+
console.log(import_chalk18.default.green(`\u2714 Config reset: ${configPath}`));
|
|
10532
10534
|
return;
|
|
10533
10535
|
}
|
|
10534
10536
|
const existing = await loadConfig(currentDir);
|
|
10535
10537
|
if (opts.show) {
|
|
10536
10538
|
if (Object.keys(existing).length === 0) {
|
|
10537
|
-
console.log(
|
|
10539
|
+
console.log(import_chalk18.default.gray("No config file found. Using built-in defaults."));
|
|
10538
10540
|
} else {
|
|
10539
|
-
console.log(
|
|
10541
|
+
console.log(import_chalk18.default.bold(`${configPath}:`));
|
|
10540
10542
|
console.log(JSON.stringify(existing, null, 2));
|
|
10541
10543
|
}
|
|
10542
10544
|
return;
|
|
@@ -10550,27 +10552,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10550
10552
|
if (opts.minSpecScore !== void 0) {
|
|
10551
10553
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10552
10554
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10553
|
-
console.error(
|
|
10555
|
+
console.error(import_chalk18.default.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10554
10556
|
process.exit(1);
|
|
10555
10557
|
}
|
|
10556
10558
|
updated.minSpecScore = score;
|
|
10557
10559
|
}
|
|
10558
|
-
await
|
|
10559
|
-
console.log(
|
|
10560
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10561
|
+
console.log(import_chalk18.default.green(`\u2714 Config saved to ${configPath}`));
|
|
10560
10562
|
console.log(JSON.stringify(updated, null, 2));
|
|
10561
10563
|
});
|
|
10562
10564
|
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) => {
|
|
10563
10565
|
const currentDir = process.cwd();
|
|
10564
|
-
const configPath =
|
|
10566
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10565
10567
|
if (opts.list) {
|
|
10566
|
-
console.log(
|
|
10568
|
+
console.log(import_chalk18.default.bold("\nAvailable providers & models:\n"));
|
|
10567
10569
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10568
10570
|
console.log(
|
|
10569
|
-
` ${
|
|
10571
|
+
` ${import_chalk18.default.bold.cyan(key.padEnd(10))} ${import_chalk18.default.white(meta.displayName)}`
|
|
10570
10572
|
);
|
|
10571
|
-
console.log(
|
|
10573
|
+
console.log(import_chalk18.default.gray(` ${meta.description}`));
|
|
10572
10574
|
console.log(
|
|
10573
|
-
|
|
10575
|
+
import_chalk18.default.gray(
|
|
10574
10576
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10575
10577
|
)
|
|
10576
10578
|
);
|
|
@@ -10579,10 +10581,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10579
10581
|
return;
|
|
10580
10582
|
}
|
|
10581
10583
|
const existing = await loadConfig(currentDir);
|
|
10582
|
-
console.log(
|
|
10584
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10583
10585
|
if (Object.keys(existing).length > 0) {
|
|
10584
10586
|
console.log(
|
|
10585
|
-
|
|
10587
|
+
import_chalk18.default.gray(
|
|
10586
10588
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10587
10589
|
)
|
|
10588
10590
|
);
|
|
@@ -10600,7 +10602,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10600
10602
|
const providerKey = await (0, import_prompts3.select)({
|
|
10601
10603
|
message: `${label} \u2014 select provider:`,
|
|
10602
10604
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10603
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
10605
|
+
name: `${meta2.displayName.padEnd(22)} ${import_chalk18.default.gray(meta2.description)}`,
|
|
10604
10606
|
value: key,
|
|
10605
10607
|
short: meta2.displayName
|
|
10606
10608
|
}))
|
|
@@ -10608,7 +10610,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10608
10610
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10609
10611
|
const modelChoices = [
|
|
10610
10612
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10611
|
-
{ name:
|
|
10613
|
+
{ name: import_chalk18.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10612
10614
|
];
|
|
10613
10615
|
let chosenModel = await (0, import_prompts3.select)({
|
|
10614
10616
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10642,37 +10644,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10642
10644
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10643
10645
|
updated.codegen = "api";
|
|
10644
10646
|
console.log(
|
|
10645
|
-
|
|
10647
|
+
import_chalk18.default.yellow(
|
|
10646
10648
|
`
|
|
10647
10649
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10648
10650
|
)
|
|
10649
10651
|
);
|
|
10650
|
-
console.log(
|
|
10652
|
+
console.log(import_chalk18.default.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10651
10653
|
}
|
|
10652
10654
|
}
|
|
10653
10655
|
}
|
|
10654
|
-
console.log(
|
|
10655
|
-
console.log(
|
|
10656
|
+
console.log(import_chalk18.default.blue("\n Preview:"));
|
|
10657
|
+
console.log(import_chalk18.default.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10656
10658
|
if (updated.codegenProvider) {
|
|
10657
10659
|
console.log(
|
|
10658
|
-
|
|
10660
|
+
import_chalk18.default.gray(
|
|
10659
10661
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10660
10662
|
)
|
|
10661
10663
|
);
|
|
10662
10664
|
}
|
|
10663
10665
|
const ok = await (0, import_prompts3.confirm)({ message: "Save to .ai-spec.json?", default: true });
|
|
10664
10666
|
if (!ok) {
|
|
10665
|
-
console.log(
|
|
10667
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
10666
10668
|
return;
|
|
10667
10669
|
}
|
|
10668
|
-
await
|
|
10669
|
-
console.log(
|
|
10670
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10671
|
+
console.log(import_chalk18.default.green(`
|
|
10670
10672
|
\u2714 Saved to ${configPath}`));
|
|
10671
10673
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10672
10674
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10673
10675
|
if (envKey && !process.env[envKey]) {
|
|
10674
10676
|
console.log(
|
|
10675
|
-
|
|
10677
|
+
import_chalk18.default.yellow(
|
|
10676
10678
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10677
10679
|
)
|
|
10678
10680
|
);
|
|
@@ -10691,29 +10693,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10691
10693
|
cliOpts,
|
|
10692
10694
|
contractContextSection
|
|
10693
10695
|
} = opts;
|
|
10694
|
-
console.log(
|
|
10696
|
+
console.log(import_chalk18.default.blue(`
|
|
10695
10697
|
[${repoName}] Loading project context...`));
|
|
10696
10698
|
const loader = new ContextLoader(repoAbsPath);
|
|
10697
10699
|
let context = await loader.loadProjectContext();
|
|
10698
10700
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10699
|
-
console.log(
|
|
10700
|
-
console.log(
|
|
10701
|
+
console.log(import_chalk18.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10702
|
+
console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10701
10703
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10702
|
-
console.log(
|
|
10704
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10703
10705
|
}
|
|
10704
10706
|
if (!context.constitution) {
|
|
10705
|
-
console.log(
|
|
10707
|
+
console.log(import_chalk18.default.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10706
10708
|
try {
|
|
10707
10709
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10708
10710
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10709
10711
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10710
10712
|
context.constitution = constitutionContent;
|
|
10711
|
-
console.log(
|
|
10713
|
+
console.log(import_chalk18.default.green(` Constitution: generated`));
|
|
10712
10714
|
} catch (err) {
|
|
10713
|
-
console.log(
|
|
10715
|
+
console.log(import_chalk18.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10714
10716
|
}
|
|
10715
10717
|
} else {
|
|
10716
|
-
console.log(
|
|
10718
|
+
console.log(import_chalk18.default.green(` Constitution: found`));
|
|
10717
10719
|
}
|
|
10718
10720
|
let fullIdea = idea;
|
|
10719
10721
|
if (contractContextSection) {
|
|
@@ -10721,58 +10723,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10721
10723
|
|
|
10722
10724
|
${contractContextSection}`;
|
|
10723
10725
|
}
|
|
10724
|
-
console.log(
|
|
10726
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Generating spec...`));
|
|
10725
10727
|
let finalSpec;
|
|
10726
10728
|
try {
|
|
10727
10729
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10728
10730
|
finalSpec = result.spec;
|
|
10729
|
-
console.log(
|
|
10731
|
+
console.log(import_chalk18.default.green(` Spec generated.`));
|
|
10730
10732
|
} catch (err) {
|
|
10731
|
-
console.error(
|
|
10733
|
+
console.error(import_chalk18.default.red(` Spec generation failed: ${err.message}`));
|
|
10732
10734
|
return { dsl: null, specFile: null };
|
|
10733
10735
|
}
|
|
10734
10736
|
let extractedDsl = null;
|
|
10735
10737
|
if (!cliOpts.skipDsl) {
|
|
10736
|
-
console.log(
|
|
10738
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Extracting DSL...`));
|
|
10737
10739
|
try {
|
|
10738
10740
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10739
10741
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10740
10742
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10741
10743
|
if (extractedDsl) {
|
|
10742
|
-
console.log(
|
|
10744
|
+
console.log(import_chalk18.default.green(` DSL extracted.`));
|
|
10743
10745
|
}
|
|
10744
10746
|
} catch (err) {
|
|
10745
|
-
console.log(
|
|
10747
|
+
console.log(import_chalk18.default.yellow(` DSL extraction failed: ${err.message}`));
|
|
10746
10748
|
}
|
|
10747
10749
|
}
|
|
10748
10750
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10749
10751
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10750
10752
|
let workingDir = repoAbsPath;
|
|
10751
10753
|
if (!skipWorktreeForRepo) {
|
|
10752
|
-
console.log(
|
|
10754
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Setting up git worktree...`));
|
|
10753
10755
|
try {
|
|
10754
10756
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10755
10757
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10756
10758
|
if (worktreePath) workingDir = worktreePath;
|
|
10757
10759
|
} catch (err) {
|
|
10758
|
-
console.log(
|
|
10760
|
+
console.log(import_chalk18.default.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10759
10761
|
}
|
|
10760
10762
|
} else {
|
|
10761
|
-
console.log(
|
|
10763
|
+
console.log(import_chalk18.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10762
10764
|
}
|
|
10763
|
-
const specsDir =
|
|
10764
|
-
await
|
|
10765
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10766
|
+
await fs23.ensureDir(specsDir);
|
|
10765
10767
|
const featureSlug = slugify(idea);
|
|
10766
10768
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10767
|
-
await
|
|
10768
|
-
console.log(
|
|
10769
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10770
|
+
console.log(import_chalk18.default.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
|
|
10769
10771
|
let savedDslFile = null;
|
|
10770
10772
|
if (extractedDsl) {
|
|
10771
10773
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10772
10774
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10773
|
-
console.log(
|
|
10775
|
+
console.log(import_chalk18.default.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
|
|
10774
10776
|
}
|
|
10775
|
-
console.log(
|
|
10777
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10776
10778
|
try {
|
|
10777
10779
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10778
10780
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10780,29 +10782,29 @@ ${contractContextSection}`;
|
|
|
10780
10782
|
dslFilePath: savedDslFile ?? void 0,
|
|
10781
10783
|
repoType: detectedRepoType
|
|
10782
10784
|
});
|
|
10783
|
-
console.log(
|
|
10785
|
+
console.log(import_chalk18.default.green(` Code generation complete.`));
|
|
10784
10786
|
} catch (err) {
|
|
10785
|
-
console.log(
|
|
10787
|
+
console.log(import_chalk18.default.yellow(` Code generation failed: ${err.message}`));
|
|
10786
10788
|
}
|
|
10787
10789
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10788
|
-
console.log(
|
|
10790
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Generating test skeletons...`));
|
|
10789
10791
|
try {
|
|
10790
10792
|
const testGen = new TestGenerator(codegenProvider);
|
|
10791
10793
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10792
|
-
console.log(
|
|
10794
|
+
console.log(import_chalk18.default.green(` ${testFiles.length} test file(s) generated.`));
|
|
10793
10795
|
} catch (err) {
|
|
10794
|
-
console.log(
|
|
10796
|
+
console.log(import_chalk18.default.yellow(` Test generation failed: ${err.message}`));
|
|
10795
10797
|
}
|
|
10796
10798
|
}
|
|
10797
10799
|
if (!cliOpts.skipErrorFeedback) {
|
|
10798
10800
|
try {
|
|
10799
10801
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10800
10802
|
} catch (err) {
|
|
10801
|
-
console.log(
|
|
10803
|
+
console.log(import_chalk18.default.yellow(` Error feedback failed: ${err.message}`));
|
|
10802
10804
|
}
|
|
10803
10805
|
}
|
|
10804
10806
|
if (!cliOpts.skipReview) {
|
|
10805
|
-
console.log(
|
|
10807
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Running code review...`));
|
|
10806
10808
|
try {
|
|
10807
10809
|
const reviewer = new CodeReviewer(specProvider);
|
|
10808
10810
|
const originalDir = process.cwd();
|
|
@@ -10814,9 +10816,9 @@ ${contractContextSection}`;
|
|
|
10814
10816
|
process.chdir(originalDir);
|
|
10815
10817
|
}
|
|
10816
10818
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10817
|
-
console.log(
|
|
10819
|
+
console.log(import_chalk18.default.green(` Code review complete.`));
|
|
10818
10820
|
} catch (err) {
|
|
10819
|
-
console.log(
|
|
10821
|
+
console.log(import_chalk18.default.yellow(` Code review failed: ${err.message}`));
|
|
10820
10822
|
}
|
|
10821
10823
|
}
|
|
10822
10824
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10839,7 +10841,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10839
10841
|
codegenModel: codegenModelName
|
|
10840
10842
|
});
|
|
10841
10843
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10842
|
-
console.log(
|
|
10844
|
+
console.log(import_chalk18.default.blue("\n[W1] Loading per-repo contexts..."));
|
|
10843
10845
|
const contexts = /* @__PURE__ */ new Map();
|
|
10844
10846
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10845
10847
|
for (const repo of workspace.repos) {
|
|
@@ -10851,27 +10853,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10851
10853
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10852
10854
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10853
10855
|
frontendContexts.set(repo.name, fctx);
|
|
10854
|
-
console.log(
|
|
10856
|
+
console.log(import_chalk18.default.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10855
10857
|
} else {
|
|
10856
|
-
console.log(
|
|
10858
|
+
console.log(import_chalk18.default.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10857
10859
|
}
|
|
10858
10860
|
} catch (err) {
|
|
10859
|
-
console.log(
|
|
10861
|
+
console.log(import_chalk18.default.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10860
10862
|
}
|
|
10861
10863
|
}
|
|
10862
|
-
console.log(
|
|
10864
|
+
console.log(import_chalk18.default.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10863
10865
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10864
10866
|
let decomposition;
|
|
10865
10867
|
try {
|
|
10866
10868
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10867
|
-
console.log(
|
|
10868
|
-
console.log(
|
|
10869
|
+
console.log(import_chalk18.default.green(` Summary: ${decomposition.summary}`));
|
|
10870
|
+
console.log(import_chalk18.default.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10869
10871
|
if (decomposition.coordinationNotes) {
|
|
10870
|
-
console.log(
|
|
10872
|
+
console.log(import_chalk18.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10871
10873
|
}
|
|
10872
10874
|
} catch (err) {
|
|
10873
|
-
console.error(
|
|
10874
|
-
console.log(
|
|
10875
|
+
console.error(import_chalk18.default.red(` Decomposition failed: ${err.message}`));
|
|
10876
|
+
console.log(import_chalk18.default.yellow(" Falling back to running all repos independently."));
|
|
10875
10877
|
decomposition = {
|
|
10876
10878
|
originalRequirement: idea,
|
|
10877
10879
|
summary: idea,
|
|
@@ -10887,11 +10889,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10887
10889
|
};
|
|
10888
10890
|
}
|
|
10889
10891
|
if (!opts.auto) {
|
|
10890
|
-
console.log(
|
|
10891
|
-
console.log(
|
|
10892
|
+
console.log(import_chalk18.default.cyan("\n[W3] Decomposition Preview:"));
|
|
10893
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
|
|
10892
10894
|
for (const r of decomposition.repos) {
|
|
10893
|
-
console.log(
|
|
10894
|
-
console.log(
|
|
10895
|
+
console.log(import_chalk18.default.bold(` ${r.repoName} (${r.role})`));
|
|
10896
|
+
console.log(import_chalk18.default.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
10895
10897
|
if (r.uxDecisions) {
|
|
10896
10898
|
const ux = r.uxDecisions;
|
|
10897
10899
|
const uxSummary = [
|
|
@@ -10900,13 +10902,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10900
10902
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
10901
10903
|
ux.errorRollback ? "rollback" : ""
|
|
10902
10904
|
].filter(Boolean).join(", ");
|
|
10903
|
-
if (uxSummary) console.log(
|
|
10905
|
+
if (uxSummary) console.log(import_chalk18.default.cyan(` UX: ${uxSummary}`));
|
|
10904
10906
|
}
|
|
10905
10907
|
if (r.dependsOnRepos.length > 0) {
|
|
10906
|
-
console.log(
|
|
10908
|
+
console.log(import_chalk18.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
10907
10909
|
}
|
|
10908
10910
|
}
|
|
10909
|
-
console.log(
|
|
10911
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
|
|
10910
10912
|
const gate = await (0, import_prompts3.select)({
|
|
10911
10913
|
message: "Proceed with multi-repo pipeline?",
|
|
10912
10914
|
choices: [
|
|
@@ -10915,24 +10917,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10915
10917
|
]
|
|
10916
10918
|
});
|
|
10917
10919
|
if (gate === "abort") {
|
|
10918
|
-
console.log(
|
|
10920
|
+
console.log(import_chalk18.default.yellow(" Aborted."));
|
|
10919
10921
|
process.exit(0);
|
|
10920
10922
|
}
|
|
10921
10923
|
}
|
|
10922
10924
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
10923
10925
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
10924
|
-
console.log(
|
|
10926
|
+
console.log(import_chalk18.default.blue(`
|
|
10925
10927
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
10926
10928
|
const results = [];
|
|
10927
10929
|
for (const repoReq of sortedRepoRequirements) {
|
|
10928
10930
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
10929
10931
|
if (!repoConfig) {
|
|
10930
|
-
console.log(
|
|
10932
|
+
console.log(import_chalk18.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
10931
10933
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
10932
10934
|
continue;
|
|
10933
10935
|
}
|
|
10934
10936
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
10935
|
-
console.log(
|
|
10937
|
+
console.log(import_chalk18.default.bold.blue(`
|
|
10936
10938
|
\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`));
|
|
10937
10939
|
let contractContextSection;
|
|
10938
10940
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -10940,7 +10942,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10940
10942
|
for (const depName of repoReq.dependsOnRepos) {
|
|
10941
10943
|
const depDsl = contractDsls.get(depName);
|
|
10942
10944
|
if (depDsl) {
|
|
10943
|
-
console.log(
|
|
10945
|
+
console.log(import_chalk18.default.gray(` Using API contract from: ${depName}`));
|
|
10944
10946
|
const contract = buildFrontendApiContract(depDsl);
|
|
10945
10947
|
contractParts.push(buildContractContextSection(contract));
|
|
10946
10948
|
}
|
|
@@ -10960,7 +10962,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10960
10962
|
frontendContext: frontendCtx
|
|
10961
10963
|
});
|
|
10962
10964
|
contractContextSection = void 0;
|
|
10963
|
-
console.log(
|
|
10965
|
+
console.log(import_chalk18.default.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
10964
10966
|
}
|
|
10965
10967
|
try {
|
|
10966
10968
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -10977,22 +10979,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10977
10979
|
});
|
|
10978
10980
|
if (repoReq.isContractProvider && dsl) {
|
|
10979
10981
|
contractDsls.set(repoReq.repoName, dsl);
|
|
10980
|
-
console.log(
|
|
10982
|
+
console.log(import_chalk18.default.green(` Contract stored for downstream repos.`));
|
|
10981
10983
|
}
|
|
10982
10984
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
10983
|
-
console.log(
|
|
10985
|
+
console.log(import_chalk18.default.green(` \u2714 ${repoReq.repoName} complete`));
|
|
10984
10986
|
} catch (err) {
|
|
10985
|
-
console.error(
|
|
10987
|
+
console.error(import_chalk18.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
10986
10988
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
10987
10989
|
}
|
|
10988
10990
|
}
|
|
10989
|
-
console.log(
|
|
10990
|
-
console.log(
|
|
10991
|
-
console.log(
|
|
10991
|
+
console.log(import_chalk18.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
10992
|
+
console.log(import_chalk18.default.gray(` Workspace: ${workspace.name}`));
|
|
10993
|
+
console.log(import_chalk18.default.gray(` Requirement: ${idea}`));
|
|
10992
10994
|
console.log();
|
|
10993
10995
|
for (const r of results) {
|
|
10994
|
-
const icon = r.status === "success" ?
|
|
10995
|
-
const specInfo = r.specFile ?
|
|
10996
|
+
const icon = r.status === "success" ? import_chalk18.default.green("\u2714") : r.status === "failed" ? import_chalk18.default.red("\u2718") : import_chalk18.default.gray("\u2212");
|
|
10997
|
+
const specInfo = r.specFile ? import_chalk18.default.gray(` \u2192 ${r.specFile}`) : "";
|
|
10996
10998
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
10997
10999
|
}
|
|
10998
11000
|
return results;
|
|
@@ -11000,18 +11002,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11000
11002
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
11001
11003
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
11002
11004
|
const currentDir = process.cwd();
|
|
11003
|
-
const configPath =
|
|
11004
|
-
if (await
|
|
11005
|
+
const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
11006
|
+
if (await fs23.pathExists(configPath)) {
|
|
11005
11007
|
const overwrite = await (0, import_prompts3.confirm)({
|
|
11006
11008
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
11007
11009
|
default: false
|
|
11008
11010
|
});
|
|
11009
11011
|
if (!overwrite) {
|
|
11010
|
-
console.log(
|
|
11012
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
11011
11013
|
return;
|
|
11012
11014
|
}
|
|
11013
11015
|
}
|
|
11014
|
-
console.log(
|
|
11016
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11015
11017
|
const workspaceName = await (0, import_prompts3.input)({
|
|
11016
11018
|
message: "Workspace name:",
|
|
11017
11019
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -11025,11 +11027,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11025
11027
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11026
11028
|
const detected = await workspaceLoader.autoDetect();
|
|
11027
11029
|
if (detected.length === 0) {
|
|
11028
|
-
console.log(
|
|
11030
|
+
console.log(import_chalk18.default.yellow(" No recognizable repos found in sibling directories."));
|
|
11029
11031
|
} else {
|
|
11030
|
-
console.log(
|
|
11032
|
+
console.log(import_chalk18.default.cyan("\n Detected repos:"));
|
|
11031
11033
|
for (const r of detected) {
|
|
11032
|
-
console.log(
|
|
11034
|
+
console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11033
11035
|
}
|
|
11034
11036
|
const keepAll = await (0, import_prompts3.confirm)({
|
|
11035
11037
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -11046,7 +11048,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11046
11048
|
if (keep) repos.push(r);
|
|
11047
11049
|
}
|
|
11048
11050
|
}
|
|
11049
|
-
console.log(
|
|
11051
|
+
console.log(import_chalk18.default.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
11050
11052
|
}
|
|
11051
11053
|
}
|
|
11052
11054
|
const repoTypeChoices = [
|
|
@@ -11068,7 +11070,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11068
11070
|
default: repos.length === 0
|
|
11069
11071
|
});
|
|
11070
11072
|
while (addMore) {
|
|
11071
|
-
console.log(
|
|
11073
|
+
console.log(import_chalk18.default.cyan(`
|
|
11072
11074
|
Adding repo #${repos.length + 1}`));
|
|
11073
11075
|
const repoName = await (0, import_prompts3.input)({
|
|
11074
11076
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -11082,16 +11084,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11082
11084
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
11083
11085
|
default: `./${repoName}`
|
|
11084
11086
|
});
|
|
11085
|
-
const absPath =
|
|
11087
|
+
const absPath = path22.resolve(currentDir, repoPath);
|
|
11086
11088
|
let detectedType = "unknown";
|
|
11087
11089
|
let detectedRole = "shared";
|
|
11088
|
-
if (await
|
|
11090
|
+
if (await fs23.pathExists(absPath)) {
|
|
11089
11091
|
const { type, role } = await detectRepoType(absPath);
|
|
11090
11092
|
detectedType = type;
|
|
11091
11093
|
detectedRole = role;
|
|
11092
|
-
console.log(
|
|
11094
|
+
console.log(import_chalk18.default.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
11093
11095
|
} else {
|
|
11094
|
-
console.log(
|
|
11096
|
+
console.log(import_chalk18.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
11095
11097
|
}
|
|
11096
11098
|
const repoType = await (0, import_prompts3.select)({
|
|
11097
11099
|
message: `Repo type for "${repoName}":`,
|
|
@@ -11114,53 +11116,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11114
11116
|
type: repoType,
|
|
11115
11117
|
role: repoRole
|
|
11116
11118
|
});
|
|
11117
|
-
console.log(
|
|
11119
|
+
console.log(import_chalk18.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
11118
11120
|
addMore = await (0, import_prompts3.confirm)({
|
|
11119
11121
|
message: "Add another repo?",
|
|
11120
11122
|
default: false
|
|
11121
11123
|
});
|
|
11122
11124
|
}
|
|
11123
11125
|
const workspaceConfig = { name: workspaceName, repos };
|
|
11124
|
-
console.log(
|
|
11125
|
-
console.log(
|
|
11126
|
+
console.log(import_chalk18.default.cyan("\n Workspace summary:"));
|
|
11127
|
+
console.log(import_chalk18.default.gray(` Name: ${workspaceName}`));
|
|
11126
11128
|
for (const r of repos) {
|
|
11127
|
-
console.log(
|
|
11129
|
+
console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11128
11130
|
}
|
|
11129
11131
|
const ok = await (0, import_prompts3.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
11130
11132
|
if (!ok) {
|
|
11131
|
-
console.log(
|
|
11133
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
11132
11134
|
return;
|
|
11133
11135
|
}
|
|
11134
11136
|
const loader = new WorkspaceLoader(currentDir);
|
|
11135
11137
|
const saved = await loader.save(workspaceConfig);
|
|
11136
|
-
console.log(
|
|
11138
|
+
console.log(import_chalk18.default.green(`
|
|
11137
11139
|
\u2714 Workspace saved: ${saved}`));
|
|
11138
|
-
console.log(
|
|
11140
|
+
console.log(import_chalk18.default.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
11139
11141
|
});
|
|
11140
11142
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
11141
11143
|
const currentDir = process.cwd();
|
|
11142
11144
|
const loader = new WorkspaceLoader(currentDir);
|
|
11143
11145
|
const config2 = await loader.load();
|
|
11144
11146
|
if (!config2) {
|
|
11145
|
-
console.log(
|
|
11146
|
-
console.log(
|
|
11147
|
+
console.log(import_chalk18.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11148
|
+
console.log(import_chalk18.default.gray(" Run `ai-spec workspace init` to create one."));
|
|
11147
11149
|
return;
|
|
11148
11150
|
}
|
|
11149
|
-
console.log(
|
|
11151
|
+
console.log(import_chalk18.default.bold(`
|
|
11150
11152
|
Workspace: ${config2.name}`));
|
|
11151
|
-
console.log(
|
|
11152
|
-
console.log(
|
|
11153
|
+
console.log(import_chalk18.default.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11154
|
+
console.log(import_chalk18.default.gray(` Repos (${config2.repos.length}):
|
|
11153
11155
|
`));
|
|
11154
11156
|
for (const repo of config2.repos) {
|
|
11155
11157
|
const absPath = loader.resolveAbsPath(repo);
|
|
11156
|
-
const exists = await
|
|
11157
|
-
const status = exists ?
|
|
11158
|
+
const exists = await fs23.pathExists(absPath);
|
|
11159
|
+
const status = exists ? import_chalk18.default.green("found") : import_chalk18.default.red("not found");
|
|
11158
11160
|
console.log(
|
|
11159
|
-
` ${
|
|
11161
|
+
` ${import_chalk18.default.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
11160
11162
|
);
|
|
11161
|
-
console.log(
|
|
11163
|
+
console.log(import_chalk18.default.gray(` path: ${absPath}`));
|
|
11162
11164
|
if (repo.constitution) {
|
|
11163
|
-
console.log(
|
|
11165
|
+
console.log(import_chalk18.default.green(` constitution: found`));
|
|
11164
11166
|
}
|
|
11165
11167
|
}
|
|
11166
11168
|
});
|
|
@@ -11177,30 +11179,30 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11177
11179
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
11178
11180
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
11179
11181
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11180
|
-
console.log(
|
|
11181
|
-
console.log(
|
|
11182
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11183
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
11182
11184
|
const updateRunId = generateRunId();
|
|
11183
11185
|
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11184
11186
|
setActiveSnapshot(updateSnapshot);
|
|
11185
11187
|
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11186
11188
|
setActiveLogger(updateLogger);
|
|
11187
|
-
console.log(
|
|
11189
|
+
console.log(import_chalk18.default.gray(` Run ID: ${updateRunId}`));
|
|
11188
11190
|
let specPath = opts.spec ?? null;
|
|
11189
11191
|
if (!specPath) {
|
|
11190
|
-
const specsDir =
|
|
11192
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
11191
11193
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11192
11194
|
if (!latest) {
|
|
11193
|
-
console.error(
|
|
11195
|
+
console.error(import_chalk18.default.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11194
11196
|
process.exit(1);
|
|
11195
11197
|
}
|
|
11196
11198
|
specPath = latest.filePath;
|
|
11197
|
-
console.log(
|
|
11199
|
+
console.log(import_chalk18.default.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11198
11200
|
}
|
|
11199
|
-
console.log(
|
|
11201
|
+
console.log(import_chalk18.default.gray(" Loading project context..."));
|
|
11200
11202
|
const loader = new ContextLoader(currentDir);
|
|
11201
11203
|
const context = await loader.loadProjectContext();
|
|
11202
11204
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
11203
|
-
console.log(
|
|
11205
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11204
11206
|
}
|
|
11205
11207
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
11206
11208
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -11212,19 +11214,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11212
11214
|
repoType
|
|
11213
11215
|
});
|
|
11214
11216
|
} catch (err) {
|
|
11215
|
-
console.error(
|
|
11217
|
+
console.error(import_chalk18.default.red(` Update failed: ${err.message}`));
|
|
11216
11218
|
process.exit(1);
|
|
11217
11219
|
}
|
|
11218
|
-
console.log(
|
|
11219
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11220
|
+
console.log(import_chalk18.default.green(`
|
|
11221
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
|
|
11220
11222
|
if (result.newDslPath) {
|
|
11221
|
-
console.log(
|
|
11223
|
+
console.log(import_chalk18.default.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
|
|
11222
11224
|
}
|
|
11223
11225
|
if (result.affectedFiles.length > 0) {
|
|
11224
|
-
console.log(
|
|
11226
|
+
console.log(import_chalk18.default.cyan("\n Affected files:"));
|
|
11225
11227
|
for (const f of result.affectedFiles) {
|
|
11226
|
-
const icon = f.action === "create" ?
|
|
11227
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11228
|
+
const icon = f.action === "create" ? import_chalk18.default.green("+") : import_chalk18.default.yellow("~");
|
|
11229
|
+
console.log(` ${icon} ${f.file}: ${import_chalk18.default.gray(f.description)}`);
|
|
11228
11230
|
}
|
|
11229
11231
|
}
|
|
11230
11232
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -11232,9 +11234,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11232
11234
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
11233
11235
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
11234
11236
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11235
|
-
console.log(
|
|
11237
|
+
console.log(import_chalk18.default.blue("\n Regenerating affected files..."));
|
|
11236
11238
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11237
|
-
const specContent = await
|
|
11239
|
+
const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
|
|
11238
11240
|
const constitutionSection = context.constitution ? `
|
|
11239
11241
|
=== Project Constitution (MUST follow) ===
|
|
11240
11242
|
${context.constitution}
|
|
@@ -11245,10 +11247,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
|
11245
11247
|
` : "";
|
|
11246
11248
|
updateLogger.stageStart("update_codegen");
|
|
11247
11249
|
for (const affected of result.affectedFiles) {
|
|
11248
|
-
const fullPath =
|
|
11250
|
+
const fullPath = path22.join(currentDir, affected.file);
|
|
11249
11251
|
let existing = "";
|
|
11250
11252
|
try {
|
|
11251
|
-
existing = await
|
|
11253
|
+
existing = await fs23.readFile(fullPath, "utf-8");
|
|
11252
11254
|
} catch {
|
|
11253
11255
|
}
|
|
11254
11256
|
const codePrompt = `Apply this change to the file.
|
|
@@ -11262,23 +11264,23 @@ ${specContent}
|
|
|
11262
11264
|
${constitutionSection}${dslSection}
|
|
11263
11265
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
11264
11266
|
${existing || "Create from scratch."}`;
|
|
11265
|
-
process.stdout.write(` ${existing ?
|
|
11267
|
+
process.stdout.write(` ${existing ? import_chalk18.default.yellow("~") : import_chalk18.default.green("+")} ${affected.file}... `);
|
|
11266
11268
|
try {
|
|
11267
11269
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11268
11270
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11269
11271
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11270
|
-
await
|
|
11272
|
+
await fs23.ensureDir(path22.dirname(fullPath));
|
|
11271
11273
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11272
|
-
await
|
|
11274
|
+
await fs23.writeFile(fullPath, content, "utf-8");
|
|
11273
11275
|
updateLogger.fileWritten(affected.file);
|
|
11274
|
-
console.log(
|
|
11276
|
+
console.log(import_chalk18.default.green("\u2714"));
|
|
11275
11277
|
} catch (err) {
|
|
11276
11278
|
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11277
|
-
console.log(
|
|
11279
|
+
console.log(import_chalk18.default.red(`\u2718 ${err.message}`));
|
|
11278
11280
|
}
|
|
11279
11281
|
}
|
|
11280
11282
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11281
|
-
const updatedSpecContent = await
|
|
11283
|
+
const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11282
11284
|
if (updatedSpecContent) {
|
|
11283
11285
|
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11284
11286
|
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
@@ -11290,13 +11292,13 @@ ${existing || "Create from scratch."}`;
|
|
|
11290
11292
|
updateLogger.finish();
|
|
11291
11293
|
updateLogger.printSummary();
|
|
11292
11294
|
if (updateSnapshot.fileCount > 0) {
|
|
11293
|
-
console.log(
|
|
11295
|
+
console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11294
11296
|
}
|
|
11295
11297
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11296
|
-
console.log(
|
|
11297
|
-
console.log(
|
|
11298
|
-
console.log(
|
|
11299
|
-
console.log(
|
|
11298
|
+
console.log(import_chalk18.default.blue("\n Next steps:"));
|
|
11299
|
+
console.log(import_chalk18.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11300
|
+
console.log(import_chalk18.default.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11301
|
+
console.log(import_chalk18.default.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
11300
11302
|
}
|
|
11301
11303
|
});
|
|
11302
11304
|
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) => {
|
|
@@ -11305,19 +11307,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11305
11307
|
if (!dslPath) {
|
|
11306
11308
|
dslPath = await findLatestDslFile(currentDir);
|
|
11307
11309
|
if (!dslPath) {
|
|
11308
|
-
console.error(
|
|
11310
|
+
console.error(import_chalk18.default.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11309
11311
|
process.exit(1);
|
|
11310
11312
|
}
|
|
11311
|
-
console.log(
|
|
11313
|
+
console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11312
11314
|
}
|
|
11313
11315
|
let dsl;
|
|
11314
11316
|
try {
|
|
11315
|
-
dsl = await
|
|
11317
|
+
dsl = await fs23.readJson(dslPath);
|
|
11316
11318
|
} catch (err) {
|
|
11317
|
-
console.error(
|
|
11319
|
+
console.error(import_chalk18.default.red(` Failed to read DSL: ${err.message}`));
|
|
11318
11320
|
process.exit(1);
|
|
11319
11321
|
}
|
|
11320
|
-
console.log(
|
|
11322
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11321
11323
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
11322
11324
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
11323
11325
|
try {
|
|
@@ -11326,31 +11328,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11326
11328
|
serverUrl,
|
|
11327
11329
|
outputPath: opts.output
|
|
11328
11330
|
});
|
|
11329
|
-
const rel =
|
|
11330
|
-
console.log(
|
|
11331
|
-
console.log(
|
|
11332
|
-
console.log(
|
|
11333
|
-
console.log(
|
|
11334
|
-
console.log(
|
|
11335
|
-
console.log(
|
|
11336
|
-
console.log(
|
|
11337
|
-
console.log(
|
|
11331
|
+
const rel = path22.relative(currentDir, outputPath);
|
|
11332
|
+
console.log(import_chalk18.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11333
|
+
console.log(import_chalk18.default.gray(` Feature : ${dsl.feature.title}`));
|
|
11334
|
+
console.log(import_chalk18.default.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11335
|
+
console.log(import_chalk18.default.gray(` Models : ${dsl.models.length}`));
|
|
11336
|
+
console.log(import_chalk18.default.gray(` Server : ${serverUrl}`));
|
|
11337
|
+
console.log(import_chalk18.default.blue("\n Next steps:"));
|
|
11338
|
+
console.log(import_chalk18.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11339
|
+
console.log(import_chalk18.default.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
11338
11340
|
} catch (err) {
|
|
11339
|
-
console.error(
|
|
11341
|
+
console.error(import_chalk18.default.red(` Export failed: ${err.message}`));
|
|
11340
11342
|
process.exit(1);
|
|
11341
11343
|
}
|
|
11342
11344
|
});
|
|
11343
11345
|
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) => {
|
|
11344
11346
|
const currentDir = process.cwd();
|
|
11345
11347
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11346
|
-
console.log(
|
|
11348
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11347
11349
|
if (opts.restore) {
|
|
11348
|
-
const frontendDir = opts.frontend ?
|
|
11350
|
+
const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
|
|
11349
11351
|
const r = await restoreMockProxy(frontendDir);
|
|
11350
11352
|
if (r.restored) {
|
|
11351
|
-
console.log(
|
|
11353
|
+
console.log(import_chalk18.default.green(" \u2714 Proxy restored and mock server stopped."));
|
|
11352
11354
|
} else {
|
|
11353
|
-
console.log(
|
|
11355
|
+
console.log(import_chalk18.default.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
11354
11356
|
}
|
|
11355
11357
|
return;
|
|
11356
11358
|
}
|
|
@@ -11358,32 +11360,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11358
11360
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11359
11361
|
const workspaceConfig = await workspaceLoader.load();
|
|
11360
11362
|
if (!workspaceConfig) {
|
|
11361
|
-
console.error(
|
|
11363
|
+
console.error(import_chalk18.default.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
11362
11364
|
process.exit(1);
|
|
11363
11365
|
}
|
|
11364
11366
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
11365
11367
|
if (backendRepos.length === 0) {
|
|
11366
|
-
console.log(
|
|
11368
|
+
console.log(import_chalk18.default.yellow(" No backend repos found in workspace."));
|
|
11367
11369
|
return;
|
|
11368
11370
|
}
|
|
11369
11371
|
for (const repo of backendRepos) {
|
|
11370
11372
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
11371
|
-
console.log(
|
|
11373
|
+
console.log(import_chalk18.default.cyan(`
|
|
11372
11374
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
11373
11375
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
11374
11376
|
if (!dslFile) {
|
|
11375
|
-
console.log(
|
|
11377
|
+
console.log(import_chalk18.default.yellow(` No DSL file found \u2014 skipping.`));
|
|
11376
11378
|
continue;
|
|
11377
11379
|
}
|
|
11378
|
-
const dsl2 = await
|
|
11380
|
+
const dsl2 = await fs23.readJson(dslFile);
|
|
11379
11381
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
11380
11382
|
port,
|
|
11381
11383
|
msw: opts.msw,
|
|
11382
11384
|
proxy: opts.proxy
|
|
11383
11385
|
});
|
|
11384
11386
|
for (const f of result2.files) {
|
|
11385
|
-
console.log(
|
|
11386
|
-
console.log(
|
|
11387
|
+
console.log(import_chalk18.default.green(` \u2714 ${f.path}`));
|
|
11388
|
+
console.log(import_chalk18.default.gray(` ${f.description}`));
|
|
11387
11389
|
}
|
|
11388
11390
|
}
|
|
11389
11391
|
return;
|
|
@@ -11393,19 +11395,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11393
11395
|
dslPath = await findLatestDslFile(currentDir);
|
|
11394
11396
|
if (!dslPath) {
|
|
11395
11397
|
console.error(
|
|
11396
|
-
|
|
11398
|
+
import_chalk18.default.red(
|
|
11397
11399
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
11398
11400
|
)
|
|
11399
11401
|
);
|
|
11400
11402
|
process.exit(1);
|
|
11401
11403
|
}
|
|
11402
|
-
console.log(
|
|
11404
|
+
console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11403
11405
|
}
|
|
11404
11406
|
let dsl;
|
|
11405
11407
|
try {
|
|
11406
|
-
dsl = await
|
|
11408
|
+
dsl = await fs23.readJson(dslPath);
|
|
11407
11409
|
} catch (err) {
|
|
11408
|
-
console.error(
|
|
11410
|
+
console.error(import_chalk18.default.red(` Failed to read DSL file: ${err.message}`));
|
|
11409
11411
|
process.exit(1);
|
|
11410
11412
|
}
|
|
11411
11413
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11413,58 +11415,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11413
11415
|
msw: opts.msw,
|
|
11414
11416
|
proxy: opts.proxy
|
|
11415
11417
|
});
|
|
11416
|
-
console.log(
|
|
11418
|
+
console.log(import_chalk18.default.green(`
|
|
11417
11419
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11418
11420
|
for (const f of result.files) {
|
|
11419
|
-
console.log(
|
|
11420
|
-
console.log(
|
|
11421
|
+
console.log(import_chalk18.default.green(` ${f.path}`));
|
|
11422
|
+
console.log(import_chalk18.default.gray(` ${f.description}`));
|
|
11421
11423
|
}
|
|
11422
11424
|
if (opts.serve) {
|
|
11423
|
-
const serverJsPath =
|
|
11424
|
-
if (!await
|
|
11425
|
-
console.error(
|
|
11425
|
+
const serverJsPath = path22.join(currentDir, "mock", "server.js");
|
|
11426
|
+
if (!await fs23.pathExists(serverJsPath)) {
|
|
11427
|
+
console.error(import_chalk18.default.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11426
11428
|
process.exit(1);
|
|
11427
11429
|
}
|
|
11428
11430
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11429
|
-
console.log(
|
|
11431
|
+
console.log(import_chalk18.default.green(`
|
|
11430
11432
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11431
11433
|
if (opts.frontend) {
|
|
11432
|
-
const frontendDir =
|
|
11434
|
+
const frontendDir = path22.resolve(opts.frontend);
|
|
11433
11435
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11434
11436
|
await saveMockServerPid(frontendDir, pid);
|
|
11435
11437
|
if (proxyResult.applied) {
|
|
11436
|
-
console.log(
|
|
11437
|
-
console.log(
|
|
11438
|
+
console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11439
|
+
console.log(import_chalk18.default.bold.cyan(`
|
|
11438
11440
|
Ready! Open a new terminal and run:`));
|
|
11439
|
-
console.log(
|
|
11440
|
-
console.log(
|
|
11441
|
-
console.log(
|
|
11441
|
+
console.log(import_chalk18.default.white(` cd ${frontendDir}`));
|
|
11442
|
+
console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
|
|
11443
|
+
console.log(import_chalk18.default.gray(`
|
|
11442
11444
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11443
11445
|
} else {
|
|
11444
|
-
console.log(
|
|
11445
|
-
if (proxyResult.note) console.log(
|
|
11446
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11447
|
+
if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
|
|
11446
11448
|
}
|
|
11447
11449
|
} else {
|
|
11448
|
-
console.log(
|
|
11449
|
-
console.log(
|
|
11450
|
+
console.log(import_chalk18.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11451
|
+
console.log(import_chalk18.default.gray(` Mock server: http://localhost:${port}`));
|
|
11450
11452
|
}
|
|
11451
11453
|
return;
|
|
11452
11454
|
}
|
|
11453
|
-
console.log(
|
|
11454
|
-
console.log(
|
|
11455
|
-
console.log(
|
|
11456
|
-
console.log(
|
|
11457
|
-
console.log(
|
|
11458
|
-
console.log(
|
|
11459
|
-
console.log(
|
|
11460
|
-
console.log(
|
|
11455
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11456
|
+
console.log(import_chalk18.default.white(` 1. Install express (if not already):`));
|
|
11457
|
+
console.log(import_chalk18.default.gray(` npm install --save-dev express`));
|
|
11458
|
+
console.log(import_chalk18.default.white(` 2. Start mock server:`));
|
|
11459
|
+
console.log(import_chalk18.default.gray(` node mock/server.js`));
|
|
11460
|
+
console.log(import_chalk18.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
11461
|
+
console.log(import_chalk18.default.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
11462
|
+
console.log(import_chalk18.default.gray(` http://localhost:${port}`));
|
|
11461
11463
|
if (opts.proxy) {
|
|
11462
|
-
console.log(
|
|
11464
|
+
console.log(import_chalk18.default.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11463
11465
|
}
|
|
11464
11466
|
if (opts.msw) {
|
|
11465
|
-
console.log(
|
|
11466
|
-
console.log(
|
|
11467
|
-
console.log(
|
|
11467
|
+
console.log(import_chalk18.default.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
11468
|
+
console.log(import_chalk18.default.gray(` import { worker } from './mocks/browser';`));
|
|
11469
|
+
console.log(import_chalk18.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11468
11470
|
}
|
|
11469
11471
|
});
|
|
11470
11472
|
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) => {
|
|
@@ -11480,34 +11482,26 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11480
11482
|
}
|
|
11481
11483
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11482
11484
|
if (result.appended) {
|
|
11483
|
-
console.log(
|
|
11485
|
+
console.log(import_chalk18.default.green(`
|
|
11484
11486
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11485
|
-
console.log(
|
|
11487
|
+
console.log(import_chalk18.default.gray(` File: .ai-spec-constitution.md`));
|
|
11486
11488
|
} else {
|
|
11487
|
-
console.log(
|
|
11489
|
+
console.log(import_chalk18.default.yellow(`
|
|
11488
11490
|
\u26A0 Not appended: ${result.reason}`));
|
|
11489
11491
|
}
|
|
11490
11492
|
});
|
|
11491
11493
|
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) => {
|
|
11492
11494
|
const currentDir = process.cwd();
|
|
11493
11495
|
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11494
|
-
console.log(
|
|
11496
|
+
console.log(import_chalk18.default.blue(`Restoring run: ${runId}...`));
|
|
11495
11497
|
const restored = await snapshot.restore();
|
|
11496
11498
|
if (restored.length === 0) {
|
|
11497
|
-
console.log(
|
|
11499
|
+
console.log(import_chalk18.default.yellow(" No backup found for this run ID."));
|
|
11498
11500
|
} else {
|
|
11499
|
-
restored.forEach((f) => console.log(
|
|
11500
|
-
console.log(
|
|
11501
|
+
restored.forEach((f) => console.log(import_chalk18.default.green(` \u2714 restored: ${f}`)));
|
|
11502
|
+
console.log(import_chalk18.default.bold.green(`
|
|
11501
11503
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11502
11504
|
}
|
|
11503
11505
|
});
|
|
11504
|
-
|
|
11505
|
-
(async () => {
|
|
11506
|
-
const currentDir = process.cwd();
|
|
11507
|
-
const config2 = await loadConfig(currentDir);
|
|
11508
|
-
await printWelcome(currentDir, config2);
|
|
11509
|
-
})();
|
|
11510
|
-
} else {
|
|
11511
|
-
program.parse();
|
|
11512
|
-
}
|
|
11506
|
+
program.parse();
|
|
11513
11507
|
//# sourceMappingURL=index.js.map
|