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.mjs
CHANGED
|
@@ -402,8 +402,8 @@ var init_workspace_loader = __esm({
|
|
|
402
402
|
const repos = [];
|
|
403
403
|
for (const entry of entries) {
|
|
404
404
|
const absPath = path17.join(this.workspaceRoot, entry);
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
405
|
+
const stat3 = await fs18.stat(absPath).catch(() => null);
|
|
406
|
+
if (!stat3 || !stat3.isDirectory()) continue;
|
|
407
407
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
408
408
|
if (names && !names.includes(entry)) continue;
|
|
409
409
|
const hasManifest = await fs18.pathExists(path17.join(absPath, "package.json")) || await fs18.pathExists(path17.join(absPath, "go.mod")) || await fs18.pathExists(path17.join(absPath, "Cargo.toml")) || await fs18.pathExists(path17.join(absPath, "pom.xml")) || await fs18.pathExists(path17.join(absPath, "build.gradle")) || await fs18.pathExists(path17.join(absPath, "requirements.txt")) || await fs18.pathExists(path17.join(absPath, "pyproject.toml")) || await fs18.pathExists(path17.join(absPath, "composer.json"));
|
|
@@ -467,9 +467,9 @@ var init_workspace_loader = __esm({
|
|
|
467
467
|
|
|
468
468
|
// cli/index.ts
|
|
469
469
|
import { Command } from "commander";
|
|
470
|
-
import * as
|
|
471
|
-
import * as
|
|
472
|
-
import
|
|
470
|
+
import * as path22 from "path";
|
|
471
|
+
import * as fs23 from "fs-extra";
|
|
472
|
+
import chalk18 from "chalk";
|
|
473
473
|
import * as dotenv from "dotenv";
|
|
474
474
|
import { input, confirm as confirm2, select as select3 } from "@inquirer/prompts";
|
|
475
475
|
|
|
@@ -4849,221 +4849,221 @@ function validateDsl(raw) {
|
|
|
4849
4849
|
}
|
|
4850
4850
|
return { valid: true, dsl: raw };
|
|
4851
4851
|
}
|
|
4852
|
-
function validateFeature(raw,
|
|
4852
|
+
function validateFeature(raw, path23, errors) {
|
|
4853
4853
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4854
|
-
errors.push({ path:
|
|
4854
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4855
4855
|
return;
|
|
4856
4856
|
}
|
|
4857
4857
|
const f = raw;
|
|
4858
|
-
requireNonEmptyString(f["id"], `${
|
|
4859
|
-
requireNonEmptyString(f["title"], `${
|
|
4860
|
-
requireNonEmptyString(f["description"], `${
|
|
4858
|
+
requireNonEmptyString(f["id"], `${path23}.id`, errors);
|
|
4859
|
+
requireNonEmptyString(f["title"], `${path23}.title`, errors);
|
|
4860
|
+
requireNonEmptyString(f["description"], `${path23}.description`, errors);
|
|
4861
4861
|
}
|
|
4862
|
-
function validateModel(raw,
|
|
4862
|
+
function validateModel(raw, path23, errors) {
|
|
4863
4863
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4864
|
-
errors.push({ path:
|
|
4864
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4865
4865
|
return;
|
|
4866
4866
|
}
|
|
4867
4867
|
const m = raw;
|
|
4868
|
-
requireNonEmptyString(m["name"], `${
|
|
4868
|
+
requireNonEmptyString(m["name"], `${path23}.name`, errors);
|
|
4869
4869
|
if (!Array.isArray(m["fields"])) {
|
|
4870
|
-
errors.push({ path: `${
|
|
4870
|
+
errors.push({ path: `${path23}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4871
4871
|
} else {
|
|
4872
4872
|
const fields = m["fields"];
|
|
4873
4873
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4874
|
-
errors.push({ path: `${
|
|
4874
|
+
errors.push({ path: `${path23}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4875
4875
|
}
|
|
4876
4876
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4877
|
-
validateModelField(fields[j2], `${
|
|
4877
|
+
validateModelField(fields[j2], `${path23}.fields[${j2}]`, errors);
|
|
4878
4878
|
}
|
|
4879
4879
|
}
|
|
4880
4880
|
if (m["relations"] !== void 0) {
|
|
4881
4881
|
if (!Array.isArray(m["relations"])) {
|
|
4882
|
-
errors.push({ path: `${
|
|
4882
|
+
errors.push({ path: `${path23}.relations`, message: "Must be an array of strings if present" });
|
|
4883
4883
|
} else {
|
|
4884
4884
|
const rels = m["relations"];
|
|
4885
4885
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4886
4886
|
if (typeof rels[j2] !== "string") {
|
|
4887
|
-
errors.push({ path: `${
|
|
4887
|
+
errors.push({ path: `${path23}.relations[${j2}]`, message: "Must be a string" });
|
|
4888
4888
|
}
|
|
4889
4889
|
}
|
|
4890
4890
|
}
|
|
4891
4891
|
}
|
|
4892
4892
|
}
|
|
4893
|
-
function validateModelField(raw,
|
|
4893
|
+
function validateModelField(raw, path23, errors) {
|
|
4894
4894
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4895
|
-
errors.push({ path:
|
|
4895
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4896
4896
|
return;
|
|
4897
4897
|
}
|
|
4898
4898
|
const f = raw;
|
|
4899
|
-
requireNonEmptyString(f["name"], `${
|
|
4900
|
-
requireNonEmptyString(f["type"], `${
|
|
4899
|
+
requireNonEmptyString(f["name"], `${path23}.name`, errors);
|
|
4900
|
+
requireNonEmptyString(f["type"], `${path23}.type`, errors);
|
|
4901
4901
|
if (typeof f["required"] !== "boolean") {
|
|
4902
|
-
errors.push({ path: `${
|
|
4902
|
+
errors.push({ path: `${path23}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4903
4903
|
}
|
|
4904
4904
|
}
|
|
4905
|
-
function validateEndpoint(raw,
|
|
4905
|
+
function validateEndpoint(raw, path23, errors) {
|
|
4906
4906
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4907
|
-
errors.push({ path:
|
|
4907
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4908
4908
|
return;
|
|
4909
4909
|
}
|
|
4910
4910
|
const e = raw;
|
|
4911
|
-
requireNonEmptyString(e["id"], `${
|
|
4912
|
-
requireNonEmptyString(e["description"], `${
|
|
4911
|
+
requireNonEmptyString(e["id"], `${path23}.id`, errors);
|
|
4912
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4913
4913
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4914
4914
|
errors.push({
|
|
4915
|
-
path: `${
|
|
4915
|
+
path: `${path23}.method`,
|
|
4916
4916
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4917
4917
|
});
|
|
4918
4918
|
}
|
|
4919
4919
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4920
4920
|
errors.push({
|
|
4921
|
-
path: `${
|
|
4921
|
+
path: `${path23}.path`,
|
|
4922
4922
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4923
4923
|
});
|
|
4924
4924
|
}
|
|
4925
4925
|
if (typeof e["auth"] !== "boolean") {
|
|
4926
|
-
errors.push({ path: `${
|
|
4926
|
+
errors.push({ path: `${path23}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4927
4927
|
}
|
|
4928
4928
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4929
4929
|
errors.push({
|
|
4930
|
-
path: `${
|
|
4930
|
+
path: `${path23}.successStatus`,
|
|
4931
4931
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4932
4932
|
});
|
|
4933
4933
|
}
|
|
4934
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4934
|
+
requireNonEmptyString(e["successDescription"], `${path23}.successDescription`, errors);
|
|
4935
4935
|
if (e["request"] !== void 0) {
|
|
4936
|
-
validateRequestSchema(e["request"], `${
|
|
4936
|
+
validateRequestSchema(e["request"], `${path23}.request`, errors);
|
|
4937
4937
|
}
|
|
4938
4938
|
if (e["errors"] !== void 0) {
|
|
4939
4939
|
if (!Array.isArray(e["errors"])) {
|
|
4940
|
-
errors.push({ path: `${
|
|
4940
|
+
errors.push({ path: `${path23}.errors`, message: "Must be an array if present" });
|
|
4941
4941
|
} else {
|
|
4942
4942
|
const errs = e["errors"];
|
|
4943
4943
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4944
|
-
errors.push({ path: `${
|
|
4944
|
+
errors.push({ path: `${path23}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4945
4945
|
}
|
|
4946
4946
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4947
|
-
validateResponseError(errs[j2], `${
|
|
4947
|
+
validateResponseError(errs[j2], `${path23}.errors[${j2}]`, errors);
|
|
4948
4948
|
}
|
|
4949
4949
|
}
|
|
4950
4950
|
}
|
|
4951
4951
|
}
|
|
4952
|
-
function validateRequestSchema(raw,
|
|
4952
|
+
function validateRequestSchema(raw, path23, errors) {
|
|
4953
4953
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4954
|
-
errors.push({ path:
|
|
4954
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4955
4955
|
return;
|
|
4956
4956
|
}
|
|
4957
4957
|
const r = raw;
|
|
4958
4958
|
for (const key of ["body", "query", "params"]) {
|
|
4959
4959
|
if (r[key] !== void 0) {
|
|
4960
|
-
validateFieldMap(r[key], `${
|
|
4960
|
+
validateFieldMap(r[key], `${path23}.${key}`, errors);
|
|
4961
4961
|
}
|
|
4962
4962
|
}
|
|
4963
4963
|
}
|
|
4964
|
-
function validateFieldMap(raw,
|
|
4964
|
+
function validateFieldMap(raw, path23, errors) {
|
|
4965
4965
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4966
|
-
errors.push({ path:
|
|
4966
|
+
errors.push({ path: path23, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4967
4967
|
return;
|
|
4968
4968
|
}
|
|
4969
4969
|
const map = raw;
|
|
4970
4970
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4971
4971
|
if (typeof v2 !== "string") {
|
|
4972
|
-
errors.push({ path: `${
|
|
4972
|
+
errors.push({ path: `${path23}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4973
4973
|
}
|
|
4974
4974
|
}
|
|
4975
4975
|
}
|
|
4976
|
-
function validateResponseError(raw,
|
|
4976
|
+
function validateResponseError(raw, path23, errors) {
|
|
4977
4977
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4978
|
-
errors.push({ path:
|
|
4978
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4979
4979
|
return;
|
|
4980
4980
|
}
|
|
4981
4981
|
const e = raw;
|
|
4982
4982
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
4983
|
-
errors.push({ path: `${
|
|
4983
|
+
errors.push({ path: `${path23}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
4984
4984
|
}
|
|
4985
|
-
requireNonEmptyString(e["code"], `${
|
|
4986
|
-
requireNonEmptyString(e["description"], `${
|
|
4985
|
+
requireNonEmptyString(e["code"], `${path23}.code`, errors);
|
|
4986
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4987
4987
|
}
|
|
4988
|
-
function validateBehavior(raw,
|
|
4988
|
+
function validateBehavior(raw, path23, errors) {
|
|
4989
4989
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4990
|
-
errors.push({ path:
|
|
4990
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4991
4991
|
return;
|
|
4992
4992
|
}
|
|
4993
4993
|
const b = raw;
|
|
4994
|
-
requireNonEmptyString(b["id"], `${
|
|
4995
|
-
requireNonEmptyString(b["description"], `${
|
|
4994
|
+
requireNonEmptyString(b["id"], `${path23}.id`, errors);
|
|
4995
|
+
requireNonEmptyString(b["description"], `${path23}.description`, errors);
|
|
4996
4996
|
if (b["constraints"] !== void 0) {
|
|
4997
4997
|
if (!Array.isArray(b["constraints"])) {
|
|
4998
|
-
errors.push({ path: `${
|
|
4998
|
+
errors.push({ path: `${path23}.constraints`, message: "Must be an array of strings if present" });
|
|
4999
4999
|
} else {
|
|
5000
5000
|
const cs2 = b["constraints"];
|
|
5001
5001
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5002
5002
|
if (typeof cs2[j2] !== "string") {
|
|
5003
|
-
errors.push({ path: `${
|
|
5003
|
+
errors.push({ path: `${path23}.constraints[${j2}]`, message: "Must be a string" });
|
|
5004
5004
|
}
|
|
5005
5005
|
}
|
|
5006
5006
|
}
|
|
5007
5007
|
}
|
|
5008
5008
|
}
|
|
5009
|
-
function validateComponent(raw,
|
|
5009
|
+
function validateComponent(raw, path23, errors) {
|
|
5010
5010
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5011
|
-
errors.push({ path:
|
|
5011
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5012
5012
|
return;
|
|
5013
5013
|
}
|
|
5014
5014
|
const c = raw;
|
|
5015
|
-
requireNonEmptyString(c["id"], `${
|
|
5016
|
-
requireNonEmptyString(c["name"], `${
|
|
5017
|
-
requireNonEmptyString(c["description"], `${
|
|
5015
|
+
requireNonEmptyString(c["id"], `${path23}.id`, errors);
|
|
5016
|
+
requireNonEmptyString(c["name"], `${path23}.name`, errors);
|
|
5017
|
+
requireNonEmptyString(c["description"], `${path23}.description`, errors);
|
|
5018
5018
|
if (c["props"] !== void 0) {
|
|
5019
5019
|
if (!Array.isArray(c["props"])) {
|
|
5020
|
-
errors.push({ path: `${
|
|
5020
|
+
errors.push({ path: `${path23}.props`, message: "Must be an array if present" });
|
|
5021
5021
|
} else {
|
|
5022
5022
|
const props = c["props"];
|
|
5023
5023
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5024
5024
|
const p = props[j2];
|
|
5025
5025
|
if (typeof p !== "object" || p === null) {
|
|
5026
|
-
errors.push({ path: `${
|
|
5026
|
+
errors.push({ path: `${path23}.props[${j2}]`, message: "Must be an object" });
|
|
5027
5027
|
continue;
|
|
5028
5028
|
}
|
|
5029
|
-
requireNonEmptyString(p["name"], `${
|
|
5030
|
-
requireNonEmptyString(p["type"], `${
|
|
5029
|
+
requireNonEmptyString(p["name"], `${path23}.props[${j2}].name`, errors);
|
|
5030
|
+
requireNonEmptyString(p["type"], `${path23}.props[${j2}].type`, errors);
|
|
5031
5031
|
if (typeof p["required"] !== "boolean") {
|
|
5032
|
-
errors.push({ path: `${
|
|
5032
|
+
errors.push({ path: `${path23}.props[${j2}].required`, message: "Must be boolean" });
|
|
5033
5033
|
}
|
|
5034
5034
|
}
|
|
5035
5035
|
}
|
|
5036
5036
|
}
|
|
5037
5037
|
if (c["events"] !== void 0) {
|
|
5038
5038
|
if (!Array.isArray(c["events"])) {
|
|
5039
|
-
errors.push({ path: `${
|
|
5039
|
+
errors.push({ path: `${path23}.events`, message: "Must be an array if present" });
|
|
5040
5040
|
} else {
|
|
5041
5041
|
const events = c["events"];
|
|
5042
5042
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5043
5043
|
const e = events[j2];
|
|
5044
5044
|
if (typeof e !== "object" || e === null) {
|
|
5045
|
-
errors.push({ path: `${
|
|
5045
|
+
errors.push({ path: `${path23}.events[${j2}]`, message: "Must be an object" });
|
|
5046
5046
|
continue;
|
|
5047
5047
|
}
|
|
5048
|
-
requireNonEmptyString(e["name"], `${
|
|
5048
|
+
requireNonEmptyString(e["name"], `${path23}.events[${j2}].name`, errors);
|
|
5049
5049
|
}
|
|
5050
5050
|
}
|
|
5051
5051
|
}
|
|
5052
5052
|
if (c["state"] !== void 0) {
|
|
5053
5053
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5054
|
-
errors.push({ path: `${
|
|
5054
|
+
errors.push({ path: `${path23}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5055
5055
|
}
|
|
5056
5056
|
}
|
|
5057
5057
|
if (c["apiCalls"] !== void 0) {
|
|
5058
5058
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5059
|
-
errors.push({ path: `${
|
|
5059
|
+
errors.push({ path: `${path23}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5060
5060
|
}
|
|
5061
5061
|
}
|
|
5062
5062
|
}
|
|
5063
|
-
function requireNonEmptyString(v2,
|
|
5063
|
+
function requireNonEmptyString(v2, path23, errors) {
|
|
5064
5064
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5065
5065
|
errors.push({
|
|
5066
|
-
path:
|
|
5066
|
+
path: path23,
|
|
5067
5067
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5068
5068
|
});
|
|
5069
5069
|
}
|
|
@@ -5497,6 +5497,41 @@ async function loadDslForSpec(specFilePath) {
|
|
|
5497
5497
|
// core/frontend-context-loader.ts
|
|
5498
5498
|
import * as fs7 from "fs-extra";
|
|
5499
5499
|
import * as path6 from "path";
|
|
5500
|
+
function parseImportStatements(content) {
|
|
5501
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, (m) => "\n".repeat(m.split("\n").length - 1));
|
|
5502
|
+
const collapsed = stripped.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
5503
|
+
const results = [];
|
|
5504
|
+
for (const rawLine of collapsed.split("\n")) {
|
|
5505
|
+
const line = rawLine.trim();
|
|
5506
|
+
if (!line) continue;
|
|
5507
|
+
if (/^import\s+type\b/.test(line)) continue;
|
|
5508
|
+
if (!line.startsWith("import")) continue;
|
|
5509
|
+
const match = line.match(/^(import\s+([\s\S]+?)\s+from\s+['"]([^'"]+)['"])/);
|
|
5510
|
+
if (!match) continue;
|
|
5511
|
+
results.push({
|
|
5512
|
+
line: match[1],
|
|
5513
|
+
modulePath: match[3],
|
|
5514
|
+
specifiers: match[2]
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
return results;
|
|
5518
|
+
}
|
|
5519
|
+
var HTTP_MODULE_PATTERNS = [
|
|
5520
|
+
// Project path aliases (@/, @@/, ~/, #/) — catches '@/utils/request', '~/lib/http', etc.
|
|
5521
|
+
/^(?:@{1,2}|~|#)[/\\]/,
|
|
5522
|
+
// Well-known HTTP libraries (exact name match)
|
|
5523
|
+
/^(?:axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)$/,
|
|
5524
|
+
// Relative imports whose path contains an HTTP-utility keyword
|
|
5525
|
+
/\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*/
|
|
5526
|
+
];
|
|
5527
|
+
function findHttpClientImport(content) {
|
|
5528
|
+
for (const stmt of parseImportStatements(content)) {
|
|
5529
|
+
if (HTTP_MODULE_PATTERNS.some((p) => p.test(stmt.modulePath))) {
|
|
5530
|
+
return stmt.line;
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
return void 0;
|
|
5534
|
+
}
|
|
5500
5535
|
var STATE_MANAGEMENT_LIBS = [
|
|
5501
5536
|
"zustand",
|
|
5502
5537
|
"redux",
|
|
@@ -5685,13 +5720,12 @@ ${preview}`);
|
|
|
5685
5720
|
} catch {
|
|
5686
5721
|
}
|
|
5687
5722
|
}
|
|
5688
|
-
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;
|
|
5689
5723
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
5690
5724
|
try {
|
|
5691
5725
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5692
|
-
const
|
|
5693
|
-
if (
|
|
5694
|
-
ctx.httpClientImport =
|
|
5726
|
+
const found = findHttpClientImport(content);
|
|
5727
|
+
if (found) {
|
|
5728
|
+
ctx.httpClientImport = found;
|
|
5695
5729
|
break;
|
|
5696
5730
|
}
|
|
5697
5731
|
} catch {
|
|
@@ -5821,13 +5855,28 @@ async function extractRouteModuleContext(projectRoot, ctx) {
|
|
|
5821
5855
|
moduleFiles.push(...files);
|
|
5822
5856
|
}
|
|
5823
5857
|
if (moduleFiles.length === 0) return;
|
|
5824
|
-
const
|
|
5858
|
+
const dynamicLayoutRegex = /const\s+Layout\s*=\s*(?:defineAsyncComponent\s*\(\s*)?(?:\(\s*\))?\s*(?:=>|function[^(]*\()\s*(?:[^)]*\))?\s*(?:=>)?\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
5825
5859
|
for (const relPath of moduleFiles) {
|
|
5826
5860
|
try {
|
|
5827
5861
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5828
|
-
const
|
|
5829
|
-
|
|
5830
|
-
|
|
5862
|
+
const stmts = parseImportStatements(content);
|
|
5863
|
+
const staticLayout = stmts.find(
|
|
5864
|
+
(s) => /\bLayout\b/.test(s.specifiers) && /layout/i.test(s.modulePath)
|
|
5865
|
+
);
|
|
5866
|
+
if (staticLayout) {
|
|
5867
|
+
ctx.layoutImport = staticLayout.line;
|
|
5868
|
+
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5869
|
+
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5870
|
+
break;
|
|
5871
|
+
}
|
|
5872
|
+
const singleLine = content.replace(
|
|
5873
|
+
/const\s+Layout\s*=[\s\S]*?import\s*\([^)]+\)/gm,
|
|
5874
|
+
(m) => m.replace(/\n\s*/g, " ")
|
|
5875
|
+
);
|
|
5876
|
+
const dynMatch = singleLine.match(dynamicLayoutRegex);
|
|
5877
|
+
if (dynMatch) {
|
|
5878
|
+
const constMatch = content.match(/^const\s+Layout\s*=.+/m);
|
|
5879
|
+
ctx.layoutImport = constMatch ? constMatch[0].trim() : dynMatch[0].trim();
|
|
5831
5880
|
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5832
5881
|
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5833
5882
|
break;
|
|
@@ -7812,6 +7861,60 @@ function parseErrors(output, source) {
|
|
|
7812
7861
|
}
|
|
7813
7862
|
return errors;
|
|
7814
7863
|
}
|
|
7864
|
+
function parseRelativeImports(content, fromFileRel) {
|
|
7865
|
+
const relDir = path15.dirname(fromFileRel);
|
|
7866
|
+
const results = [];
|
|
7867
|
+
const normalized = content.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
7868
|
+
for (const line of normalized.split("\n")) {
|
|
7869
|
+
const trimmed = line.trim();
|
|
7870
|
+
if (/^import\s+type\b/.test(trimmed)) continue;
|
|
7871
|
+
const match = trimmed.match(/^import\b[^'"]*from\s+['"](\.\.?\/[^'"]+)['"]/);
|
|
7872
|
+
if (!match) continue;
|
|
7873
|
+
const resolved = path15.normalize(path15.join(relDir, match[1]));
|
|
7874
|
+
results.push(resolved);
|
|
7875
|
+
}
|
|
7876
|
+
return results;
|
|
7877
|
+
}
|
|
7878
|
+
async function buildRepairOrder(errorsByFile, workingDir) {
|
|
7879
|
+
const files = Array.from(errorsByFile.keys());
|
|
7880
|
+
if (files.length <= 1) return Array.from(errorsByFile.entries());
|
|
7881
|
+
const deps = new Map(files.map((f) => [f, []]));
|
|
7882
|
+
for (const file of files) {
|
|
7883
|
+
try {
|
|
7884
|
+
const content = await fs16.readFile(path15.join(workingDir, file), "utf-8");
|
|
7885
|
+
const importedPaths = parseRelativeImports(content, file);
|
|
7886
|
+
for (const importedPath of importedPaths) {
|
|
7887
|
+
const matched = files.find((f) => {
|
|
7888
|
+
if (f === file) return false;
|
|
7889
|
+
const fNoExt = f.replace(/\.[^.]+$/, "");
|
|
7890
|
+
return importedPath === fNoExt || importedPath === f || `${importedPath}.ts` === f || `${importedPath}.tsx` === f || `${importedPath}.js` === f || `${importedPath}.jsx` === f;
|
|
7891
|
+
});
|
|
7892
|
+
if (matched) deps.get(file).push(matched);
|
|
7893
|
+
}
|
|
7894
|
+
} catch {
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7897
|
+
const dependents = new Map(files.map((f) => [f, []]));
|
|
7898
|
+
for (const [file, fileDeps] of deps) {
|
|
7899
|
+
for (const dep of fileDeps) dependents.get(dep).push(file);
|
|
7900
|
+
}
|
|
7901
|
+
const inDegree = new Map(files.map((f) => [f, deps.get(f).length]));
|
|
7902
|
+
const queue = files.filter((f) => inDegree.get(f) === 0);
|
|
7903
|
+
const sorted = [];
|
|
7904
|
+
while (queue.length > 0) {
|
|
7905
|
+
const file = queue.shift();
|
|
7906
|
+
sorted.push(file);
|
|
7907
|
+
for (const dependent of dependents.get(file) ?? []) {
|
|
7908
|
+
const degree = (inDegree.get(dependent) ?? 1) - 1;
|
|
7909
|
+
inDegree.set(dependent, degree);
|
|
7910
|
+
if (degree === 0) queue.push(dependent);
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
for (const f of files) {
|
|
7914
|
+
if (!sorted.includes(f)) sorted.push(f);
|
|
7915
|
+
}
|
|
7916
|
+
return sorted.map((f) => [f, errorsByFile.get(f)]);
|
|
7917
|
+
}
|
|
7815
7918
|
async function attemptFix(provider, errors, workingDir, dsl) {
|
|
7816
7919
|
const results = [];
|
|
7817
7920
|
const errorsByFile = /* @__PURE__ */ new Map();
|
|
@@ -7820,7 +7923,8 @@ async function attemptFix(provider, errors, workingDir, dsl) {
|
|
|
7820
7923
|
if (!errorsByFile.has(file)) errorsByFile.set(file, []);
|
|
7821
7924
|
errorsByFile.get(file).push(err);
|
|
7822
7925
|
}
|
|
7823
|
-
|
|
7926
|
+
const sortedEntries = await buildRepairOrder(errorsByFile, workingDir);
|
|
7927
|
+
for (const [file, fileErrors] of sortedEntries) {
|
|
7824
7928
|
const fullPath = path15.join(workingDir, file);
|
|
7825
7929
|
let existingContent = "";
|
|
7826
7930
|
try {
|
|
@@ -8214,108 +8318,6 @@ async function clearKey(provider) {
|
|
|
8214
8318
|
await writeStore(store);
|
|
8215
8319
|
}
|
|
8216
8320
|
|
|
8217
|
-
// cli/welcome.ts
|
|
8218
|
-
import chalk17 from "chalk";
|
|
8219
|
-
import * as os4 from "os";
|
|
8220
|
-
import * as path19 from "path";
|
|
8221
|
-
import * as fs20 from "fs-extra";
|
|
8222
|
-
var VERSION = "0.14.1";
|
|
8223
|
-
var TOTAL_W = 76;
|
|
8224
|
-
var L_WIDTH = 44;
|
|
8225
|
-
var R_WIDTH = TOTAL_W - L_WIDTH - 4;
|
|
8226
|
-
var ROBOT_COLOR = chalk17.hex("#E8885A");
|
|
8227
|
-
var ROBOT = [
|
|
8228
|
-
ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
|
|
8229
|
-
ROBOT_COLOR(" \u2502") + chalk17.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + chalk17.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
|
|
8230
|
-
ROBOT_COLOR(" \u2502") + chalk17.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
|
|
8231
|
-
ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
|
|
8232
|
-
ROBOT_COLOR(" \u2502"),
|
|
8233
|
-
ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
|
|
8234
|
-
];
|
|
8235
|
-
function visLen(s) {
|
|
8236
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
8237
|
-
}
|
|
8238
|
-
function padR(s, width) {
|
|
8239
|
-
const vl = visLen(s);
|
|
8240
|
-
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
8241
|
-
}
|
|
8242
|
-
function row(left, right) {
|
|
8243
|
-
return padR(left, L_WIDTH) + " " + chalk17.gray("\u2502") + " " + padR(right, R_WIDTH);
|
|
8244
|
-
}
|
|
8245
|
-
function center(s, width) {
|
|
8246
|
-
const vl = visLen(s);
|
|
8247
|
-
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
8248
|
-
return " ".repeat(pad) + s;
|
|
8249
|
-
}
|
|
8250
|
-
async function getRecentSpecs(dir) {
|
|
8251
|
-
const specsDir = path19.join(dir, "specs");
|
|
8252
|
-
if (!await fs20.pathExists(specsDir)) return [];
|
|
8253
|
-
try {
|
|
8254
|
-
const files = await fs20.readdir(specsDir);
|
|
8255
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
8256
|
-
const withStats = await Promise.all(
|
|
8257
|
-
mdFiles.map(async (f) => {
|
|
8258
|
-
const stat4 = await fs20.stat(path19.join(specsDir, f));
|
|
8259
|
-
return { name: f, mtime: stat4.mtime.getTime() };
|
|
8260
|
-
})
|
|
8261
|
-
);
|
|
8262
|
-
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
8263
|
-
return withStats.slice(0, 3).map(({ name, mtime }) => {
|
|
8264
|
-
const ms2 = Date.now() - mtime;
|
|
8265
|
-
const hours = Math.floor(ms2 / 36e5);
|
|
8266
|
-
const days = Math.floor(hours / 24);
|
|
8267
|
-
const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
|
|
8268
|
-
const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
|
|
8269
|
-
return chalk17.white(slug) + chalk17.dim(" " + age);
|
|
8270
|
-
});
|
|
8271
|
-
} catch {
|
|
8272
|
-
return [];
|
|
8273
|
-
}
|
|
8274
|
-
}
|
|
8275
|
-
async function printWelcome(currentDir, config2) {
|
|
8276
|
-
const username = os4.userInfo().username;
|
|
8277
|
-
const homeDir = os4.homedir();
|
|
8278
|
-
const shortDir = currentDir.startsWith(homeDir) ? "~" + currentDir.slice(homeDir.length) : currentDir;
|
|
8279
|
-
const recentSpecs = await getRecentSpecs(currentDir);
|
|
8280
|
-
const providerBit = config2?.provider ? config2.provider + (config2.model ? " \xB7 " + config2.model : "") : "";
|
|
8281
|
-
const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
|
|
8282
|
-
const maxInfoLen = L_WIDTH - 2;
|
|
8283
|
-
const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
|
|
8284
|
-
const bottomLine = " " + chalk17.dim(bottomTruncated);
|
|
8285
|
-
const titleInner = `ai-spec v${VERSION} `;
|
|
8286
|
-
const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
|
|
8287
|
-
console.log(
|
|
8288
|
-
"\n" + chalk17.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
|
|
8289
|
-
);
|
|
8290
|
-
const welcomeText = "Welcome back, " + chalk17.bold.white(username) + "!";
|
|
8291
|
-
const leftLines = [
|
|
8292
|
-
"",
|
|
8293
|
-
center(welcomeText, L_WIDTH),
|
|
8294
|
-
"",
|
|
8295
|
-
...ROBOT,
|
|
8296
|
-
"",
|
|
8297
|
-
bottomLine,
|
|
8298
|
-
""
|
|
8299
|
-
];
|
|
8300
|
-
const rightLines = [
|
|
8301
|
-
chalk17.hex("#FF8C00").bold("Tips for getting started"),
|
|
8302
|
-
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8303
|
-
chalk17.white('ai-spec create "feature"'),
|
|
8304
|
-
chalk17.gray("ai-spec workspace run"),
|
|
8305
|
-
chalk17.gray('ai-spec update "change"'),
|
|
8306
|
-
"",
|
|
8307
|
-
chalk17.hex("#FF8C00").bold("Recent activity"),
|
|
8308
|
-
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8309
|
-
...recentSpecs.length > 0 ? recentSpecs : [chalk17.dim("No recent activity")]
|
|
8310
|
-
];
|
|
8311
|
-
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
8312
|
-
for (let i = 0; i < maxLines; i++) {
|
|
8313
|
-
console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
|
|
8314
|
-
}
|
|
8315
|
-
console.log(chalk17.gray("\u2500".repeat(TOTAL_W)));
|
|
8316
|
-
console.log();
|
|
8317
|
-
}
|
|
8318
|
-
|
|
8319
8321
|
// prompts/global-constitution.prompt.ts
|
|
8320
8322
|
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.
|
|
8321
8323
|
|
|
@@ -8825,8 +8827,8 @@ Existing API/service files:`);
|
|
|
8825
8827
|
}
|
|
8826
8828
|
|
|
8827
8829
|
// core/mock-server-generator.ts
|
|
8828
|
-
import * as
|
|
8829
|
-
import * as
|
|
8830
|
+
import * as path19 from "path";
|
|
8831
|
+
import * as fs20 from "fs-extra";
|
|
8830
8832
|
import { spawn } from "child_process";
|
|
8831
8833
|
function typeToFixture(fieldName, typeDesc) {
|
|
8832
8834
|
const t = typeDesc.toLowerCase();
|
|
@@ -8962,22 +8964,22 @@ function generateMockServerJs(dsl, port) {
|
|
|
8962
8964
|
}
|
|
8963
8965
|
function detectFrontendFramework(projectDir) {
|
|
8964
8966
|
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
8965
|
-
if (
|
|
8967
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "vite";
|
|
8966
8968
|
}
|
|
8967
8969
|
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
8968
|
-
if (
|
|
8970
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "next";
|
|
8969
8971
|
}
|
|
8970
|
-
const pkgPath =
|
|
8971
|
-
if (
|
|
8972
|
+
const pkgPath = path19.join(projectDir, "package.json");
|
|
8973
|
+
if (fs20.existsSync(pkgPath)) {
|
|
8972
8974
|
try {
|
|
8973
|
-
const pkg = JSON.parse(
|
|
8975
|
+
const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
|
|
8974
8976
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
8975
8977
|
if (deps["react-scripts"]) return "cra";
|
|
8976
8978
|
} catch {
|
|
8977
8979
|
}
|
|
8978
8980
|
}
|
|
8979
8981
|
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
8980
|
-
if (
|
|
8982
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "webpack";
|
|
8981
8983
|
}
|
|
8982
8984
|
return "unknown";
|
|
8983
8985
|
}
|
|
@@ -9157,7 +9159,7 @@ export const worker = setupWorker(...handlers);
|
|
|
9157
9159
|
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
9158
9160
|
function findViteConfigFile(projectDir) {
|
|
9159
9161
|
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
9160
|
-
if (
|
|
9162
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return f;
|
|
9161
9163
|
}
|
|
9162
9164
|
return null;
|
|
9163
9165
|
}
|
|
@@ -9203,73 +9205,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
|
9203
9205
|
if (framework === "vite") {
|
|
9204
9206
|
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
9205
9207
|
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
9206
|
-
const mockConfigPath =
|
|
9207
|
-
await
|
|
9208
|
+
const mockConfigPath = path19.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
9209
|
+
await fs20.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
9208
9210
|
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
9209
|
-
const pkgPath =
|
|
9210
|
-
if (await
|
|
9211
|
-
const pkg = await
|
|
9211
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9212
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9213
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9212
9214
|
pkg.scripts = pkg.scripts ?? {};
|
|
9213
9215
|
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
9214
9216
|
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
9215
|
-
await
|
|
9217
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9216
9218
|
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
9217
9219
|
}
|
|
9218
9220
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9219
|
-
await
|
|
9221
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9220
9222
|
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
9221
9223
|
}
|
|
9222
9224
|
if (framework === "cra") {
|
|
9223
|
-
const pkgPath =
|
|
9224
|
-
if (await
|
|
9225
|
-
const pkg = await
|
|
9225
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9226
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9227
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9226
9228
|
const originalProxy = pkg.proxy ?? null;
|
|
9227
9229
|
pkg.proxy = `http://localhost:${mockPort}`;
|
|
9228
|
-
await
|
|
9230
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9229
9231
|
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
9230
9232
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9231
|
-
await
|
|
9233
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9232
9234
|
return { framework, applied: true, devCommand: "npm start" };
|
|
9233
9235
|
}
|
|
9234
9236
|
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
9235
9237
|
}
|
|
9236
9238
|
const lock = { framework, mockPort, frontendDir, actions };
|
|
9237
|
-
await
|
|
9239
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
9238
9240
|
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}`;
|
|
9239
9241
|
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
9240
9242
|
}
|
|
9241
9243
|
async function restoreMockProxy(frontendDir) {
|
|
9242
|
-
const lockPath =
|
|
9243
|
-
if (!await
|
|
9244
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9245
|
+
if (!await fs20.pathExists(lockPath)) {
|
|
9244
9246
|
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
9245
9247
|
}
|
|
9246
|
-
const lock = await
|
|
9248
|
+
const lock = await fs20.readJson(lockPath);
|
|
9247
9249
|
for (const action of lock.actions) {
|
|
9248
9250
|
if (action.type === "wrote-file") {
|
|
9249
|
-
const fp =
|
|
9250
|
-
if (await
|
|
9251
|
+
const fp = path19.join(frontendDir, action.filePath);
|
|
9252
|
+
if (await fs20.pathExists(fp)) await fs20.remove(fp);
|
|
9251
9253
|
} else if (action.type === "added-pkg-script") {
|
|
9252
|
-
const pkgPath =
|
|
9253
|
-
if (await
|
|
9254
|
-
const pkg = await
|
|
9254
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9255
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9256
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9255
9257
|
if (action.originalValue == null) {
|
|
9256
9258
|
delete pkg.scripts?.[action.key];
|
|
9257
9259
|
} else {
|
|
9258
9260
|
pkg.scripts = pkg.scripts ?? {};
|
|
9259
9261
|
pkg.scripts[action.key] = action.originalValue;
|
|
9260
9262
|
}
|
|
9261
|
-
await
|
|
9263
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9262
9264
|
}
|
|
9263
9265
|
} else if (action.type === "patched-pkg-proxy") {
|
|
9264
|
-
const pkgPath =
|
|
9265
|
-
if (await
|
|
9266
|
-
const pkg = await
|
|
9266
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9267
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9268
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9267
9269
|
if (action.originalProxy == null) {
|
|
9268
9270
|
delete pkg.proxy;
|
|
9269
9271
|
} else {
|
|
9270
9272
|
pkg.proxy = action.originalProxy;
|
|
9271
9273
|
}
|
|
9272
|
-
await
|
|
9274
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9273
9275
|
}
|
|
9274
9276
|
}
|
|
9275
9277
|
}
|
|
@@ -9279,7 +9281,7 @@ async function restoreMockProxy(frontendDir) {
|
|
|
9279
9281
|
} catch {
|
|
9280
9282
|
}
|
|
9281
9283
|
}
|
|
9282
|
-
await
|
|
9284
|
+
await fs20.remove(lockPath);
|
|
9283
9285
|
return { restored: true };
|
|
9284
9286
|
}
|
|
9285
9287
|
function startMockServerBackground(serverJsPath, port) {
|
|
@@ -9292,23 +9294,23 @@ function startMockServerBackground(serverJsPath, port) {
|
|
|
9292
9294
|
return child.pid;
|
|
9293
9295
|
}
|
|
9294
9296
|
async function saveMockServerPid(frontendDir, pid) {
|
|
9295
|
-
const lockPath =
|
|
9296
|
-
if (await
|
|
9297
|
-
const lock = await
|
|
9297
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9298
|
+
if (await fs20.pathExists(lockPath)) {
|
|
9299
|
+
const lock = await fs20.readJson(lockPath);
|
|
9298
9300
|
lock.mockServerPid = pid;
|
|
9299
|
-
await
|
|
9301
|
+
await fs20.writeJson(lockPath, lock, { spaces: 2 });
|
|
9300
9302
|
}
|
|
9301
9303
|
}
|
|
9302
9304
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
9303
9305
|
const port = opts.port ?? 3001;
|
|
9304
|
-
const outputDir =
|
|
9306
|
+
const outputDir = path19.join(projectDir, opts.outputDir ?? "mock");
|
|
9305
9307
|
const result = { files: [] };
|
|
9306
|
-
await
|
|
9308
|
+
await fs20.ensureDir(outputDir);
|
|
9307
9309
|
const serverJs = generateMockServerJs(dsl, port);
|
|
9308
|
-
const serverPath =
|
|
9309
|
-
await
|
|
9310
|
+
const serverPath = path19.join(outputDir, "server.js");
|
|
9311
|
+
await fs20.writeFile(serverPath, serverJs, "utf-8");
|
|
9310
9312
|
result.files.push({
|
|
9311
|
-
path:
|
|
9313
|
+
path: path19.relative(projectDir, serverPath),
|
|
9312
9314
|
description: `Express mock server \u2014 run with: node mock/server.js`
|
|
9313
9315
|
});
|
|
9314
9316
|
const mockReadme = `# Mock Server
|
|
@@ -9338,52 +9340,52 @@ ${dsl.endpoints.map(
|
|
|
9338
9340
|
|
|
9339
9341
|
Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
|
|
9340
9342
|
`;
|
|
9341
|
-
const readmePath =
|
|
9342
|
-
await
|
|
9343
|
+
const readmePath = path19.join(outputDir, "README.md");
|
|
9344
|
+
await fs20.writeFile(readmePath, mockReadme, "utf-8");
|
|
9343
9345
|
result.files.push({
|
|
9344
|
-
path:
|
|
9346
|
+
path: path19.relative(projectDir, readmePath),
|
|
9345
9347
|
description: "Mock server usage guide"
|
|
9346
9348
|
});
|
|
9347
9349
|
if (opts.proxy) {
|
|
9348
9350
|
const { content, filename } = generateProxyConfig(dsl, port, projectDir);
|
|
9349
|
-
const proxyPath =
|
|
9350
|
-
await
|
|
9351
|
-
await
|
|
9351
|
+
const proxyPath = path19.join(projectDir, filename);
|
|
9352
|
+
await fs20.ensureDir(path19.dirname(proxyPath));
|
|
9353
|
+
await fs20.writeFile(proxyPath, content, "utf-8");
|
|
9352
9354
|
result.files.push({
|
|
9353
9355
|
path: filename,
|
|
9354
9356
|
description: "Proxy config snippet \u2014 copy instructions into your framework config"
|
|
9355
9357
|
});
|
|
9356
9358
|
}
|
|
9357
9359
|
if (opts.msw) {
|
|
9358
|
-
const mswDir =
|
|
9359
|
-
await
|
|
9360
|
+
const mswDir = path19.join(projectDir, "src", "mocks");
|
|
9361
|
+
await fs20.ensureDir(mswDir);
|
|
9360
9362
|
const handlersContent = generateMswHandlers(dsl);
|
|
9361
|
-
const handlersPath =
|
|
9362
|
-
await
|
|
9363
|
+
const handlersPath = path19.join(mswDir, "handlers.ts");
|
|
9364
|
+
await fs20.writeFile(handlersPath, handlersContent, "utf-8");
|
|
9363
9365
|
result.files.push({
|
|
9364
|
-
path:
|
|
9366
|
+
path: path19.relative(projectDir, handlersPath),
|
|
9365
9367
|
description: "MSW request handlers"
|
|
9366
9368
|
});
|
|
9367
9369
|
const browserContent = generateMswBrowser();
|
|
9368
|
-
const browserPath =
|
|
9369
|
-
await
|
|
9370
|
+
const browserPath = path19.join(mswDir, "browser.ts");
|
|
9371
|
+
await fs20.writeFile(browserPath, browserContent, "utf-8");
|
|
9370
9372
|
result.files.push({
|
|
9371
|
-
path:
|
|
9373
|
+
path: path19.relative(projectDir, browserPath),
|
|
9372
9374
|
description: "MSW browser worker setup"
|
|
9373
9375
|
});
|
|
9374
9376
|
}
|
|
9375
9377
|
return result;
|
|
9376
9378
|
}
|
|
9377
9379
|
async function findLatestDslFile(projectDir) {
|
|
9378
|
-
const specDir =
|
|
9379
|
-
if (!await
|
|
9380
|
+
const specDir = path19.join(projectDir, ".ai-spec");
|
|
9381
|
+
if (!await fs20.pathExists(specDir)) return null;
|
|
9380
9382
|
const allFiles = [];
|
|
9381
9383
|
async function scan(dir) {
|
|
9382
|
-
const entries = await
|
|
9384
|
+
const entries = await fs20.readdir(dir);
|
|
9383
9385
|
for (const entry of entries) {
|
|
9384
|
-
const abs =
|
|
9385
|
-
const
|
|
9386
|
-
if (
|
|
9386
|
+
const abs = path19.join(dir, entry);
|
|
9387
|
+
const stat3 = await fs20.stat(abs);
|
|
9388
|
+
if (stat3.isDirectory()) {
|
|
9387
9389
|
await scan(abs);
|
|
9388
9390
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9389
9391
|
allFiles.push(abs);
|
|
@@ -9393,16 +9395,16 @@ async function findLatestDslFile(projectDir) {
|
|
|
9393
9395
|
await scan(specDir);
|
|
9394
9396
|
if (allFiles.length === 0) return null;
|
|
9395
9397
|
const withMtimes = await Promise.all(
|
|
9396
|
-
allFiles.map(async (f) => ({ f, mtime: (await
|
|
9398
|
+
allFiles.map(async (f) => ({ f, mtime: (await fs20.stat(f)).mtime }))
|
|
9397
9399
|
);
|
|
9398
9400
|
withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
9399
9401
|
return withMtimes[0].f;
|
|
9400
9402
|
}
|
|
9401
9403
|
|
|
9402
9404
|
// core/spec-updater.ts
|
|
9403
|
-
import
|
|
9404
|
-
import * as
|
|
9405
|
-
import * as
|
|
9405
|
+
import chalk17 from "chalk";
|
|
9406
|
+
import * as path20 from "path";
|
|
9407
|
+
import * as fs21 from "fs-extra";
|
|
9406
9408
|
|
|
9407
9409
|
// prompts/update.prompt.ts
|
|
9408
9410
|
var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
|
|
@@ -9548,8 +9550,8 @@ var SpecUpdater = class {
|
|
|
9548
9550
|
* Returns all .md spec files sorted newest-first.
|
|
9549
9551
|
*/
|
|
9550
9552
|
static async findLatestSpec(specsDir) {
|
|
9551
|
-
if (!await
|
|
9552
|
-
const files = await
|
|
9553
|
+
if (!await fs21.pathExists(specsDir)) return null;
|
|
9554
|
+
const files = await fs21.readdir(specsDir);
|
|
9553
9555
|
const pattern = /^feature-(.+)-v(\d+)\.md$/;
|
|
9554
9556
|
let latest = null;
|
|
9555
9557
|
for (const file of files) {
|
|
@@ -9557,8 +9559,8 @@ var SpecUpdater = class {
|
|
|
9557
9559
|
if (!m) continue;
|
|
9558
9560
|
const version = parseInt(m[2], 10);
|
|
9559
9561
|
if (!latest || version > latest.version) {
|
|
9560
|
-
const filePath =
|
|
9561
|
-
const content = await
|
|
9562
|
+
const filePath = path20.join(specsDir, file);
|
|
9563
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
9562
9564
|
latest = { filePath, version, slug: m[1], content };
|
|
9563
9565
|
}
|
|
9564
9566
|
}
|
|
@@ -9569,16 +9571,16 @@ var SpecUpdater = class {
|
|
|
9569
9571
|
* Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
|
|
9570
9572
|
*/
|
|
9571
9573
|
async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
|
|
9572
|
-
const existingSpec = await
|
|
9574
|
+
const existingSpec = await fs21.readFile(existingSpecPath, "utf-8");
|
|
9573
9575
|
let existingDsl = null;
|
|
9574
9576
|
const dslFile = await findLatestDslFile(projectDir);
|
|
9575
9577
|
if (dslFile) {
|
|
9576
9578
|
try {
|
|
9577
|
-
existingDsl = await
|
|
9579
|
+
existingDsl = await fs21.readJson(dslFile);
|
|
9578
9580
|
} catch {
|
|
9579
9581
|
}
|
|
9580
9582
|
}
|
|
9581
|
-
console.log(
|
|
9583
|
+
console.log(chalk17.blue(" [1/3] Generating updated spec..."));
|
|
9582
9584
|
const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
|
|
9583
9585
|
let updatedSpecContent;
|
|
9584
9586
|
try {
|
|
@@ -9587,15 +9589,15 @@ var SpecUpdater = class {
|
|
|
9587
9589
|
} catch (err) {
|
|
9588
9590
|
throw new Error(`Spec update generation failed: ${err.message}`);
|
|
9589
9591
|
}
|
|
9590
|
-
const specBasename =
|
|
9592
|
+
const specBasename = path20.basename(existingSpecPath);
|
|
9591
9593
|
const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
|
|
9592
9594
|
const slug = slugMatch ? slugMatch[1] : "feature";
|
|
9593
|
-
const specsDir =
|
|
9595
|
+
const specsDir = path20.dirname(existingSpecPath);
|
|
9594
9596
|
const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
|
|
9595
|
-
await
|
|
9596
|
-
await
|
|
9597
|
-
console.log(
|
|
9598
|
-
console.log(
|
|
9597
|
+
await fs21.ensureDir(specsDir);
|
|
9598
|
+
await fs21.writeFile(newSpecPath, updatedSpecContent, "utf-8");
|
|
9599
|
+
console.log(chalk17.green(` \u2714 New spec written: ${path20.relative(projectDir, newSpecPath)}`));
|
|
9600
|
+
console.log(chalk17.blue(" [2/3] Updating DSL..."));
|
|
9599
9601
|
let updatedDsl = null;
|
|
9600
9602
|
let newDslPath = null;
|
|
9601
9603
|
if (existingDsl) {
|
|
@@ -9607,7 +9609,7 @@ var SpecUpdater = class {
|
|
|
9607
9609
|
updatedDsl = parsed;
|
|
9608
9610
|
}
|
|
9609
9611
|
} catch {
|
|
9610
|
-
console.log(
|
|
9612
|
+
console.log(chalk17.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
|
|
9611
9613
|
}
|
|
9612
9614
|
}
|
|
9613
9615
|
if (!updatedDsl) {
|
|
@@ -9616,15 +9618,15 @@ var SpecUpdater = class {
|
|
|
9616
9618
|
}
|
|
9617
9619
|
if (updatedDsl) {
|
|
9618
9620
|
const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
|
|
9619
|
-
await
|
|
9621
|
+
await fs21.writeJson(dslPath, updatedDsl, { spaces: 2 });
|
|
9620
9622
|
newDslPath = dslPath;
|
|
9621
|
-
console.log(
|
|
9623
|
+
console.log(chalk17.green(` \u2714 DSL updated: ${path20.relative(projectDir, dslPath)}`));
|
|
9622
9624
|
} else {
|
|
9623
|
-
console.log(
|
|
9625
|
+
console.log(chalk17.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
|
|
9624
9626
|
}
|
|
9625
9627
|
let affectedFiles = [];
|
|
9626
9628
|
if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
|
|
9627
|
-
console.log(
|
|
9629
|
+
console.log(chalk17.blue(" [3/3] Identifying affected files..."));
|
|
9628
9630
|
const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
|
|
9629
9631
|
const affectedPrompt = buildAffectedFilesPrompt(
|
|
9630
9632
|
changeRequest,
|
|
@@ -9635,9 +9637,9 @@ var SpecUpdater = class {
|
|
|
9635
9637
|
try {
|
|
9636
9638
|
const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
|
|
9637
9639
|
affectedFiles = parseAffectedFiles(affectedRaw);
|
|
9638
|
-
console.log(
|
|
9640
|
+
console.log(chalk17.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
|
|
9639
9641
|
} catch {
|
|
9640
|
-
console.log(
|
|
9642
|
+
console.log(chalk17.gray(" Could not identify affected files \u2014 use manual selection."));
|
|
9641
9643
|
}
|
|
9642
9644
|
}
|
|
9643
9645
|
return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
|
|
@@ -9645,8 +9647,8 @@ var SpecUpdater = class {
|
|
|
9645
9647
|
};
|
|
9646
9648
|
|
|
9647
9649
|
// core/openapi-exporter.ts
|
|
9648
|
-
import * as
|
|
9649
|
-
import * as
|
|
9650
|
+
import * as path21 from "path";
|
|
9651
|
+
import * as fs22 from "fs-extra";
|
|
9650
9652
|
function dslTypeToOASchema(typeDesc, fieldName = "") {
|
|
9651
9653
|
const t = typeDesc.toLowerCase();
|
|
9652
9654
|
if (t === "string" || t.includes("string")) {
|
|
@@ -9875,7 +9877,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9875
9877
|
const format = opts.format ?? "yaml";
|
|
9876
9878
|
const serverUrl = opts.serverUrl ?? "http://localhost:3000";
|
|
9877
9879
|
const defaultName = `openapi.${format}`;
|
|
9878
|
-
const outputPath = opts.outputPath ?
|
|
9880
|
+
const outputPath = opts.outputPath ? path21.isAbsolute(opts.outputPath) ? opts.outputPath : path21.join(projectDir, opts.outputPath) : path21.join(projectDir, defaultName);
|
|
9879
9881
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
9880
9882
|
let content;
|
|
9881
9883
|
if (format === "json") {
|
|
@@ -9883,8 +9885,8 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9883
9885
|
} else {
|
|
9884
9886
|
content = buildYamlDoc(doc);
|
|
9885
9887
|
}
|
|
9886
|
-
await
|
|
9887
|
-
await
|
|
9888
|
+
await fs22.ensureDir(path21.dirname(outputPath));
|
|
9889
|
+
await fs22.writeFile(outputPath, content, "utf-8");
|
|
9888
9890
|
return outputPath;
|
|
9889
9891
|
}
|
|
9890
9892
|
|
|
@@ -9892,9 +9894,9 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9892
9894
|
dotenv.config();
|
|
9893
9895
|
var CONFIG_FILE = ".ai-spec.json";
|
|
9894
9896
|
async function loadConfig(dir) {
|
|
9895
|
-
const p =
|
|
9896
|
-
if (await
|
|
9897
|
-
return
|
|
9897
|
+
const p = path22.join(dir, CONFIG_FILE);
|
|
9898
|
+
if (await fs23.pathExists(p)) {
|
|
9899
|
+
return fs23.readJson(p);
|
|
9898
9900
|
}
|
|
9899
9901
|
return {};
|
|
9900
9902
|
}
|
|
@@ -9919,20 +9921,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
9919
9921
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
9920
9922
|
});
|
|
9921
9923
|
await saveKey(providerName, newKey.trim());
|
|
9922
|
-
console.log(
|
|
9924
|
+
console.log(chalk18.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
9923
9925
|
return newKey.trim();
|
|
9924
9926
|
}
|
|
9925
9927
|
function printBanner(opts) {
|
|
9926
|
-
console.log(
|
|
9927
|
-
console.log(
|
|
9928
|
-
console.log(
|
|
9929
|
-
console.log(
|
|
9928
|
+
console.log(chalk18.blue("\n" + "\u2500".repeat(52)));
|
|
9929
|
+
console.log(chalk18.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
9930
|
+
console.log(chalk18.blue("\u2500".repeat(52)));
|
|
9931
|
+
console.log(chalk18.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
9930
9932
|
console.log(
|
|
9931
|
-
|
|
9933
|
+
chalk18.gray(
|
|
9932
9934
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
9933
9935
|
)
|
|
9934
9936
|
);
|
|
9935
|
-
console.log(
|
|
9937
|
+
console.log(chalk18.blue("\u2500".repeat(52) + "\n"));
|
|
9936
9938
|
}
|
|
9937
9939
|
var program = new Command();
|
|
9938
9940
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -9959,42 +9961,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9959
9961
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
9960
9962
|
const workspaceConfig = await workspaceLoader.load();
|
|
9961
9963
|
if (workspaceConfig) {
|
|
9962
|
-
console.log(
|
|
9964
|
+
console.log(chalk18.cyan(`
|
|
9963
9965
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
9964
|
-
console.log(
|
|
9966
|
+
console.log(chalk18.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
9965
9967
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9966
9968
|
if (opts.serve) {
|
|
9967
|
-
console.log(
|
|
9969
|
+
console.log(chalk18.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9968
9970
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
9969
9971
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
9970
9972
|
if (!backendResult) {
|
|
9971
|
-
console.log(
|
|
9973
|
+
console.log(chalk18.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
9972
9974
|
} else {
|
|
9973
9975
|
const mockPort = 3001;
|
|
9974
9976
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
9975
|
-
const serverJsPath =
|
|
9976
|
-
console.log(
|
|
9977
|
+
const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9978
|
+
console.log(chalk18.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
9977
9979
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
9978
|
-
console.log(
|
|
9980
|
+
console.log(chalk18.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
9979
9981
|
if (frontendResult) {
|
|
9980
9982
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
9981
9983
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
9982
9984
|
if (proxyResult.applied) {
|
|
9983
|
-
console.log(
|
|
9984
|
-
console.log(
|
|
9985
|
+
console.log(chalk18.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
9986
|
+
console.log(chalk18.bold.cyan(`
|
|
9985
9987
|
Ready! Run your frontend dev server:`));
|
|
9986
|
-
console.log(
|
|
9987
|
-
console.log(
|
|
9988
|
-
console.log(
|
|
9988
|
+
console.log(chalk18.white(` cd ${frontendResult.repoAbsPath}`));
|
|
9989
|
+
console.log(chalk18.white(` ${proxyResult.devCommand}`));
|
|
9990
|
+
console.log(chalk18.gray(`
|
|
9989
9991
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
9990
9992
|
} else {
|
|
9991
|
-
console.log(
|
|
9992
|
-
if (proxyResult.note) console.log(
|
|
9993
|
-
console.log(
|
|
9993
|
+
console.log(chalk18.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
9994
|
+
if (proxyResult.note) console.log(chalk18.gray(` ${proxyResult.note}`));
|
|
9995
|
+
console.log(chalk18.gray(` Mock server: http://localhost:${mockPort}`));
|
|
9994
9996
|
}
|
|
9995
9997
|
} else {
|
|
9996
|
-
console.log(
|
|
9997
|
-
console.log(
|
|
9998
|
+
console.log(chalk18.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
9999
|
+
console.log(chalk18.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
9998
10000
|
}
|
|
9999
10001
|
}
|
|
10000
10002
|
}
|
|
@@ -10015,7 +10017,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10015
10017
|
codegenModel: codegenModelName
|
|
10016
10018
|
});
|
|
10017
10019
|
const runId = generateRunId();
|
|
10018
|
-
console.log(
|
|
10020
|
+
console.log(chalk18.gray(` Run ID: ${runId}`));
|
|
10019
10021
|
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10020
10022
|
setActiveSnapshot(runSnapshot);
|
|
10021
10023
|
const runLogger = new RunLogger(currentDir, runId, {
|
|
@@ -10023,25 +10025,25 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10023
10025
|
model: specModelName
|
|
10024
10026
|
});
|
|
10025
10027
|
setActiveLogger(runLogger);
|
|
10026
|
-
console.log(
|
|
10028
|
+
console.log(chalk18.blue("[1/6] Loading project context..."));
|
|
10027
10029
|
runLogger.stageStart("context_load");
|
|
10028
10030
|
const loader = new ContextLoader(currentDir);
|
|
10029
10031
|
const context = await loader.loadProjectContext();
|
|
10030
10032
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10031
10033
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10032
|
-
console.log(
|
|
10033
|
-
console.log(
|
|
10034
|
-
console.log(
|
|
10034
|
+
console.log(chalk18.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10035
|
+
console.log(chalk18.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10036
|
+
console.log(chalk18.gray(` API files : ${context.apiStructure.length} files`));
|
|
10035
10037
|
if (context.schema) {
|
|
10036
|
-
console.log(
|
|
10038
|
+
console.log(chalk18.gray(` Prisma schema: found`));
|
|
10037
10039
|
}
|
|
10038
10040
|
if (context.constitution) {
|
|
10039
|
-
console.log(
|
|
10041
|
+
console.log(chalk18.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
10040
10042
|
if (context.constitution.length > 6e3) {
|
|
10041
|
-
console.log(
|
|
10043
|
+
console.log(chalk18.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10042
10044
|
}
|
|
10043
10045
|
} else {
|
|
10044
|
-
console.log(
|
|
10046
|
+
console.log(chalk18.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
10045
10047
|
try {
|
|
10046
10048
|
const constitutionGen = new ConstitutionGenerator(
|
|
10047
10049
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -10049,12 +10051,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10049
10051
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
10050
10052
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
10051
10053
|
context.constitution = constitutionContent;
|
|
10052
|
-
console.log(
|
|
10054
|
+
console.log(chalk18.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
10053
10055
|
} catch (err) {
|
|
10054
|
-
console.log(
|
|
10056
|
+
console.log(chalk18.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
10055
10057
|
}
|
|
10056
10058
|
}
|
|
10057
|
-
console.log(
|
|
10059
|
+
console.log(chalk18.blue(`
|
|
10058
10060
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
10059
10061
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10060
10062
|
let initialSpec;
|
|
@@ -10064,30 +10066,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10064
10066
|
if (opts.skipTasks) {
|
|
10065
10067
|
const generator = new SpecGenerator(specProvider);
|
|
10066
10068
|
initialSpec = await generator.generateSpec(idea, context);
|
|
10067
|
-
console.log(
|
|
10069
|
+
console.log(chalk18.green(" \u2714 Spec generated."));
|
|
10068
10070
|
} else {
|
|
10069
10071
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
10070
10072
|
initialSpec = result.spec;
|
|
10071
10073
|
initialTasks = result.tasks;
|
|
10072
|
-
console.log(
|
|
10074
|
+
console.log(chalk18.green(` \u2714 Spec generated.`));
|
|
10073
10075
|
if (initialTasks.length > 0) {
|
|
10074
|
-
console.log(
|
|
10076
|
+
console.log(chalk18.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
10075
10077
|
} else {
|
|
10076
|
-
console.log(
|
|
10078
|
+
console.log(chalk18.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10077
10079
|
}
|
|
10078
10080
|
}
|
|
10079
10081
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10080
10082
|
} catch (err) {
|
|
10081
10083
|
runLogger.stageFail("spec_gen", err.message);
|
|
10082
|
-
console.error(
|
|
10084
|
+
console.error(chalk18.red(" \u2718 Spec generation failed:"), err);
|
|
10083
10085
|
process.exit(1);
|
|
10084
10086
|
}
|
|
10085
10087
|
let finalSpec;
|
|
10086
10088
|
if (opts.fast) {
|
|
10087
|
-
console.log(
|
|
10089
|
+
console.log(chalk18.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
10088
10090
|
finalSpec = initialSpec;
|
|
10089
10091
|
} else {
|
|
10090
|
-
console.log(
|
|
10092
|
+
console.log(chalk18.blue("\n[3/6] Interactive spec refinement..."));
|
|
10091
10093
|
runLogger.stageStart("spec_refine");
|
|
10092
10094
|
const refiner = new SpecRefiner(specProvider);
|
|
10093
10095
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
@@ -10098,7 +10100,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10098
10100
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
10099
10101
|
if (shouldRunAssessment) {
|
|
10100
10102
|
if (!opts.auto) {
|
|
10101
|
-
console.log(
|
|
10103
|
+
console.log(chalk18.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10102
10104
|
}
|
|
10103
10105
|
runLogger.stageStart("spec_assess");
|
|
10104
10106
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
@@ -10107,45 +10109,45 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10107
10109
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10108
10110
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10109
10111
|
if (opts.force) {
|
|
10110
|
-
console.log(
|
|
10112
|
+
console.log(chalk18.yellow(`
|
|
10111
10113
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10112
10114
|
} else {
|
|
10113
10115
|
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10114
|
-
console.log(
|
|
10116
|
+
console.log(chalk18.red(`
|
|
10115
10117
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10116
10118
|
if (!opts.auto) {
|
|
10117
|
-
console.log(
|
|
10119
|
+
console.log(chalk18.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
10118
10120
|
} else {
|
|
10119
|
-
console.log(
|
|
10121
|
+
console.log(chalk18.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
10120
10122
|
}
|
|
10121
|
-
console.log(
|
|
10123
|
+
console.log(chalk18.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
10122
10124
|
process.exit(1);
|
|
10123
10125
|
}
|
|
10124
10126
|
}
|
|
10125
10127
|
} else {
|
|
10126
10128
|
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10127
10129
|
if (!opts.auto) {
|
|
10128
|
-
console.log(
|
|
10130
|
+
console.log(chalk18.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10129
10131
|
}
|
|
10130
10132
|
}
|
|
10131
10133
|
}
|
|
10132
10134
|
if (!opts.auto) {
|
|
10133
|
-
console.log(
|
|
10135
|
+
console.log(chalk18.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
10134
10136
|
const specLines = finalSpec.split("\n").length;
|
|
10135
10137
|
const specWords = finalSpec.split(/\s+/).length;
|
|
10136
10138
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10137
|
-
console.log(
|
|
10138
|
-
if (taskCountHint) console.log(
|
|
10139
|
-
const previewSpecsDir =
|
|
10139
|
+
console.log(chalk18.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10140
|
+
if (taskCountHint) console.log(chalk18.gray(taskCountHint));
|
|
10141
|
+
const previewSpecsDir = path22.join(currentDir, "specs");
|
|
10140
10142
|
const slug = featureSlug;
|
|
10141
10143
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10142
10144
|
if (prevVersion) {
|
|
10143
|
-
console.log(
|
|
10145
|
+
console.log(chalk18.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
10144
10146
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
10145
|
-
console.log(
|
|
10147
|
+
console.log(chalk18.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10146
10148
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
10147
10149
|
printDiff(diff);
|
|
10148
|
-
console.log(
|
|
10150
|
+
console.log(chalk18.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"));
|
|
10149
10151
|
}
|
|
10150
10152
|
const gate = await select3({
|
|
10151
10153
|
message: "Ready to proceed to code generation?",
|
|
@@ -10156,9 +10158,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10156
10158
|
]
|
|
10157
10159
|
});
|
|
10158
10160
|
if (gate === "view") {
|
|
10159
|
-
console.log(
|
|
10161
|
+
console.log(chalk18.cyan("\n" + "\u2500".repeat(52)));
|
|
10160
10162
|
console.log(finalSpec);
|
|
10161
|
-
console.log(
|
|
10163
|
+
console.log(chalk18.cyan("\u2500".repeat(52) + "\n"));
|
|
10162
10164
|
const confirm22 = await select3({
|
|
10163
10165
|
message: "Proceed to code generation?",
|
|
10164
10166
|
choices: [
|
|
@@ -10167,92 +10169,92 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10167
10169
|
]
|
|
10168
10170
|
});
|
|
10169
10171
|
if (confirm22 === "abort") {
|
|
10170
|
-
console.log(
|
|
10172
|
+
console.log(chalk18.yellow(" Aborted. Spec was NOT saved."));
|
|
10171
10173
|
process.exit(0);
|
|
10172
10174
|
}
|
|
10173
10175
|
} else if (gate === "abort") {
|
|
10174
|
-
console.log(
|
|
10176
|
+
console.log(chalk18.yellow(" Aborted. Spec was NOT saved."));
|
|
10175
10177
|
process.exit(0);
|
|
10176
10178
|
}
|
|
10177
|
-
console.log(
|
|
10179
|
+
console.log(chalk18.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
10178
10180
|
} else {
|
|
10179
|
-
console.log(
|
|
10181
|
+
console.log(chalk18.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
10180
10182
|
}
|
|
10181
10183
|
let extractedDsl = null;
|
|
10182
10184
|
if (opts.skipDsl) {
|
|
10183
|
-
console.log(
|
|
10185
|
+
console.log(chalk18.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
10184
10186
|
} else {
|
|
10185
|
-
console.log(
|
|
10186
|
-
console.log(
|
|
10187
|
+
console.log(chalk18.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10188
|
+
console.log(chalk18.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10187
10189
|
runLogger.stageStart("dsl_extract");
|
|
10188
10190
|
try {
|
|
10189
10191
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10190
|
-
if (isFrontend) console.log(
|
|
10192
|
+
if (isFrontend) console.log(chalk18.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10191
10193
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10192
10194
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10193
10195
|
if (extractedDsl) {
|
|
10194
10196
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10195
|
-
console.log(
|
|
10197
|
+
console.log(chalk18.green(" \u2714 DSL extracted and validated."));
|
|
10196
10198
|
} else {
|
|
10197
10199
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10198
|
-
console.log(
|
|
10200
|
+
console.log(chalk18.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10199
10201
|
}
|
|
10200
10202
|
} catch (err) {
|
|
10201
10203
|
runLogger.stageFail("dsl_extract", err.message);
|
|
10202
|
-
console.log(
|
|
10204
|
+
console.log(chalk18.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10203
10205
|
}
|
|
10204
10206
|
}
|
|
10205
10207
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
10206
10208
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
10207
10209
|
let workingDir = currentDir;
|
|
10208
10210
|
if (!skipWorktree) {
|
|
10209
|
-
console.log(
|
|
10211
|
+
console.log(chalk18.blue("\n[4/6] Setting up git worktree..."));
|
|
10210
10212
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
10211
10213
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10212
10214
|
if (worktreePath) workingDir = worktreePath;
|
|
10213
10215
|
} else {
|
|
10214
10216
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10215
|
-
console.log(
|
|
10217
|
+
console.log(chalk18.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10216
10218
|
}
|
|
10217
|
-
const specsDir =
|
|
10218
|
-
await
|
|
10219
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10220
|
+
await fs23.ensureDir(specsDir);
|
|
10219
10221
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10220
|
-
await
|
|
10221
|
-
console.log(
|
|
10222
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10222
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10223
|
+
console.log(chalk18.green(`
|
|
10224
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + chalk18.gray(` (v${specVersion})`));
|
|
10223
10225
|
let savedDslFile = null;
|
|
10224
10226
|
if (extractedDsl) {
|
|
10225
10227
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10226
10228
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
10227
|
-
console.log(
|
|
10229
|
+
console.log(chalk18.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
10228
10230
|
}
|
|
10229
10231
|
if (!opts.skipTasks) {
|
|
10230
10232
|
const taskGen = new TaskGenerator(specProvider);
|
|
10231
10233
|
let tasksToSave = initialTasks;
|
|
10232
10234
|
if (tasksToSave.length === 0) {
|
|
10233
|
-
console.log(
|
|
10235
|
+
console.log(chalk18.blue(`
|
|
10234
10236
|
Generating tasks (separate call)...`));
|
|
10235
10237
|
try {
|
|
10236
10238
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
10237
10239
|
} catch (err) {
|
|
10238
|
-
console.log(
|
|
10240
|
+
console.log(chalk18.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
10239
10241
|
}
|
|
10240
10242
|
}
|
|
10241
10243
|
if (tasksToSave.length > 0) {
|
|
10242
10244
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
10243
10245
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
10244
10246
|
printTasks(sorted);
|
|
10245
|
-
console.log(
|
|
10247
|
+
console.log(chalk18.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
10246
10248
|
} else {
|
|
10247
|
-
console.log(
|
|
10249
|
+
console.log(chalk18.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
10248
10250
|
}
|
|
10249
10251
|
}
|
|
10250
|
-
console.log(
|
|
10252
|
+
console.log(chalk18.blue(`
|
|
10251
10253
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
10252
10254
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10253
10255
|
let generatedTestFiles = [];
|
|
10254
10256
|
if (opts.tdd && extractedDsl) {
|
|
10255
|
-
console.log(
|
|
10257
|
+
console.log(chalk18.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
10256
10258
|
const testGen = new TestGenerator(codegenProvider);
|
|
10257
10259
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10258
10260
|
}
|
|
@@ -10266,13 +10268,13 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10266
10268
|
});
|
|
10267
10269
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10268
10270
|
if (opts.tdd) {
|
|
10269
|
-
console.log(
|
|
10271
|
+
console.log(chalk18.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10270
10272
|
} else if (opts.skipTests) {
|
|
10271
|
-
console.log(
|
|
10273
|
+
console.log(chalk18.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
10272
10274
|
} else if (!extractedDsl) {
|
|
10273
|
-
console.log(
|
|
10275
|
+
console.log(chalk18.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
10274
10276
|
} else {
|
|
10275
|
-
console.log(
|
|
10277
|
+
console.log(chalk18.blue(`
|
|
10276
10278
|
[7/9] Test skeleton generation...`));
|
|
10277
10279
|
runLogger.stageStart("test_gen");
|
|
10278
10280
|
const testGen = new TestGenerator(codegenProvider);
|
|
@@ -10280,10 +10282,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10280
10282
|
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10281
10283
|
}
|
|
10282
10284
|
if (opts.skipErrorFeedback) {
|
|
10283
|
-
console.log(
|
|
10285
|
+
console.log(chalk18.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10284
10286
|
} else {
|
|
10285
10287
|
if (opts.tdd) {
|
|
10286
|
-
console.log(
|
|
10288
|
+
console.log(chalk18.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10287
10289
|
}
|
|
10288
10290
|
runLogger.stageStart("error_feedback");
|
|
10289
10291
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
@@ -10294,10 +10296,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10294
10296
|
}
|
|
10295
10297
|
let reviewResult = "";
|
|
10296
10298
|
if (!opts.skipReview) {
|
|
10297
|
-
console.log(
|
|
10299
|
+
console.log(chalk18.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10298
10300
|
runLogger.stageStart("review");
|
|
10299
10301
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10300
|
-
const savedSpec = await
|
|
10302
|
+
const savedSpec = await fs23.readFile(specFile, "utf-8");
|
|
10301
10303
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
10302
10304
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
10303
10305
|
} else {
|
|
@@ -10313,19 +10315,19 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10313
10315
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10314
10316
|
}
|
|
10315
10317
|
runLogger.finish();
|
|
10316
|
-
console.log(
|
|
10317
|
-
console.log(
|
|
10318
|
-
if (savedDslFile) console.log(
|
|
10318
|
+
console.log(chalk18.bold.green("\n\u2714 All done!"));
|
|
10319
|
+
console.log(chalk18.gray(` Spec : ${specFile}`));
|
|
10320
|
+
if (savedDslFile) console.log(chalk18.gray(` DSL : ${savedDslFile}`));
|
|
10319
10321
|
if (generatedTestFiles.length > 0) {
|
|
10320
|
-
console.log(
|
|
10322
|
+
console.log(chalk18.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
10321
10323
|
}
|
|
10322
|
-
console.log(
|
|
10324
|
+
console.log(chalk18.gray(` Working dir : ${workingDir}`));
|
|
10323
10325
|
if (workingDir !== currentDir) {
|
|
10324
|
-
console.log(
|
|
10326
|
+
console.log(chalk18.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10325
10327
|
}
|
|
10326
10328
|
runLogger.printSummary();
|
|
10327
10329
|
if (runSnapshot.fileCount > 0) {
|
|
10328
|
-
console.log(
|
|
10330
|
+
console.log(chalk18.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
10329
10331
|
}
|
|
10330
10332
|
});
|
|
10331
10333
|
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(
|
|
@@ -10342,24 +10344,24 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10342
10344
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
10343
10345
|
let specContent = "";
|
|
10344
10346
|
let resolvedSpecFile;
|
|
10345
|
-
if (specFile && await
|
|
10346
|
-
specContent = await
|
|
10347
|
+
if (specFile && await fs23.pathExists(specFile)) {
|
|
10348
|
+
specContent = await fs23.readFile(specFile, "utf-8");
|
|
10347
10349
|
resolvedSpecFile = specFile;
|
|
10348
|
-
console.log(
|
|
10350
|
+
console.log(chalk18.gray(`Using spec: ${specFile}`));
|
|
10349
10351
|
} else {
|
|
10350
|
-
const specsDir =
|
|
10351
|
-
if (await
|
|
10352
|
-
const files = (await
|
|
10352
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
10353
|
+
if (await fs23.pathExists(specsDir)) {
|
|
10354
|
+
const files = (await fs23.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
10353
10355
|
if (files.length > 0) {
|
|
10354
|
-
const latest =
|
|
10355
|
-
specContent = await
|
|
10356
|
+
const latest = path22.join(specsDir, files[0]);
|
|
10357
|
+
specContent = await fs23.readFile(latest, "utf-8");
|
|
10356
10358
|
resolvedSpecFile = latest;
|
|
10357
|
-
console.log(
|
|
10359
|
+
console.log(chalk18.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10358
10360
|
}
|
|
10359
10361
|
}
|
|
10360
10362
|
}
|
|
10361
10363
|
if (!specContent) {
|
|
10362
|
-
console.log(
|
|
10364
|
+
console.log(chalk18.yellow("No spec file found. Running review without spec context."));
|
|
10363
10365
|
}
|
|
10364
10366
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10365
10367
|
await reviewer.printScoreTrend();
|
|
@@ -10386,15 +10388,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10386
10388
|
auto: opts.auto
|
|
10387
10389
|
});
|
|
10388
10390
|
if (result.written) {
|
|
10389
|
-
console.log(
|
|
10390
|
-
console.log(
|
|
10391
|
-
console.log(
|
|
10391
|
+
console.log(chalk18.blue("\n Summary:"));
|
|
10392
|
+
console.log(chalk18.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10393
|
+
console.log(chalk18.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10392
10394
|
if (result.backupPath) {
|
|
10393
|
-
console.log(
|
|
10395
|
+
console.log(chalk18.gray(` Backup: ${path22.basename(result.backupPath)}`));
|
|
10394
10396
|
}
|
|
10395
10397
|
}
|
|
10396
10398
|
} catch (err) {
|
|
10397
|
-
console.error(
|
|
10399
|
+
console.error(chalk18.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10398
10400
|
process.exit(1);
|
|
10399
10401
|
}
|
|
10400
10402
|
return;
|
|
@@ -10402,75 +10404,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10402
10404
|
if (opts.global) {
|
|
10403
10405
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10404
10406
|
if (existing && !opts.force) {
|
|
10405
|
-
console.log(
|
|
10407
|
+
console.log(chalk18.yellow(`
|
|
10406
10408
|
Global constitution already exists at: ${existing.source}`));
|
|
10407
|
-
console.log(
|
|
10409
|
+
console.log(chalk18.gray(" Use --force to overwrite it."));
|
|
10408
10410
|
return;
|
|
10409
10411
|
}
|
|
10410
|
-
console.log(
|
|
10411
|
-
console.log(
|
|
10412
|
-
console.log(
|
|
10412
|
+
console.log(chalk18.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10413
|
+
console.log(chalk18.gray(` Provider: ${providerName}/${modelName}`));
|
|
10414
|
+
console.log(chalk18.gray(" Scanning repos in workspace..."));
|
|
10413
10415
|
const loader = new ContextLoader(currentDir);
|
|
10414
10416
|
const ctx = await loader.loadProjectContext();
|
|
10415
10417
|
const summary = [
|
|
10416
10418
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10417
10419
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10418
10420
|
].join("\n");
|
|
10419
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10421
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
|
|
10420
10422
|
let globalConstitution;
|
|
10421
10423
|
try {
|
|
10422
10424
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10423
10425
|
} catch (err) {
|
|
10424
|
-
console.error(
|
|
10426
|
+
console.error(chalk18.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10425
10427
|
process.exit(1);
|
|
10426
10428
|
}
|
|
10427
10429
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10428
|
-
console.log(
|
|
10430
|
+
console.log(chalk18.green(`
|
|
10429
10431
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10430
|
-
console.log(
|
|
10431
|
-
console.log(
|
|
10432
|
-
console.log(
|
|
10433
|
-
console.log(
|
|
10432
|
+
console.log(chalk18.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
10433
|
+
console.log(chalk18.gray(" Project-level rules always override global rules.\n"));
|
|
10434
|
+
console.log(chalk18.bold(" Preview:"));
|
|
10435
|
+
console.log(chalk18.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10434
10436
|
if (globalConstitution.split("\n").length > 12) {
|
|
10435
|
-
console.log(
|
|
10437
|
+
console.log(chalk18.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10436
10438
|
}
|
|
10437
10439
|
return;
|
|
10438
10440
|
}
|
|
10439
|
-
const constitutionPath =
|
|
10440
|
-
if (!opts.force && await
|
|
10441
|
-
console.log(
|
|
10441
|
+
const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
|
|
10442
|
+
if (!opts.force && await fs23.pathExists(constitutionPath)) {
|
|
10443
|
+
console.log(chalk18.yellow(`
|
|
10442
10444
|
${CONSTITUTION_FILE} already exists.`));
|
|
10443
|
-
console.log(
|
|
10444
|
-
console.log(
|
|
10445
|
+
console.log(chalk18.gray(" Use --force to overwrite it."));
|
|
10446
|
+
console.log(chalk18.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10445
10447
|
return;
|
|
10446
10448
|
}
|
|
10447
|
-
console.log(
|
|
10448
|
-
console.log(
|
|
10449
|
-
console.log(
|
|
10449
|
+
console.log(chalk18.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10450
|
+
console.log(chalk18.gray(` Provider: ${providerName}/${modelName}`));
|
|
10451
|
+
console.log(chalk18.gray(" Analyzing codebase..."));
|
|
10450
10452
|
const generator = new ConstitutionGenerator(provider);
|
|
10451
10453
|
let constitution;
|
|
10452
10454
|
try {
|
|
10453
10455
|
constitution = await generator.generate(currentDir);
|
|
10454
10456
|
} catch (err) {
|
|
10455
|
-
console.error(
|
|
10457
|
+
console.error(chalk18.red(" \u2718 Failed to generate constitution:"), err);
|
|
10456
10458
|
process.exit(1);
|
|
10457
10459
|
}
|
|
10458
10460
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10459
|
-
const globalResult = await loadGlobalConstitution([
|
|
10461
|
+
const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
|
|
10460
10462
|
if (globalResult) {
|
|
10461
|
-
console.log(
|
|
10463
|
+
console.log(chalk18.cyan(`
|
|
10462
10464
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10463
|
-
console.log(
|
|
10464
|
-
console.log(
|
|
10465
|
+
console.log(chalk18.gray(" It will be merged with this project constitution at runtime."));
|
|
10466
|
+
console.log(chalk18.gray(" Project rules take priority over global rules."));
|
|
10465
10467
|
}
|
|
10466
|
-
console.log(
|
|
10468
|
+
console.log(chalk18.green(`
|
|
10467
10469
|
\u2714 Constitution saved: ${saved}`));
|
|
10468
|
-
console.log(
|
|
10469
|
-
console.log(
|
|
10470
|
-
console.log(
|
|
10471
|
-
console.log(
|
|
10470
|
+
console.log(chalk18.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
10471
|
+
console.log(chalk18.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
10472
|
+
console.log(chalk18.bold(" Preview:"));
|
|
10473
|
+
console.log(chalk18.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10472
10474
|
if (constitution.split("\n").length > 15) {
|
|
10473
|
-
console.log(
|
|
10475
|
+
console.log(chalk18.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10474
10476
|
}
|
|
10475
10477
|
});
|
|
10476
10478
|
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(
|
|
@@ -10478,44 +10480,44 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10478
10480
|
"Default code generation mode (claude-code|api|plan)"
|
|
10479
10481
|
).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) => {
|
|
10480
10482
|
const currentDir = process.cwd();
|
|
10481
|
-
const configPath =
|
|
10483
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10482
10484
|
if (opts.clearKeys) {
|
|
10483
10485
|
await clearAllKeys();
|
|
10484
|
-
console.log(
|
|
10486
|
+
console.log(chalk18.green(`\u2714 All saved API keys cleared.`));
|
|
10485
10487
|
return;
|
|
10486
10488
|
}
|
|
10487
10489
|
if (opts.clearKey) {
|
|
10488
10490
|
await clearKey(opts.clearKey);
|
|
10489
|
-
console.log(
|
|
10491
|
+
console.log(chalk18.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10490
10492
|
return;
|
|
10491
10493
|
}
|
|
10492
10494
|
if (opts.listKeys) {
|
|
10493
|
-
const store = await
|
|
10495
|
+
const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10494
10496
|
const providers = Object.keys(store);
|
|
10495
10497
|
if (providers.length === 0) {
|
|
10496
|
-
console.log(
|
|
10498
|
+
console.log(chalk18.gray("No saved API keys."));
|
|
10497
10499
|
} else {
|
|
10498
|
-
console.log(
|
|
10500
|
+
console.log(chalk18.bold("Saved API keys:"));
|
|
10499
10501
|
for (const p of providers) {
|
|
10500
10502
|
const k2 = store[p];
|
|
10501
|
-
console.log(
|
|
10503
|
+
console.log(chalk18.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10502
10504
|
}
|
|
10503
|
-
console.log(
|
|
10505
|
+
console.log(chalk18.gray(`
|
|
10504
10506
|
File: ${KEY_STORE_FILE}`));
|
|
10505
10507
|
}
|
|
10506
10508
|
return;
|
|
10507
10509
|
}
|
|
10508
10510
|
if (opts.reset) {
|
|
10509
|
-
await
|
|
10510
|
-
console.log(
|
|
10511
|
+
await fs23.writeJson(configPath, {}, { spaces: 2 });
|
|
10512
|
+
console.log(chalk18.green(`\u2714 Config reset: ${configPath}`));
|
|
10511
10513
|
return;
|
|
10512
10514
|
}
|
|
10513
10515
|
const existing = await loadConfig(currentDir);
|
|
10514
10516
|
if (opts.show) {
|
|
10515
10517
|
if (Object.keys(existing).length === 0) {
|
|
10516
|
-
console.log(
|
|
10518
|
+
console.log(chalk18.gray("No config file found. Using built-in defaults."));
|
|
10517
10519
|
} else {
|
|
10518
|
-
console.log(
|
|
10520
|
+
console.log(chalk18.bold(`${configPath}:`));
|
|
10519
10521
|
console.log(JSON.stringify(existing, null, 2));
|
|
10520
10522
|
}
|
|
10521
10523
|
return;
|
|
@@ -10529,27 +10531,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10529
10531
|
if (opts.minSpecScore !== void 0) {
|
|
10530
10532
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10531
10533
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10532
|
-
console.error(
|
|
10534
|
+
console.error(chalk18.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10533
10535
|
process.exit(1);
|
|
10534
10536
|
}
|
|
10535
10537
|
updated.minSpecScore = score;
|
|
10536
10538
|
}
|
|
10537
|
-
await
|
|
10538
|
-
console.log(
|
|
10539
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10540
|
+
console.log(chalk18.green(`\u2714 Config saved to ${configPath}`));
|
|
10539
10541
|
console.log(JSON.stringify(updated, null, 2));
|
|
10540
10542
|
});
|
|
10541
10543
|
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) => {
|
|
10542
10544
|
const currentDir = process.cwd();
|
|
10543
|
-
const configPath =
|
|
10545
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10544
10546
|
if (opts.list) {
|
|
10545
|
-
console.log(
|
|
10547
|
+
console.log(chalk18.bold("\nAvailable providers & models:\n"));
|
|
10546
10548
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10547
10549
|
console.log(
|
|
10548
|
-
` ${
|
|
10550
|
+
` ${chalk18.bold.cyan(key.padEnd(10))} ${chalk18.white(meta.displayName)}`
|
|
10549
10551
|
);
|
|
10550
|
-
console.log(
|
|
10552
|
+
console.log(chalk18.gray(` ${meta.description}`));
|
|
10551
10553
|
console.log(
|
|
10552
|
-
|
|
10554
|
+
chalk18.gray(
|
|
10553
10555
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10554
10556
|
)
|
|
10555
10557
|
);
|
|
@@ -10558,10 +10560,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10558
10560
|
return;
|
|
10559
10561
|
}
|
|
10560
10562
|
const existing = await loadConfig(currentDir);
|
|
10561
|
-
console.log(
|
|
10563
|
+
console.log(chalk18.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"));
|
|
10562
10564
|
if (Object.keys(existing).length > 0) {
|
|
10563
10565
|
console.log(
|
|
10564
|
-
|
|
10566
|
+
chalk18.gray(
|
|
10565
10567
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10566
10568
|
)
|
|
10567
10569
|
);
|
|
@@ -10579,7 +10581,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10579
10581
|
const providerKey = await select3({
|
|
10580
10582
|
message: `${label} \u2014 select provider:`,
|
|
10581
10583
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10582
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
10584
|
+
name: `${meta2.displayName.padEnd(22)} ${chalk18.gray(meta2.description)}`,
|
|
10583
10585
|
value: key,
|
|
10584
10586
|
short: meta2.displayName
|
|
10585
10587
|
}))
|
|
@@ -10587,7 +10589,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10587
10589
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10588
10590
|
const modelChoices = [
|
|
10589
10591
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10590
|
-
{ name:
|
|
10592
|
+
{ name: chalk18.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10591
10593
|
];
|
|
10592
10594
|
let chosenModel = await select3({
|
|
10593
10595
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10621,37 +10623,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10621
10623
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10622
10624
|
updated.codegen = "api";
|
|
10623
10625
|
console.log(
|
|
10624
|
-
|
|
10626
|
+
chalk18.yellow(
|
|
10625
10627
|
`
|
|
10626
10628
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10627
10629
|
)
|
|
10628
10630
|
);
|
|
10629
|
-
console.log(
|
|
10631
|
+
console.log(chalk18.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10630
10632
|
}
|
|
10631
10633
|
}
|
|
10632
10634
|
}
|
|
10633
|
-
console.log(
|
|
10634
|
-
console.log(
|
|
10635
|
+
console.log(chalk18.blue("\n Preview:"));
|
|
10636
|
+
console.log(chalk18.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10635
10637
|
if (updated.codegenProvider) {
|
|
10636
10638
|
console.log(
|
|
10637
|
-
|
|
10639
|
+
chalk18.gray(
|
|
10638
10640
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10639
10641
|
)
|
|
10640
10642
|
);
|
|
10641
10643
|
}
|
|
10642
10644
|
const ok = await confirm2({ message: "Save to .ai-spec.json?", default: true });
|
|
10643
10645
|
if (!ok) {
|
|
10644
|
-
console.log(
|
|
10646
|
+
console.log(chalk18.gray(" Cancelled."));
|
|
10645
10647
|
return;
|
|
10646
10648
|
}
|
|
10647
|
-
await
|
|
10648
|
-
console.log(
|
|
10649
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10650
|
+
console.log(chalk18.green(`
|
|
10649
10651
|
\u2714 Saved to ${configPath}`));
|
|
10650
10652
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10651
10653
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10652
10654
|
if (envKey && !process.env[envKey]) {
|
|
10653
10655
|
console.log(
|
|
10654
|
-
|
|
10656
|
+
chalk18.yellow(
|
|
10655
10657
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10656
10658
|
)
|
|
10657
10659
|
);
|
|
@@ -10670,29 +10672,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10670
10672
|
cliOpts,
|
|
10671
10673
|
contractContextSection
|
|
10672
10674
|
} = opts;
|
|
10673
|
-
console.log(
|
|
10675
|
+
console.log(chalk18.blue(`
|
|
10674
10676
|
[${repoName}] Loading project context...`));
|
|
10675
10677
|
const loader = new ContextLoader(repoAbsPath);
|
|
10676
10678
|
let context = await loader.loadProjectContext();
|
|
10677
10679
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10678
|
-
console.log(
|
|
10679
|
-
console.log(
|
|
10680
|
+
console.log(chalk18.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10681
|
+
console.log(chalk18.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10680
10682
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10681
|
-
console.log(
|
|
10683
|
+
console.log(chalk18.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10682
10684
|
}
|
|
10683
10685
|
if (!context.constitution) {
|
|
10684
|
-
console.log(
|
|
10686
|
+
console.log(chalk18.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10685
10687
|
try {
|
|
10686
10688
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10687
10689
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10688
10690
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10689
10691
|
context.constitution = constitutionContent;
|
|
10690
|
-
console.log(
|
|
10692
|
+
console.log(chalk18.green(` Constitution: generated`));
|
|
10691
10693
|
} catch (err) {
|
|
10692
|
-
console.log(
|
|
10694
|
+
console.log(chalk18.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10693
10695
|
}
|
|
10694
10696
|
} else {
|
|
10695
|
-
console.log(
|
|
10697
|
+
console.log(chalk18.green(` Constitution: found`));
|
|
10696
10698
|
}
|
|
10697
10699
|
let fullIdea = idea;
|
|
10698
10700
|
if (contractContextSection) {
|
|
@@ -10700,58 +10702,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10700
10702
|
|
|
10701
10703
|
${contractContextSection}`;
|
|
10702
10704
|
}
|
|
10703
|
-
console.log(
|
|
10705
|
+
console.log(chalk18.blue(` [${repoName}] Generating spec...`));
|
|
10704
10706
|
let finalSpec;
|
|
10705
10707
|
try {
|
|
10706
10708
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10707
10709
|
finalSpec = result.spec;
|
|
10708
|
-
console.log(
|
|
10710
|
+
console.log(chalk18.green(` Spec generated.`));
|
|
10709
10711
|
} catch (err) {
|
|
10710
|
-
console.error(
|
|
10712
|
+
console.error(chalk18.red(` Spec generation failed: ${err.message}`));
|
|
10711
10713
|
return { dsl: null, specFile: null };
|
|
10712
10714
|
}
|
|
10713
10715
|
let extractedDsl = null;
|
|
10714
10716
|
if (!cliOpts.skipDsl) {
|
|
10715
|
-
console.log(
|
|
10717
|
+
console.log(chalk18.blue(` [${repoName}] Extracting DSL...`));
|
|
10716
10718
|
try {
|
|
10717
10719
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10718
10720
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10719
10721
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10720
10722
|
if (extractedDsl) {
|
|
10721
|
-
console.log(
|
|
10723
|
+
console.log(chalk18.green(` DSL extracted.`));
|
|
10722
10724
|
}
|
|
10723
10725
|
} catch (err) {
|
|
10724
|
-
console.log(
|
|
10726
|
+
console.log(chalk18.yellow(` DSL extraction failed: ${err.message}`));
|
|
10725
10727
|
}
|
|
10726
10728
|
}
|
|
10727
10729
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10728
10730
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10729
10731
|
let workingDir = repoAbsPath;
|
|
10730
10732
|
if (!skipWorktreeForRepo) {
|
|
10731
|
-
console.log(
|
|
10733
|
+
console.log(chalk18.blue(` [${repoName}] Setting up git worktree...`));
|
|
10732
10734
|
try {
|
|
10733
10735
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10734
10736
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10735
10737
|
if (worktreePath) workingDir = worktreePath;
|
|
10736
10738
|
} catch (err) {
|
|
10737
|
-
console.log(
|
|
10739
|
+
console.log(chalk18.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10738
10740
|
}
|
|
10739
10741
|
} else {
|
|
10740
|
-
console.log(
|
|
10742
|
+
console.log(chalk18.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10741
10743
|
}
|
|
10742
|
-
const specsDir =
|
|
10743
|
-
await
|
|
10744
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10745
|
+
await fs23.ensureDir(specsDir);
|
|
10744
10746
|
const featureSlug = slugify(idea);
|
|
10745
10747
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10746
|
-
await
|
|
10747
|
-
console.log(
|
|
10748
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10749
|
+
console.log(chalk18.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
|
|
10748
10750
|
let savedDslFile = null;
|
|
10749
10751
|
if (extractedDsl) {
|
|
10750
10752
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10751
10753
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10752
|
-
console.log(
|
|
10754
|
+
console.log(chalk18.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
|
|
10753
10755
|
}
|
|
10754
|
-
console.log(
|
|
10756
|
+
console.log(chalk18.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10755
10757
|
try {
|
|
10756
10758
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10757
10759
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10759,29 +10761,29 @@ ${contractContextSection}`;
|
|
|
10759
10761
|
dslFilePath: savedDslFile ?? void 0,
|
|
10760
10762
|
repoType: detectedRepoType
|
|
10761
10763
|
});
|
|
10762
|
-
console.log(
|
|
10764
|
+
console.log(chalk18.green(` Code generation complete.`));
|
|
10763
10765
|
} catch (err) {
|
|
10764
|
-
console.log(
|
|
10766
|
+
console.log(chalk18.yellow(` Code generation failed: ${err.message}`));
|
|
10765
10767
|
}
|
|
10766
10768
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10767
|
-
console.log(
|
|
10769
|
+
console.log(chalk18.blue(` [${repoName}] Generating test skeletons...`));
|
|
10768
10770
|
try {
|
|
10769
10771
|
const testGen = new TestGenerator(codegenProvider);
|
|
10770
10772
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10771
|
-
console.log(
|
|
10773
|
+
console.log(chalk18.green(` ${testFiles.length} test file(s) generated.`));
|
|
10772
10774
|
} catch (err) {
|
|
10773
|
-
console.log(
|
|
10775
|
+
console.log(chalk18.yellow(` Test generation failed: ${err.message}`));
|
|
10774
10776
|
}
|
|
10775
10777
|
}
|
|
10776
10778
|
if (!cliOpts.skipErrorFeedback) {
|
|
10777
10779
|
try {
|
|
10778
10780
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10779
10781
|
} catch (err) {
|
|
10780
|
-
console.log(
|
|
10782
|
+
console.log(chalk18.yellow(` Error feedback failed: ${err.message}`));
|
|
10781
10783
|
}
|
|
10782
10784
|
}
|
|
10783
10785
|
if (!cliOpts.skipReview) {
|
|
10784
|
-
console.log(
|
|
10786
|
+
console.log(chalk18.blue(` [${repoName}] Running code review...`));
|
|
10785
10787
|
try {
|
|
10786
10788
|
const reviewer = new CodeReviewer(specProvider);
|
|
10787
10789
|
const originalDir = process.cwd();
|
|
@@ -10793,9 +10795,9 @@ ${contractContextSection}`;
|
|
|
10793
10795
|
process.chdir(originalDir);
|
|
10794
10796
|
}
|
|
10795
10797
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10796
|
-
console.log(
|
|
10798
|
+
console.log(chalk18.green(` Code review complete.`));
|
|
10797
10799
|
} catch (err) {
|
|
10798
|
-
console.log(
|
|
10800
|
+
console.log(chalk18.yellow(` Code review failed: ${err.message}`));
|
|
10799
10801
|
}
|
|
10800
10802
|
}
|
|
10801
10803
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10818,7 +10820,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10818
10820
|
codegenModel: codegenModelName
|
|
10819
10821
|
});
|
|
10820
10822
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10821
|
-
console.log(
|
|
10823
|
+
console.log(chalk18.blue("\n[W1] Loading per-repo contexts..."));
|
|
10822
10824
|
const contexts = /* @__PURE__ */ new Map();
|
|
10823
10825
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10824
10826
|
for (const repo of workspace.repos) {
|
|
@@ -10830,27 +10832,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10830
10832
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10831
10833
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10832
10834
|
frontendContexts.set(repo.name, fctx);
|
|
10833
|
-
console.log(
|
|
10835
|
+
console.log(chalk18.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10834
10836
|
} else {
|
|
10835
|
-
console.log(
|
|
10837
|
+
console.log(chalk18.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10836
10838
|
}
|
|
10837
10839
|
} catch (err) {
|
|
10838
|
-
console.log(
|
|
10840
|
+
console.log(chalk18.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10839
10841
|
}
|
|
10840
10842
|
}
|
|
10841
|
-
console.log(
|
|
10843
|
+
console.log(chalk18.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10842
10844
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10843
10845
|
let decomposition;
|
|
10844
10846
|
try {
|
|
10845
10847
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10846
|
-
console.log(
|
|
10847
|
-
console.log(
|
|
10848
|
+
console.log(chalk18.green(` Summary: ${decomposition.summary}`));
|
|
10849
|
+
console.log(chalk18.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10848
10850
|
if (decomposition.coordinationNotes) {
|
|
10849
|
-
console.log(
|
|
10851
|
+
console.log(chalk18.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10850
10852
|
}
|
|
10851
10853
|
} catch (err) {
|
|
10852
|
-
console.error(
|
|
10853
|
-
console.log(
|
|
10854
|
+
console.error(chalk18.red(` Decomposition failed: ${err.message}`));
|
|
10855
|
+
console.log(chalk18.yellow(" Falling back to running all repos independently."));
|
|
10854
10856
|
decomposition = {
|
|
10855
10857
|
originalRequirement: idea,
|
|
10856
10858
|
summary: idea,
|
|
@@ -10866,11 +10868,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10866
10868
|
};
|
|
10867
10869
|
}
|
|
10868
10870
|
if (!opts.auto) {
|
|
10869
|
-
console.log(
|
|
10870
|
-
console.log(
|
|
10871
|
+
console.log(chalk18.cyan("\n[W3] Decomposition Preview:"));
|
|
10872
|
+
console.log(chalk18.cyan("\u2500".repeat(52)));
|
|
10871
10873
|
for (const r of decomposition.repos) {
|
|
10872
|
-
console.log(
|
|
10873
|
-
console.log(
|
|
10874
|
+
console.log(chalk18.bold(` ${r.repoName} (${r.role})`));
|
|
10875
|
+
console.log(chalk18.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
10874
10876
|
if (r.uxDecisions) {
|
|
10875
10877
|
const ux = r.uxDecisions;
|
|
10876
10878
|
const uxSummary = [
|
|
@@ -10879,13 +10881,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10879
10881
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
10880
10882
|
ux.errorRollback ? "rollback" : ""
|
|
10881
10883
|
].filter(Boolean).join(", ");
|
|
10882
|
-
if (uxSummary) console.log(
|
|
10884
|
+
if (uxSummary) console.log(chalk18.cyan(` UX: ${uxSummary}`));
|
|
10883
10885
|
}
|
|
10884
10886
|
if (r.dependsOnRepos.length > 0) {
|
|
10885
|
-
console.log(
|
|
10887
|
+
console.log(chalk18.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
10886
10888
|
}
|
|
10887
10889
|
}
|
|
10888
|
-
console.log(
|
|
10890
|
+
console.log(chalk18.cyan("\u2500".repeat(52)));
|
|
10889
10891
|
const gate = await select3({
|
|
10890
10892
|
message: "Proceed with multi-repo pipeline?",
|
|
10891
10893
|
choices: [
|
|
@@ -10894,24 +10896,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10894
10896
|
]
|
|
10895
10897
|
});
|
|
10896
10898
|
if (gate === "abort") {
|
|
10897
|
-
console.log(
|
|
10899
|
+
console.log(chalk18.yellow(" Aborted."));
|
|
10898
10900
|
process.exit(0);
|
|
10899
10901
|
}
|
|
10900
10902
|
}
|
|
10901
10903
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
10902
10904
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
10903
|
-
console.log(
|
|
10905
|
+
console.log(chalk18.blue(`
|
|
10904
10906
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
10905
10907
|
const results = [];
|
|
10906
10908
|
for (const repoReq of sortedRepoRequirements) {
|
|
10907
10909
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
10908
10910
|
if (!repoConfig) {
|
|
10909
|
-
console.log(
|
|
10911
|
+
console.log(chalk18.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
10910
10912
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
10911
10913
|
continue;
|
|
10912
10914
|
}
|
|
10913
10915
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
10914
|
-
console.log(
|
|
10916
|
+
console.log(chalk18.bold.blue(`
|
|
10915
10917
|
\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`));
|
|
10916
10918
|
let contractContextSection;
|
|
10917
10919
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -10919,7 +10921,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10919
10921
|
for (const depName of repoReq.dependsOnRepos) {
|
|
10920
10922
|
const depDsl = contractDsls.get(depName);
|
|
10921
10923
|
if (depDsl) {
|
|
10922
|
-
console.log(
|
|
10924
|
+
console.log(chalk18.gray(` Using API contract from: ${depName}`));
|
|
10923
10925
|
const contract = buildFrontendApiContract(depDsl);
|
|
10924
10926
|
contractParts.push(buildContractContextSection(contract));
|
|
10925
10927
|
}
|
|
@@ -10939,7 +10941,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10939
10941
|
frontendContext: frontendCtx
|
|
10940
10942
|
});
|
|
10941
10943
|
contractContextSection = void 0;
|
|
10942
|
-
console.log(
|
|
10944
|
+
console.log(chalk18.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
10943
10945
|
}
|
|
10944
10946
|
try {
|
|
10945
10947
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -10956,22 +10958,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10956
10958
|
});
|
|
10957
10959
|
if (repoReq.isContractProvider && dsl) {
|
|
10958
10960
|
contractDsls.set(repoReq.repoName, dsl);
|
|
10959
|
-
console.log(
|
|
10961
|
+
console.log(chalk18.green(` Contract stored for downstream repos.`));
|
|
10960
10962
|
}
|
|
10961
10963
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
10962
|
-
console.log(
|
|
10964
|
+
console.log(chalk18.green(` \u2714 ${repoReq.repoName} complete`));
|
|
10963
10965
|
} catch (err) {
|
|
10964
|
-
console.error(
|
|
10966
|
+
console.error(chalk18.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
10965
10967
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
10966
10968
|
}
|
|
10967
10969
|
}
|
|
10968
|
-
console.log(
|
|
10969
|
-
console.log(
|
|
10970
|
-
console.log(
|
|
10970
|
+
console.log(chalk18.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
10971
|
+
console.log(chalk18.gray(` Workspace: ${workspace.name}`));
|
|
10972
|
+
console.log(chalk18.gray(` Requirement: ${idea}`));
|
|
10971
10973
|
console.log();
|
|
10972
10974
|
for (const r of results) {
|
|
10973
|
-
const icon = r.status === "success" ?
|
|
10974
|
-
const specInfo = r.specFile ?
|
|
10975
|
+
const icon = r.status === "success" ? chalk18.green("\u2714") : r.status === "failed" ? chalk18.red("\u2718") : chalk18.gray("\u2212");
|
|
10976
|
+
const specInfo = r.specFile ? chalk18.gray(` \u2192 ${r.specFile}`) : "";
|
|
10975
10977
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
10976
10978
|
}
|
|
10977
10979
|
return results;
|
|
@@ -10979,18 +10981,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10979
10981
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
10980
10982
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
10981
10983
|
const currentDir = process.cwd();
|
|
10982
|
-
const configPath =
|
|
10983
|
-
if (await
|
|
10984
|
+
const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
10985
|
+
if (await fs23.pathExists(configPath)) {
|
|
10984
10986
|
const overwrite = await confirm2({
|
|
10985
10987
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
10986
10988
|
default: false
|
|
10987
10989
|
});
|
|
10988
10990
|
if (!overwrite) {
|
|
10989
|
-
console.log(
|
|
10991
|
+
console.log(chalk18.gray(" Cancelled."));
|
|
10990
10992
|
return;
|
|
10991
10993
|
}
|
|
10992
10994
|
}
|
|
10993
|
-
console.log(
|
|
10995
|
+
console.log(chalk18.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"));
|
|
10994
10996
|
const workspaceName = await input({
|
|
10995
10997
|
message: "Workspace name:",
|
|
10996
10998
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -11004,11 +11006,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11004
11006
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11005
11007
|
const detected = await workspaceLoader.autoDetect();
|
|
11006
11008
|
if (detected.length === 0) {
|
|
11007
|
-
console.log(
|
|
11009
|
+
console.log(chalk18.yellow(" No recognizable repos found in sibling directories."));
|
|
11008
11010
|
} else {
|
|
11009
|
-
console.log(
|
|
11011
|
+
console.log(chalk18.cyan("\n Detected repos:"));
|
|
11010
11012
|
for (const r of detected) {
|
|
11011
|
-
console.log(
|
|
11013
|
+
console.log(chalk18.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11012
11014
|
}
|
|
11013
11015
|
const keepAll = await confirm2({
|
|
11014
11016
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -11025,7 +11027,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11025
11027
|
if (keep) repos.push(r);
|
|
11026
11028
|
}
|
|
11027
11029
|
}
|
|
11028
|
-
console.log(
|
|
11030
|
+
console.log(chalk18.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
11029
11031
|
}
|
|
11030
11032
|
}
|
|
11031
11033
|
const repoTypeChoices = [
|
|
@@ -11047,7 +11049,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11047
11049
|
default: repos.length === 0
|
|
11048
11050
|
});
|
|
11049
11051
|
while (addMore) {
|
|
11050
|
-
console.log(
|
|
11052
|
+
console.log(chalk18.cyan(`
|
|
11051
11053
|
Adding repo #${repos.length + 1}`));
|
|
11052
11054
|
const repoName = await input({
|
|
11053
11055
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -11061,16 +11063,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11061
11063
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
11062
11064
|
default: `./${repoName}`
|
|
11063
11065
|
});
|
|
11064
|
-
const absPath =
|
|
11066
|
+
const absPath = path22.resolve(currentDir, repoPath);
|
|
11065
11067
|
let detectedType = "unknown";
|
|
11066
11068
|
let detectedRole = "shared";
|
|
11067
|
-
if (await
|
|
11069
|
+
if (await fs23.pathExists(absPath)) {
|
|
11068
11070
|
const { type, role } = await detectRepoType(absPath);
|
|
11069
11071
|
detectedType = type;
|
|
11070
11072
|
detectedRole = role;
|
|
11071
|
-
console.log(
|
|
11073
|
+
console.log(chalk18.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
11072
11074
|
} else {
|
|
11073
|
-
console.log(
|
|
11075
|
+
console.log(chalk18.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
11074
11076
|
}
|
|
11075
11077
|
const repoType = await select3({
|
|
11076
11078
|
message: `Repo type for "${repoName}":`,
|
|
@@ -11093,53 +11095,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11093
11095
|
type: repoType,
|
|
11094
11096
|
role: repoRole
|
|
11095
11097
|
});
|
|
11096
|
-
console.log(
|
|
11098
|
+
console.log(chalk18.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
11097
11099
|
addMore = await confirm2({
|
|
11098
11100
|
message: "Add another repo?",
|
|
11099
11101
|
default: false
|
|
11100
11102
|
});
|
|
11101
11103
|
}
|
|
11102
11104
|
const workspaceConfig = { name: workspaceName, repos };
|
|
11103
|
-
console.log(
|
|
11104
|
-
console.log(
|
|
11105
|
+
console.log(chalk18.cyan("\n Workspace summary:"));
|
|
11106
|
+
console.log(chalk18.gray(` Name: ${workspaceName}`));
|
|
11105
11107
|
for (const r of repos) {
|
|
11106
|
-
console.log(
|
|
11108
|
+
console.log(chalk18.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11107
11109
|
}
|
|
11108
11110
|
const ok = await confirm2({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
11109
11111
|
if (!ok) {
|
|
11110
|
-
console.log(
|
|
11112
|
+
console.log(chalk18.gray(" Cancelled."));
|
|
11111
11113
|
return;
|
|
11112
11114
|
}
|
|
11113
11115
|
const loader = new WorkspaceLoader(currentDir);
|
|
11114
11116
|
const saved = await loader.save(workspaceConfig);
|
|
11115
|
-
console.log(
|
|
11117
|
+
console.log(chalk18.green(`
|
|
11116
11118
|
\u2714 Workspace saved: ${saved}`));
|
|
11117
|
-
console.log(
|
|
11119
|
+
console.log(chalk18.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
11118
11120
|
});
|
|
11119
11121
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
11120
11122
|
const currentDir = process.cwd();
|
|
11121
11123
|
const loader = new WorkspaceLoader(currentDir);
|
|
11122
11124
|
const config2 = await loader.load();
|
|
11123
11125
|
if (!config2) {
|
|
11124
|
-
console.log(
|
|
11125
|
-
console.log(
|
|
11126
|
+
console.log(chalk18.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11127
|
+
console.log(chalk18.gray(" Run `ai-spec workspace init` to create one."));
|
|
11126
11128
|
return;
|
|
11127
11129
|
}
|
|
11128
|
-
console.log(
|
|
11130
|
+
console.log(chalk18.bold(`
|
|
11129
11131
|
Workspace: ${config2.name}`));
|
|
11130
|
-
console.log(
|
|
11131
|
-
console.log(
|
|
11132
|
+
console.log(chalk18.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11133
|
+
console.log(chalk18.gray(` Repos (${config2.repos.length}):
|
|
11132
11134
|
`));
|
|
11133
11135
|
for (const repo of config2.repos) {
|
|
11134
11136
|
const absPath = loader.resolveAbsPath(repo);
|
|
11135
|
-
const exists = await
|
|
11136
|
-
const status = exists ?
|
|
11137
|
+
const exists = await fs23.pathExists(absPath);
|
|
11138
|
+
const status = exists ? chalk18.green("found") : chalk18.red("not found");
|
|
11137
11139
|
console.log(
|
|
11138
|
-
` ${
|
|
11140
|
+
` ${chalk18.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
11139
11141
|
);
|
|
11140
|
-
console.log(
|
|
11142
|
+
console.log(chalk18.gray(` path: ${absPath}`));
|
|
11141
11143
|
if (repo.constitution) {
|
|
11142
|
-
console.log(
|
|
11144
|
+
console.log(chalk18.green(` constitution: found`));
|
|
11143
11145
|
}
|
|
11144
11146
|
}
|
|
11145
11147
|
});
|
|
@@ -11156,30 +11158,30 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11156
11158
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
11157
11159
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
11158
11160
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11159
|
-
console.log(
|
|
11160
|
-
console.log(
|
|
11161
|
+
console.log(chalk18.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"));
|
|
11162
|
+
console.log(chalk18.gray(` Provider: ${providerName}/${modelName}`));
|
|
11161
11163
|
const updateRunId = generateRunId();
|
|
11162
11164
|
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11163
11165
|
setActiveSnapshot(updateSnapshot);
|
|
11164
11166
|
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11165
11167
|
setActiveLogger(updateLogger);
|
|
11166
|
-
console.log(
|
|
11168
|
+
console.log(chalk18.gray(` Run ID: ${updateRunId}`));
|
|
11167
11169
|
let specPath = opts.spec ?? null;
|
|
11168
11170
|
if (!specPath) {
|
|
11169
|
-
const specsDir =
|
|
11171
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
11170
11172
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11171
11173
|
if (!latest) {
|
|
11172
|
-
console.error(
|
|
11174
|
+
console.error(chalk18.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11173
11175
|
process.exit(1);
|
|
11174
11176
|
}
|
|
11175
11177
|
specPath = latest.filePath;
|
|
11176
|
-
console.log(
|
|
11178
|
+
console.log(chalk18.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11177
11179
|
}
|
|
11178
|
-
console.log(
|
|
11180
|
+
console.log(chalk18.gray(" Loading project context..."));
|
|
11179
11181
|
const loader = new ContextLoader(currentDir);
|
|
11180
11182
|
const context = await loader.loadProjectContext();
|
|
11181
11183
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
11182
|
-
console.log(
|
|
11184
|
+
console.log(chalk18.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11183
11185
|
}
|
|
11184
11186
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
11185
11187
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -11191,19 +11193,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11191
11193
|
repoType
|
|
11192
11194
|
});
|
|
11193
11195
|
} catch (err) {
|
|
11194
|
-
console.error(
|
|
11196
|
+
console.error(chalk18.red(` Update failed: ${err.message}`));
|
|
11195
11197
|
process.exit(1);
|
|
11196
11198
|
}
|
|
11197
|
-
console.log(
|
|
11198
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11199
|
+
console.log(chalk18.green(`
|
|
11200
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
|
|
11199
11201
|
if (result.newDslPath) {
|
|
11200
|
-
console.log(
|
|
11202
|
+
console.log(chalk18.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
|
|
11201
11203
|
}
|
|
11202
11204
|
if (result.affectedFiles.length > 0) {
|
|
11203
|
-
console.log(
|
|
11205
|
+
console.log(chalk18.cyan("\n Affected files:"));
|
|
11204
11206
|
for (const f of result.affectedFiles) {
|
|
11205
|
-
const icon = f.action === "create" ?
|
|
11206
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11207
|
+
const icon = f.action === "create" ? chalk18.green("+") : chalk18.yellow("~");
|
|
11208
|
+
console.log(` ${icon} ${f.file}: ${chalk18.gray(f.description)}`);
|
|
11207
11209
|
}
|
|
11208
11210
|
}
|
|
11209
11211
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -11211,9 +11213,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11211
11213
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
11212
11214
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
11213
11215
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11214
|
-
console.log(
|
|
11216
|
+
console.log(chalk18.blue("\n Regenerating affected files..."));
|
|
11215
11217
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11216
|
-
const specContent = await
|
|
11218
|
+
const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
|
|
11217
11219
|
const constitutionSection = context.constitution ? `
|
|
11218
11220
|
=== Project Constitution (MUST follow) ===
|
|
11219
11221
|
${context.constitution}
|
|
@@ -11224,10 +11226,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
|
11224
11226
|
` : "";
|
|
11225
11227
|
updateLogger.stageStart("update_codegen");
|
|
11226
11228
|
for (const affected of result.affectedFiles) {
|
|
11227
|
-
const fullPath =
|
|
11229
|
+
const fullPath = path22.join(currentDir, affected.file);
|
|
11228
11230
|
let existing = "";
|
|
11229
11231
|
try {
|
|
11230
|
-
existing = await
|
|
11232
|
+
existing = await fs23.readFile(fullPath, "utf-8");
|
|
11231
11233
|
} catch {
|
|
11232
11234
|
}
|
|
11233
11235
|
const codePrompt = `Apply this change to the file.
|
|
@@ -11241,23 +11243,23 @@ ${specContent}
|
|
|
11241
11243
|
${constitutionSection}${dslSection}
|
|
11242
11244
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
11243
11245
|
${existing || "Create from scratch."}`;
|
|
11244
|
-
process.stdout.write(` ${existing ?
|
|
11246
|
+
process.stdout.write(` ${existing ? chalk18.yellow("~") : chalk18.green("+")} ${affected.file}... `);
|
|
11245
11247
|
try {
|
|
11246
11248
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11247
11249
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11248
11250
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11249
|
-
await
|
|
11251
|
+
await fs23.ensureDir(path22.dirname(fullPath));
|
|
11250
11252
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11251
|
-
await
|
|
11253
|
+
await fs23.writeFile(fullPath, content, "utf-8");
|
|
11252
11254
|
updateLogger.fileWritten(affected.file);
|
|
11253
|
-
console.log(
|
|
11255
|
+
console.log(chalk18.green("\u2714"));
|
|
11254
11256
|
} catch (err) {
|
|
11255
11257
|
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11256
|
-
console.log(
|
|
11258
|
+
console.log(chalk18.red(`\u2718 ${err.message}`));
|
|
11257
11259
|
}
|
|
11258
11260
|
}
|
|
11259
11261
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11260
|
-
const updatedSpecContent = await
|
|
11262
|
+
const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11261
11263
|
if (updatedSpecContent) {
|
|
11262
11264
|
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11263
11265
|
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
@@ -11269,13 +11271,13 @@ ${existing || "Create from scratch."}`;
|
|
|
11269
11271
|
updateLogger.finish();
|
|
11270
11272
|
updateLogger.printSummary();
|
|
11271
11273
|
if (updateSnapshot.fileCount > 0) {
|
|
11272
|
-
console.log(
|
|
11274
|
+
console.log(chalk18.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11273
11275
|
}
|
|
11274
11276
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11275
|
-
console.log(
|
|
11276
|
-
console.log(
|
|
11277
|
-
console.log(
|
|
11278
|
-
console.log(
|
|
11277
|
+
console.log(chalk18.blue("\n Next steps:"));
|
|
11278
|
+
console.log(chalk18.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11279
|
+
console.log(chalk18.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11280
|
+
console.log(chalk18.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
11279
11281
|
}
|
|
11280
11282
|
});
|
|
11281
11283
|
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) => {
|
|
@@ -11284,19 +11286,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11284
11286
|
if (!dslPath) {
|
|
11285
11287
|
dslPath = await findLatestDslFile(currentDir);
|
|
11286
11288
|
if (!dslPath) {
|
|
11287
|
-
console.error(
|
|
11289
|
+
console.error(chalk18.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11288
11290
|
process.exit(1);
|
|
11289
11291
|
}
|
|
11290
|
-
console.log(
|
|
11292
|
+
console.log(chalk18.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11291
11293
|
}
|
|
11292
11294
|
let dsl;
|
|
11293
11295
|
try {
|
|
11294
|
-
dsl = await
|
|
11296
|
+
dsl = await fs23.readJson(dslPath);
|
|
11295
11297
|
} catch (err) {
|
|
11296
|
-
console.error(
|
|
11298
|
+
console.error(chalk18.red(` Failed to read DSL: ${err.message}`));
|
|
11297
11299
|
process.exit(1);
|
|
11298
11300
|
}
|
|
11299
|
-
console.log(
|
|
11301
|
+
console.log(chalk18.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"));
|
|
11300
11302
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
11301
11303
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
11302
11304
|
try {
|
|
@@ -11305,31 +11307,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11305
11307
|
serverUrl,
|
|
11306
11308
|
outputPath: opts.output
|
|
11307
11309
|
});
|
|
11308
|
-
const rel =
|
|
11309
|
-
console.log(
|
|
11310
|
-
console.log(
|
|
11311
|
-
console.log(
|
|
11312
|
-
console.log(
|
|
11313
|
-
console.log(
|
|
11314
|
-
console.log(
|
|
11315
|
-
console.log(
|
|
11316
|
-
console.log(
|
|
11310
|
+
const rel = path22.relative(currentDir, outputPath);
|
|
11311
|
+
console.log(chalk18.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11312
|
+
console.log(chalk18.gray(` Feature : ${dsl.feature.title}`));
|
|
11313
|
+
console.log(chalk18.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11314
|
+
console.log(chalk18.gray(` Models : ${dsl.models.length}`));
|
|
11315
|
+
console.log(chalk18.gray(` Server : ${serverUrl}`));
|
|
11316
|
+
console.log(chalk18.blue("\n Next steps:"));
|
|
11317
|
+
console.log(chalk18.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11318
|
+
console.log(chalk18.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
11317
11319
|
} catch (err) {
|
|
11318
|
-
console.error(
|
|
11320
|
+
console.error(chalk18.red(` Export failed: ${err.message}`));
|
|
11319
11321
|
process.exit(1);
|
|
11320
11322
|
}
|
|
11321
11323
|
});
|
|
11322
11324
|
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) => {
|
|
11323
11325
|
const currentDir = process.cwd();
|
|
11324
11326
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11325
|
-
console.log(
|
|
11327
|
+
console.log(chalk18.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"));
|
|
11326
11328
|
if (opts.restore) {
|
|
11327
|
-
const frontendDir = opts.frontend ?
|
|
11329
|
+
const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
|
|
11328
11330
|
const r = await restoreMockProxy(frontendDir);
|
|
11329
11331
|
if (r.restored) {
|
|
11330
|
-
console.log(
|
|
11332
|
+
console.log(chalk18.green(" \u2714 Proxy restored and mock server stopped."));
|
|
11331
11333
|
} else {
|
|
11332
|
-
console.log(
|
|
11334
|
+
console.log(chalk18.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
11333
11335
|
}
|
|
11334
11336
|
return;
|
|
11335
11337
|
}
|
|
@@ -11337,32 +11339,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11337
11339
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11338
11340
|
const workspaceConfig = await workspaceLoader.load();
|
|
11339
11341
|
if (!workspaceConfig) {
|
|
11340
|
-
console.error(
|
|
11342
|
+
console.error(chalk18.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
11341
11343
|
process.exit(1);
|
|
11342
11344
|
}
|
|
11343
11345
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
11344
11346
|
if (backendRepos.length === 0) {
|
|
11345
|
-
console.log(
|
|
11347
|
+
console.log(chalk18.yellow(" No backend repos found in workspace."));
|
|
11346
11348
|
return;
|
|
11347
11349
|
}
|
|
11348
11350
|
for (const repo of backendRepos) {
|
|
11349
11351
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
11350
|
-
console.log(
|
|
11352
|
+
console.log(chalk18.cyan(`
|
|
11351
11353
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
11352
11354
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
11353
11355
|
if (!dslFile) {
|
|
11354
|
-
console.log(
|
|
11356
|
+
console.log(chalk18.yellow(` No DSL file found \u2014 skipping.`));
|
|
11355
11357
|
continue;
|
|
11356
11358
|
}
|
|
11357
|
-
const dsl2 = await
|
|
11359
|
+
const dsl2 = await fs23.readJson(dslFile);
|
|
11358
11360
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
11359
11361
|
port,
|
|
11360
11362
|
msw: opts.msw,
|
|
11361
11363
|
proxy: opts.proxy
|
|
11362
11364
|
});
|
|
11363
11365
|
for (const f of result2.files) {
|
|
11364
|
-
console.log(
|
|
11365
|
-
console.log(
|
|
11366
|
+
console.log(chalk18.green(` \u2714 ${f.path}`));
|
|
11367
|
+
console.log(chalk18.gray(` ${f.description}`));
|
|
11366
11368
|
}
|
|
11367
11369
|
}
|
|
11368
11370
|
return;
|
|
@@ -11372,19 +11374,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11372
11374
|
dslPath = await findLatestDslFile(currentDir);
|
|
11373
11375
|
if (!dslPath) {
|
|
11374
11376
|
console.error(
|
|
11375
|
-
|
|
11377
|
+
chalk18.red(
|
|
11376
11378
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
11377
11379
|
)
|
|
11378
11380
|
);
|
|
11379
11381
|
process.exit(1);
|
|
11380
11382
|
}
|
|
11381
|
-
console.log(
|
|
11383
|
+
console.log(chalk18.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11382
11384
|
}
|
|
11383
11385
|
let dsl;
|
|
11384
11386
|
try {
|
|
11385
|
-
dsl = await
|
|
11387
|
+
dsl = await fs23.readJson(dslPath);
|
|
11386
11388
|
} catch (err) {
|
|
11387
|
-
console.error(
|
|
11389
|
+
console.error(chalk18.red(` Failed to read DSL file: ${err.message}`));
|
|
11388
11390
|
process.exit(1);
|
|
11389
11391
|
}
|
|
11390
11392
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11392,58 +11394,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11392
11394
|
msw: opts.msw,
|
|
11393
11395
|
proxy: opts.proxy
|
|
11394
11396
|
});
|
|
11395
|
-
console.log(
|
|
11397
|
+
console.log(chalk18.green(`
|
|
11396
11398
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11397
11399
|
for (const f of result.files) {
|
|
11398
|
-
console.log(
|
|
11399
|
-
console.log(
|
|
11400
|
+
console.log(chalk18.green(` ${f.path}`));
|
|
11401
|
+
console.log(chalk18.gray(` ${f.description}`));
|
|
11400
11402
|
}
|
|
11401
11403
|
if (opts.serve) {
|
|
11402
|
-
const serverJsPath =
|
|
11403
|
-
if (!await
|
|
11404
|
-
console.error(
|
|
11404
|
+
const serverJsPath = path22.join(currentDir, "mock", "server.js");
|
|
11405
|
+
if (!await fs23.pathExists(serverJsPath)) {
|
|
11406
|
+
console.error(chalk18.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11405
11407
|
process.exit(1);
|
|
11406
11408
|
}
|
|
11407
11409
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11408
|
-
console.log(
|
|
11410
|
+
console.log(chalk18.green(`
|
|
11409
11411
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11410
11412
|
if (opts.frontend) {
|
|
11411
|
-
const frontendDir =
|
|
11413
|
+
const frontendDir = path22.resolve(opts.frontend);
|
|
11412
11414
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11413
11415
|
await saveMockServerPid(frontendDir, pid);
|
|
11414
11416
|
if (proxyResult.applied) {
|
|
11415
|
-
console.log(
|
|
11416
|
-
console.log(
|
|
11417
|
+
console.log(chalk18.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11418
|
+
console.log(chalk18.bold.cyan(`
|
|
11417
11419
|
Ready! Open a new terminal and run:`));
|
|
11418
|
-
console.log(
|
|
11419
|
-
console.log(
|
|
11420
|
-
console.log(
|
|
11420
|
+
console.log(chalk18.white(` cd ${frontendDir}`));
|
|
11421
|
+
console.log(chalk18.white(` ${proxyResult.devCommand}`));
|
|
11422
|
+
console.log(chalk18.gray(`
|
|
11421
11423
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11422
11424
|
} else {
|
|
11423
|
-
console.log(
|
|
11424
|
-
if (proxyResult.note) console.log(
|
|
11425
|
+
console.log(chalk18.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11426
|
+
if (proxyResult.note) console.log(chalk18.gray(` ${proxyResult.note}`));
|
|
11425
11427
|
}
|
|
11426
11428
|
} else {
|
|
11427
|
-
console.log(
|
|
11428
|
-
console.log(
|
|
11429
|
+
console.log(chalk18.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11430
|
+
console.log(chalk18.gray(` Mock server: http://localhost:${port}`));
|
|
11429
11431
|
}
|
|
11430
11432
|
return;
|
|
11431
11433
|
}
|
|
11432
|
-
console.log(
|
|
11433
|
-
console.log(
|
|
11434
|
-
console.log(
|
|
11435
|
-
console.log(
|
|
11436
|
-
console.log(
|
|
11437
|
-
console.log(
|
|
11438
|
-
console.log(
|
|
11439
|
-
console.log(
|
|
11434
|
+
console.log(chalk18.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"));
|
|
11435
|
+
console.log(chalk18.white(` 1. Install express (if not already):`));
|
|
11436
|
+
console.log(chalk18.gray(` npm install --save-dev express`));
|
|
11437
|
+
console.log(chalk18.white(` 2. Start mock server:`));
|
|
11438
|
+
console.log(chalk18.gray(` node mock/server.js`));
|
|
11439
|
+
console.log(chalk18.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
11440
|
+
console.log(chalk18.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
11441
|
+
console.log(chalk18.gray(` http://localhost:${port}`));
|
|
11440
11442
|
if (opts.proxy) {
|
|
11441
|
-
console.log(
|
|
11443
|
+
console.log(chalk18.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11442
11444
|
}
|
|
11443
11445
|
if (opts.msw) {
|
|
11444
|
-
console.log(
|
|
11445
|
-
console.log(
|
|
11446
|
-
console.log(
|
|
11446
|
+
console.log(chalk18.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
11447
|
+
console.log(chalk18.gray(` import { worker } from './mocks/browser';`));
|
|
11448
|
+
console.log(chalk18.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11447
11449
|
}
|
|
11448
11450
|
});
|
|
11449
11451
|
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) => {
|
|
@@ -11459,34 +11461,26 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11459
11461
|
}
|
|
11460
11462
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11461
11463
|
if (result.appended) {
|
|
11462
|
-
console.log(
|
|
11464
|
+
console.log(chalk18.green(`
|
|
11463
11465
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11464
|
-
console.log(
|
|
11466
|
+
console.log(chalk18.gray(` File: .ai-spec-constitution.md`));
|
|
11465
11467
|
} else {
|
|
11466
|
-
console.log(
|
|
11468
|
+
console.log(chalk18.yellow(`
|
|
11467
11469
|
\u26A0 Not appended: ${result.reason}`));
|
|
11468
11470
|
}
|
|
11469
11471
|
});
|
|
11470
11472
|
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) => {
|
|
11471
11473
|
const currentDir = process.cwd();
|
|
11472
11474
|
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11473
|
-
console.log(
|
|
11475
|
+
console.log(chalk18.blue(`Restoring run: ${runId}...`));
|
|
11474
11476
|
const restored = await snapshot.restore();
|
|
11475
11477
|
if (restored.length === 0) {
|
|
11476
|
-
console.log(
|
|
11478
|
+
console.log(chalk18.yellow(" No backup found for this run ID."));
|
|
11477
11479
|
} else {
|
|
11478
|
-
restored.forEach((f) => console.log(
|
|
11479
|
-
console.log(
|
|
11480
|
+
restored.forEach((f) => console.log(chalk18.green(` \u2714 restored: ${f}`)));
|
|
11481
|
+
console.log(chalk18.bold.green(`
|
|
11480
11482
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11481
11483
|
}
|
|
11482
11484
|
});
|
|
11483
|
-
|
|
11484
|
-
(async () => {
|
|
11485
|
-
const currentDir = process.cwd();
|
|
11486
|
-
const config2 = await loadConfig(currentDir);
|
|
11487
|
-
await printWelcome(currentDir, config2);
|
|
11488
|
-
})();
|
|
11489
|
-
} else {
|
|
11490
|
-
program.parse();
|
|
11491
|
-
}
|
|
11485
|
+
program.parse();
|
|
11492
11486
|
//# sourceMappingURL=index.mjs.map
|