ai-spec-dev 0.30.0 → 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/cli/index.ts +1 -11
- package/dist/cli/index.js +536 -646
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +536 -646
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +1 -1
- package/cli/welcome.ts +0 -151
package/dist/cli/index.js
CHANGED
|
@@ -423,8 +423,8 @@ var init_workspace_loader = __esm({
|
|
|
423
423
|
const repos = [];
|
|
424
424
|
for (const entry of entries) {
|
|
425
425
|
const absPath = path17.join(this.workspaceRoot, entry);
|
|
426
|
-
const
|
|
427
|
-
if (!
|
|
426
|
+
const stat3 = await fs18.stat(absPath).catch(() => null);
|
|
427
|
+
if (!stat3 || !stat3.isDirectory()) continue;
|
|
428
428
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
429
429
|
if (names && !names.includes(entry)) continue;
|
|
430
430
|
const hasManifest = await fs18.pathExists(path17.join(absPath, "package.json")) || await fs18.pathExists(path17.join(absPath, "go.mod")) || await fs18.pathExists(path17.join(absPath, "Cargo.toml")) || await fs18.pathExists(path17.join(absPath, "pom.xml")) || await fs18.pathExists(path17.join(absPath, "build.gradle")) || await fs18.pathExists(path17.join(absPath, "requirements.txt")) || await fs18.pathExists(path17.join(absPath, "pyproject.toml")) || await fs18.pathExists(path17.join(absPath, "composer.json"));
|
|
@@ -488,9 +488,9 @@ var init_workspace_loader = __esm({
|
|
|
488
488
|
|
|
489
489
|
// cli/index.ts
|
|
490
490
|
var import_commander = require("commander");
|
|
491
|
-
var
|
|
492
|
-
var
|
|
493
|
-
var
|
|
491
|
+
var path22 = __toESM(require("path"));
|
|
492
|
+
var fs23 = __toESM(require("fs-extra"));
|
|
493
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
494
494
|
var dotenv = __toESM(require("dotenv"));
|
|
495
495
|
var import_prompts3 = require("@inquirer/prompts");
|
|
496
496
|
|
|
@@ -4870,221 +4870,221 @@ function validateDsl(raw) {
|
|
|
4870
4870
|
}
|
|
4871
4871
|
return { valid: true, dsl: raw };
|
|
4872
4872
|
}
|
|
4873
|
-
function validateFeature(raw,
|
|
4873
|
+
function validateFeature(raw, path23, errors) {
|
|
4874
4874
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4875
|
-
errors.push({ path:
|
|
4875
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4876
4876
|
return;
|
|
4877
4877
|
}
|
|
4878
4878
|
const f = raw;
|
|
4879
|
-
requireNonEmptyString(f["id"], `${
|
|
4880
|
-
requireNonEmptyString(f["title"], `${
|
|
4881
|
-
requireNonEmptyString(f["description"], `${
|
|
4879
|
+
requireNonEmptyString(f["id"], `${path23}.id`, errors);
|
|
4880
|
+
requireNonEmptyString(f["title"], `${path23}.title`, errors);
|
|
4881
|
+
requireNonEmptyString(f["description"], `${path23}.description`, errors);
|
|
4882
4882
|
}
|
|
4883
|
-
function validateModel(raw,
|
|
4883
|
+
function validateModel(raw, path23, errors) {
|
|
4884
4884
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4885
|
-
errors.push({ path:
|
|
4885
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4886
4886
|
return;
|
|
4887
4887
|
}
|
|
4888
4888
|
const m = raw;
|
|
4889
|
-
requireNonEmptyString(m["name"], `${
|
|
4889
|
+
requireNonEmptyString(m["name"], `${path23}.name`, errors);
|
|
4890
4890
|
if (!Array.isArray(m["fields"])) {
|
|
4891
|
-
errors.push({ path: `${
|
|
4891
|
+
errors.push({ path: `${path23}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4892
4892
|
} else {
|
|
4893
4893
|
const fields = m["fields"];
|
|
4894
4894
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4895
|
-
errors.push({ path: `${
|
|
4895
|
+
errors.push({ path: `${path23}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4896
4896
|
}
|
|
4897
4897
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4898
|
-
validateModelField(fields[j2], `${
|
|
4898
|
+
validateModelField(fields[j2], `${path23}.fields[${j2}]`, errors);
|
|
4899
4899
|
}
|
|
4900
4900
|
}
|
|
4901
4901
|
if (m["relations"] !== void 0) {
|
|
4902
4902
|
if (!Array.isArray(m["relations"])) {
|
|
4903
|
-
errors.push({ path: `${
|
|
4903
|
+
errors.push({ path: `${path23}.relations`, message: "Must be an array of strings if present" });
|
|
4904
4904
|
} else {
|
|
4905
4905
|
const rels = m["relations"];
|
|
4906
4906
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4907
4907
|
if (typeof rels[j2] !== "string") {
|
|
4908
|
-
errors.push({ path: `${
|
|
4908
|
+
errors.push({ path: `${path23}.relations[${j2}]`, message: "Must be a string" });
|
|
4909
4909
|
}
|
|
4910
4910
|
}
|
|
4911
4911
|
}
|
|
4912
4912
|
}
|
|
4913
4913
|
}
|
|
4914
|
-
function validateModelField(raw,
|
|
4914
|
+
function validateModelField(raw, path23, errors) {
|
|
4915
4915
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4916
|
-
errors.push({ path:
|
|
4916
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4917
4917
|
return;
|
|
4918
4918
|
}
|
|
4919
4919
|
const f = raw;
|
|
4920
|
-
requireNonEmptyString(f["name"], `${
|
|
4921
|
-
requireNonEmptyString(f["type"], `${
|
|
4920
|
+
requireNonEmptyString(f["name"], `${path23}.name`, errors);
|
|
4921
|
+
requireNonEmptyString(f["type"], `${path23}.type`, errors);
|
|
4922
4922
|
if (typeof f["required"] !== "boolean") {
|
|
4923
|
-
errors.push({ path: `${
|
|
4923
|
+
errors.push({ path: `${path23}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4924
4924
|
}
|
|
4925
4925
|
}
|
|
4926
|
-
function validateEndpoint(raw,
|
|
4926
|
+
function validateEndpoint(raw, path23, errors) {
|
|
4927
4927
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4928
|
-
errors.push({ path:
|
|
4928
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4929
4929
|
return;
|
|
4930
4930
|
}
|
|
4931
4931
|
const e = raw;
|
|
4932
|
-
requireNonEmptyString(e["id"], `${
|
|
4933
|
-
requireNonEmptyString(e["description"], `${
|
|
4932
|
+
requireNonEmptyString(e["id"], `${path23}.id`, errors);
|
|
4933
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
4934
4934
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4935
4935
|
errors.push({
|
|
4936
|
-
path: `${
|
|
4936
|
+
path: `${path23}.method`,
|
|
4937
4937
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4938
4938
|
});
|
|
4939
4939
|
}
|
|
4940
4940
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4941
4941
|
errors.push({
|
|
4942
|
-
path: `${
|
|
4942
|
+
path: `${path23}.path`,
|
|
4943
4943
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4944
4944
|
});
|
|
4945
4945
|
}
|
|
4946
4946
|
if (typeof e["auth"] !== "boolean") {
|
|
4947
|
-
errors.push({ path: `${
|
|
4947
|
+
errors.push({ path: `${path23}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4948
4948
|
}
|
|
4949
4949
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4950
4950
|
errors.push({
|
|
4951
|
-
path: `${
|
|
4951
|
+
path: `${path23}.successStatus`,
|
|
4952
4952
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4953
4953
|
});
|
|
4954
4954
|
}
|
|
4955
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4955
|
+
requireNonEmptyString(e["successDescription"], `${path23}.successDescription`, errors);
|
|
4956
4956
|
if (e["request"] !== void 0) {
|
|
4957
|
-
validateRequestSchema(e["request"], `${
|
|
4957
|
+
validateRequestSchema(e["request"], `${path23}.request`, errors);
|
|
4958
4958
|
}
|
|
4959
4959
|
if (e["errors"] !== void 0) {
|
|
4960
4960
|
if (!Array.isArray(e["errors"])) {
|
|
4961
|
-
errors.push({ path: `${
|
|
4961
|
+
errors.push({ path: `${path23}.errors`, message: "Must be an array if present" });
|
|
4962
4962
|
} else {
|
|
4963
4963
|
const errs = e["errors"];
|
|
4964
4964
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4965
|
-
errors.push({ path: `${
|
|
4965
|
+
errors.push({ path: `${path23}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4966
4966
|
}
|
|
4967
4967
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4968
|
-
validateResponseError(errs[j2], `${
|
|
4968
|
+
validateResponseError(errs[j2], `${path23}.errors[${j2}]`, errors);
|
|
4969
4969
|
}
|
|
4970
4970
|
}
|
|
4971
4971
|
}
|
|
4972
4972
|
}
|
|
4973
|
-
function validateRequestSchema(raw,
|
|
4973
|
+
function validateRequestSchema(raw, path23, errors) {
|
|
4974
4974
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4975
|
-
errors.push({ path:
|
|
4975
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4976
4976
|
return;
|
|
4977
4977
|
}
|
|
4978
4978
|
const r = raw;
|
|
4979
4979
|
for (const key of ["body", "query", "params"]) {
|
|
4980
4980
|
if (r[key] !== void 0) {
|
|
4981
|
-
validateFieldMap(r[key], `${
|
|
4981
|
+
validateFieldMap(r[key], `${path23}.${key}`, errors);
|
|
4982
4982
|
}
|
|
4983
4983
|
}
|
|
4984
4984
|
}
|
|
4985
|
-
function validateFieldMap(raw,
|
|
4985
|
+
function validateFieldMap(raw, path23, errors) {
|
|
4986
4986
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4987
|
-
errors.push({ path:
|
|
4987
|
+
errors.push({ path: path23, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4988
4988
|
return;
|
|
4989
4989
|
}
|
|
4990
4990
|
const map = raw;
|
|
4991
4991
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4992
4992
|
if (typeof v2 !== "string") {
|
|
4993
|
-
errors.push({ path: `${
|
|
4993
|
+
errors.push({ path: `${path23}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4994
4994
|
}
|
|
4995
4995
|
}
|
|
4996
4996
|
}
|
|
4997
|
-
function validateResponseError(raw,
|
|
4997
|
+
function validateResponseError(raw, path23, errors) {
|
|
4998
4998
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4999
|
-
errors.push({ path:
|
|
4999
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5000
5000
|
return;
|
|
5001
5001
|
}
|
|
5002
5002
|
const e = raw;
|
|
5003
5003
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5004
|
-
errors.push({ path: `${
|
|
5004
|
+
errors.push({ path: `${path23}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5005
5005
|
}
|
|
5006
|
-
requireNonEmptyString(e["code"], `${
|
|
5007
|
-
requireNonEmptyString(e["description"], `${
|
|
5006
|
+
requireNonEmptyString(e["code"], `${path23}.code`, errors);
|
|
5007
|
+
requireNonEmptyString(e["description"], `${path23}.description`, errors);
|
|
5008
5008
|
}
|
|
5009
|
-
function validateBehavior(raw,
|
|
5009
|
+
function validateBehavior(raw, path23, errors) {
|
|
5010
5010
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5011
|
-
errors.push({ path:
|
|
5011
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5012
5012
|
return;
|
|
5013
5013
|
}
|
|
5014
5014
|
const b = raw;
|
|
5015
|
-
requireNonEmptyString(b["id"], `${
|
|
5016
|
-
requireNonEmptyString(b["description"], `${
|
|
5015
|
+
requireNonEmptyString(b["id"], `${path23}.id`, errors);
|
|
5016
|
+
requireNonEmptyString(b["description"], `${path23}.description`, errors);
|
|
5017
5017
|
if (b["constraints"] !== void 0) {
|
|
5018
5018
|
if (!Array.isArray(b["constraints"])) {
|
|
5019
|
-
errors.push({ path: `${
|
|
5019
|
+
errors.push({ path: `${path23}.constraints`, message: "Must be an array of strings if present" });
|
|
5020
5020
|
} else {
|
|
5021
5021
|
const cs2 = b["constraints"];
|
|
5022
5022
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5023
5023
|
if (typeof cs2[j2] !== "string") {
|
|
5024
|
-
errors.push({ path: `${
|
|
5024
|
+
errors.push({ path: `${path23}.constraints[${j2}]`, message: "Must be a string" });
|
|
5025
5025
|
}
|
|
5026
5026
|
}
|
|
5027
5027
|
}
|
|
5028
5028
|
}
|
|
5029
5029
|
}
|
|
5030
|
-
function validateComponent(raw,
|
|
5030
|
+
function validateComponent(raw, path23, errors) {
|
|
5031
5031
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5032
|
-
errors.push({ path:
|
|
5032
|
+
errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5033
5033
|
return;
|
|
5034
5034
|
}
|
|
5035
5035
|
const c = raw;
|
|
5036
|
-
requireNonEmptyString(c["id"], `${
|
|
5037
|
-
requireNonEmptyString(c["name"], `${
|
|
5038
|
-
requireNonEmptyString(c["description"], `${
|
|
5036
|
+
requireNonEmptyString(c["id"], `${path23}.id`, errors);
|
|
5037
|
+
requireNonEmptyString(c["name"], `${path23}.name`, errors);
|
|
5038
|
+
requireNonEmptyString(c["description"], `${path23}.description`, errors);
|
|
5039
5039
|
if (c["props"] !== void 0) {
|
|
5040
5040
|
if (!Array.isArray(c["props"])) {
|
|
5041
|
-
errors.push({ path: `${
|
|
5041
|
+
errors.push({ path: `${path23}.props`, message: "Must be an array if present" });
|
|
5042
5042
|
} else {
|
|
5043
5043
|
const props = c["props"];
|
|
5044
5044
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5045
5045
|
const p = props[j2];
|
|
5046
5046
|
if (typeof p !== "object" || p === null) {
|
|
5047
|
-
errors.push({ path: `${
|
|
5047
|
+
errors.push({ path: `${path23}.props[${j2}]`, message: "Must be an object" });
|
|
5048
5048
|
continue;
|
|
5049
5049
|
}
|
|
5050
|
-
requireNonEmptyString(p["name"], `${
|
|
5051
|
-
requireNonEmptyString(p["type"], `${
|
|
5050
|
+
requireNonEmptyString(p["name"], `${path23}.props[${j2}].name`, errors);
|
|
5051
|
+
requireNonEmptyString(p["type"], `${path23}.props[${j2}].type`, errors);
|
|
5052
5052
|
if (typeof p["required"] !== "boolean") {
|
|
5053
|
-
errors.push({ path: `${
|
|
5053
|
+
errors.push({ path: `${path23}.props[${j2}].required`, message: "Must be boolean" });
|
|
5054
5054
|
}
|
|
5055
5055
|
}
|
|
5056
5056
|
}
|
|
5057
5057
|
}
|
|
5058
5058
|
if (c["events"] !== void 0) {
|
|
5059
5059
|
if (!Array.isArray(c["events"])) {
|
|
5060
|
-
errors.push({ path: `${
|
|
5060
|
+
errors.push({ path: `${path23}.events`, message: "Must be an array if present" });
|
|
5061
5061
|
} else {
|
|
5062
5062
|
const events = c["events"];
|
|
5063
5063
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5064
5064
|
const e = events[j2];
|
|
5065
5065
|
if (typeof e !== "object" || e === null) {
|
|
5066
|
-
errors.push({ path: `${
|
|
5066
|
+
errors.push({ path: `${path23}.events[${j2}]`, message: "Must be an object" });
|
|
5067
5067
|
continue;
|
|
5068
5068
|
}
|
|
5069
|
-
requireNonEmptyString(e["name"], `${
|
|
5069
|
+
requireNonEmptyString(e["name"], `${path23}.events[${j2}].name`, errors);
|
|
5070
5070
|
}
|
|
5071
5071
|
}
|
|
5072
5072
|
}
|
|
5073
5073
|
if (c["state"] !== void 0) {
|
|
5074
5074
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5075
|
-
errors.push({ path: `${
|
|
5075
|
+
errors.push({ path: `${path23}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5076
5076
|
}
|
|
5077
5077
|
}
|
|
5078
5078
|
if (c["apiCalls"] !== void 0) {
|
|
5079
5079
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5080
|
-
errors.push({ path: `${
|
|
5080
|
+
errors.push({ path: `${path23}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5081
5081
|
}
|
|
5082
5082
|
}
|
|
5083
5083
|
}
|
|
5084
|
-
function requireNonEmptyString(v2,
|
|
5084
|
+
function requireNonEmptyString(v2, path23, errors) {
|
|
5085
5085
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5086
5086
|
errors.push({
|
|
5087
|
-
path:
|
|
5087
|
+
path: path23,
|
|
5088
5088
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5089
5089
|
});
|
|
5090
5090
|
}
|
|
@@ -8339,108 +8339,6 @@ async function clearKey(provider) {
|
|
|
8339
8339
|
await writeStore(store);
|
|
8340
8340
|
}
|
|
8341
8341
|
|
|
8342
|
-
// cli/welcome.ts
|
|
8343
|
-
var import_chalk17 = __toESM(require("chalk"));
|
|
8344
|
-
var os4 = __toESM(require("os"));
|
|
8345
|
-
var path19 = __toESM(require("path"));
|
|
8346
|
-
var fs20 = __toESM(require("fs-extra"));
|
|
8347
|
-
var VERSION = "0.14.1";
|
|
8348
|
-
var TOTAL_W = 76;
|
|
8349
|
-
var L_WIDTH = 44;
|
|
8350
|
-
var R_WIDTH = TOTAL_W - L_WIDTH - 4;
|
|
8351
|
-
var ROBOT_COLOR = import_chalk17.default.hex("#E8885A");
|
|
8352
|
-
var ROBOT = [
|
|
8353
|
-
ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
|
|
8354
|
-
ROBOT_COLOR(" \u2502") + import_chalk17.default.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + import_chalk17.default.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
|
|
8355
|
-
ROBOT_COLOR(" \u2502") + import_chalk17.default.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
|
|
8356
|
-
ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
|
|
8357
|
-
ROBOT_COLOR(" \u2502"),
|
|
8358
|
-
ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
|
|
8359
|
-
];
|
|
8360
|
-
function visLen(s) {
|
|
8361
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
8362
|
-
}
|
|
8363
|
-
function padR(s, width) {
|
|
8364
|
-
const vl = visLen(s);
|
|
8365
|
-
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
8366
|
-
}
|
|
8367
|
-
function row(left, right) {
|
|
8368
|
-
return padR(left, L_WIDTH) + " " + import_chalk17.default.gray("\u2502") + " " + padR(right, R_WIDTH);
|
|
8369
|
-
}
|
|
8370
|
-
function center(s, width) {
|
|
8371
|
-
const vl = visLen(s);
|
|
8372
|
-
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
8373
|
-
return " ".repeat(pad) + s;
|
|
8374
|
-
}
|
|
8375
|
-
async function getRecentSpecs(dir) {
|
|
8376
|
-
const specsDir = path19.join(dir, "specs");
|
|
8377
|
-
if (!await fs20.pathExists(specsDir)) return [];
|
|
8378
|
-
try {
|
|
8379
|
-
const files = await fs20.readdir(specsDir);
|
|
8380
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
8381
|
-
const withStats = await Promise.all(
|
|
8382
|
-
mdFiles.map(async (f) => {
|
|
8383
|
-
const stat4 = await fs20.stat(path19.join(specsDir, f));
|
|
8384
|
-
return { name: f, mtime: stat4.mtime.getTime() };
|
|
8385
|
-
})
|
|
8386
|
-
);
|
|
8387
|
-
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
8388
|
-
return withStats.slice(0, 3).map(({ name, mtime }) => {
|
|
8389
|
-
const ms2 = Date.now() - mtime;
|
|
8390
|
-
const hours = Math.floor(ms2 / 36e5);
|
|
8391
|
-
const days = Math.floor(hours / 24);
|
|
8392
|
-
const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
|
|
8393
|
-
const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
|
|
8394
|
-
return import_chalk17.default.white(slug) + import_chalk17.default.dim(" " + age);
|
|
8395
|
-
});
|
|
8396
|
-
} catch {
|
|
8397
|
-
return [];
|
|
8398
|
-
}
|
|
8399
|
-
}
|
|
8400
|
-
async function printWelcome(currentDir, config2) {
|
|
8401
|
-
const username = os4.userInfo().username;
|
|
8402
|
-
const homeDir = os4.homedir();
|
|
8403
|
-
const shortDir = currentDir.startsWith(homeDir) ? "~" + currentDir.slice(homeDir.length) : currentDir;
|
|
8404
|
-
const recentSpecs = await getRecentSpecs(currentDir);
|
|
8405
|
-
const providerBit = config2?.provider ? config2.provider + (config2.model ? " \xB7 " + config2.model : "") : "";
|
|
8406
|
-
const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
|
|
8407
|
-
const maxInfoLen = L_WIDTH - 2;
|
|
8408
|
-
const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
|
|
8409
|
-
const bottomLine = " " + import_chalk17.default.dim(bottomTruncated);
|
|
8410
|
-
const titleInner = `ai-spec v${VERSION} `;
|
|
8411
|
-
const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
|
|
8412
|
-
console.log(
|
|
8413
|
-
"\n" + import_chalk17.default.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
|
|
8414
|
-
);
|
|
8415
|
-
const welcomeText = "Welcome back, " + import_chalk17.default.bold.white(username) + "!";
|
|
8416
|
-
const leftLines = [
|
|
8417
|
-
"",
|
|
8418
|
-
center(welcomeText, L_WIDTH),
|
|
8419
|
-
"",
|
|
8420
|
-
...ROBOT,
|
|
8421
|
-
"",
|
|
8422
|
-
bottomLine,
|
|
8423
|
-
""
|
|
8424
|
-
];
|
|
8425
|
-
const rightLines = [
|
|
8426
|
-
import_chalk17.default.hex("#FF8C00").bold("Tips for getting started"),
|
|
8427
|
-
import_chalk17.default.gray("\u2500".repeat(R_WIDTH)),
|
|
8428
|
-
import_chalk17.default.white('ai-spec create "feature"'),
|
|
8429
|
-
import_chalk17.default.gray("ai-spec workspace run"),
|
|
8430
|
-
import_chalk17.default.gray('ai-spec update "change"'),
|
|
8431
|
-
"",
|
|
8432
|
-
import_chalk17.default.hex("#FF8C00").bold("Recent activity"),
|
|
8433
|
-
import_chalk17.default.gray("\u2500".repeat(R_WIDTH)),
|
|
8434
|
-
...recentSpecs.length > 0 ? recentSpecs : [import_chalk17.default.dim("No recent activity")]
|
|
8435
|
-
];
|
|
8436
|
-
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
8437
|
-
for (let i = 0; i < maxLines; i++) {
|
|
8438
|
-
console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
|
|
8439
|
-
}
|
|
8440
|
-
console.log(import_chalk17.default.gray("\u2500".repeat(TOTAL_W)));
|
|
8441
|
-
console.log();
|
|
8442
|
-
}
|
|
8443
|
-
|
|
8444
8342
|
// prompts/global-constitution.prompt.ts
|
|
8445
8343
|
var globalConstitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided multi-project context and generate a "Global Constitution" \u2014 a team-level baseline document that captures cross-project rules, shared conventions, and universal constraints that every repository in this workspace must follow.
|
|
8446
8344
|
|
|
@@ -8950,8 +8848,8 @@ Existing API/service files:`);
|
|
|
8950
8848
|
}
|
|
8951
8849
|
|
|
8952
8850
|
// core/mock-server-generator.ts
|
|
8953
|
-
var
|
|
8954
|
-
var
|
|
8851
|
+
var path19 = __toESM(require("path"));
|
|
8852
|
+
var fs20 = __toESM(require("fs-extra"));
|
|
8955
8853
|
var import_child_process5 = require("child_process");
|
|
8956
8854
|
function typeToFixture(fieldName, typeDesc) {
|
|
8957
8855
|
const t = typeDesc.toLowerCase();
|
|
@@ -9087,22 +8985,22 @@ function generateMockServerJs(dsl, port) {
|
|
|
9087
8985
|
}
|
|
9088
8986
|
function detectFrontendFramework(projectDir) {
|
|
9089
8987
|
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
9090
|
-
if (
|
|
8988
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "vite";
|
|
9091
8989
|
}
|
|
9092
8990
|
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
9093
|
-
if (
|
|
8991
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "next";
|
|
9094
8992
|
}
|
|
9095
|
-
const pkgPath =
|
|
9096
|
-
if (
|
|
8993
|
+
const pkgPath = path19.join(projectDir, "package.json");
|
|
8994
|
+
if (fs20.existsSync(pkgPath)) {
|
|
9097
8995
|
try {
|
|
9098
|
-
const pkg = JSON.parse(
|
|
8996
|
+
const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
|
|
9099
8997
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9100
8998
|
if (deps["react-scripts"]) return "cra";
|
|
9101
8999
|
} catch {
|
|
9102
9000
|
}
|
|
9103
9001
|
}
|
|
9104
9002
|
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
9105
|
-
if (
|
|
9003
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return "webpack";
|
|
9106
9004
|
}
|
|
9107
9005
|
return "unknown";
|
|
9108
9006
|
}
|
|
@@ -9282,7 +9180,7 @@ export const worker = setupWorker(...handlers);
|
|
|
9282
9180
|
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
9283
9181
|
function findViteConfigFile(projectDir) {
|
|
9284
9182
|
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
9285
|
-
if (
|
|
9183
|
+
if (fs20.existsSync(path19.join(projectDir, f))) return f;
|
|
9286
9184
|
}
|
|
9287
9185
|
return null;
|
|
9288
9186
|
}
|
|
@@ -9328,73 +9226,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
|
9328
9226
|
if (framework === "vite") {
|
|
9329
9227
|
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
9330
9228
|
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
9331
|
-
const mockConfigPath =
|
|
9332
|
-
await
|
|
9229
|
+
const mockConfigPath = path19.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
9230
|
+
await fs20.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
9333
9231
|
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
9334
|
-
const pkgPath =
|
|
9335
|
-
if (await
|
|
9336
|
-
const pkg = await
|
|
9232
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9233
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9234
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9337
9235
|
pkg.scripts = pkg.scripts ?? {};
|
|
9338
9236
|
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
9339
9237
|
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
9340
|
-
await
|
|
9238
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9341
9239
|
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
9342
9240
|
}
|
|
9343
9241
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9344
|
-
await
|
|
9242
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9345
9243
|
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
9346
9244
|
}
|
|
9347
9245
|
if (framework === "cra") {
|
|
9348
|
-
const pkgPath =
|
|
9349
|
-
if (await
|
|
9350
|
-
const pkg = await
|
|
9246
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9247
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9248
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9351
9249
|
const originalProxy = pkg.proxy ?? null;
|
|
9352
9250
|
pkg.proxy = `http://localhost:${mockPort}`;
|
|
9353
|
-
await
|
|
9251
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9354
9252
|
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
9355
9253
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
9356
|
-
await
|
|
9254
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
9357
9255
|
return { framework, applied: true, devCommand: "npm start" };
|
|
9358
9256
|
}
|
|
9359
9257
|
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
9360
9258
|
}
|
|
9361
9259
|
const lock = { framework, mockPort, frontendDir, actions };
|
|
9362
|
-
await
|
|
9260
|
+
await fs20.writeJson(path19.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
9363
9261
|
const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
|
|
9364
9262
|
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
9365
9263
|
}
|
|
9366
9264
|
async function restoreMockProxy(frontendDir) {
|
|
9367
|
-
const lockPath =
|
|
9368
|
-
if (!await
|
|
9265
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9266
|
+
if (!await fs20.pathExists(lockPath)) {
|
|
9369
9267
|
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
9370
9268
|
}
|
|
9371
|
-
const lock = await
|
|
9269
|
+
const lock = await fs20.readJson(lockPath);
|
|
9372
9270
|
for (const action of lock.actions) {
|
|
9373
9271
|
if (action.type === "wrote-file") {
|
|
9374
|
-
const fp =
|
|
9375
|
-
if (await
|
|
9272
|
+
const fp = path19.join(frontendDir, action.filePath);
|
|
9273
|
+
if (await fs20.pathExists(fp)) await fs20.remove(fp);
|
|
9376
9274
|
} else if (action.type === "added-pkg-script") {
|
|
9377
|
-
const pkgPath =
|
|
9378
|
-
if (await
|
|
9379
|
-
const pkg = await
|
|
9275
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9276
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9277
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9380
9278
|
if (action.originalValue == null) {
|
|
9381
9279
|
delete pkg.scripts?.[action.key];
|
|
9382
9280
|
} else {
|
|
9383
9281
|
pkg.scripts = pkg.scripts ?? {};
|
|
9384
9282
|
pkg.scripts[action.key] = action.originalValue;
|
|
9385
9283
|
}
|
|
9386
|
-
await
|
|
9284
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9387
9285
|
}
|
|
9388
9286
|
} else if (action.type === "patched-pkg-proxy") {
|
|
9389
|
-
const pkgPath =
|
|
9390
|
-
if (await
|
|
9391
|
-
const pkg = await
|
|
9287
|
+
const pkgPath = path19.join(frontendDir, "package.json");
|
|
9288
|
+
if (await fs20.pathExists(pkgPath)) {
|
|
9289
|
+
const pkg = await fs20.readJson(pkgPath);
|
|
9392
9290
|
if (action.originalProxy == null) {
|
|
9393
9291
|
delete pkg.proxy;
|
|
9394
9292
|
} else {
|
|
9395
9293
|
pkg.proxy = action.originalProxy;
|
|
9396
9294
|
}
|
|
9397
|
-
await
|
|
9295
|
+
await fs20.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
9398
9296
|
}
|
|
9399
9297
|
}
|
|
9400
9298
|
}
|
|
@@ -9404,7 +9302,7 @@ async function restoreMockProxy(frontendDir) {
|
|
|
9404
9302
|
} catch {
|
|
9405
9303
|
}
|
|
9406
9304
|
}
|
|
9407
|
-
await
|
|
9305
|
+
await fs20.remove(lockPath);
|
|
9408
9306
|
return { restored: true };
|
|
9409
9307
|
}
|
|
9410
9308
|
function startMockServerBackground(serverJsPath, port) {
|
|
@@ -9417,23 +9315,23 @@ function startMockServerBackground(serverJsPath, port) {
|
|
|
9417
9315
|
return child.pid;
|
|
9418
9316
|
}
|
|
9419
9317
|
async function saveMockServerPid(frontendDir, pid) {
|
|
9420
|
-
const lockPath =
|
|
9421
|
-
if (await
|
|
9422
|
-
const lock = await
|
|
9318
|
+
const lockPath = path19.join(frontendDir, MOCK_LOCK_FILE);
|
|
9319
|
+
if (await fs20.pathExists(lockPath)) {
|
|
9320
|
+
const lock = await fs20.readJson(lockPath);
|
|
9423
9321
|
lock.mockServerPid = pid;
|
|
9424
|
-
await
|
|
9322
|
+
await fs20.writeJson(lockPath, lock, { spaces: 2 });
|
|
9425
9323
|
}
|
|
9426
9324
|
}
|
|
9427
9325
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
9428
9326
|
const port = opts.port ?? 3001;
|
|
9429
|
-
const outputDir =
|
|
9327
|
+
const outputDir = path19.join(projectDir, opts.outputDir ?? "mock");
|
|
9430
9328
|
const result = { files: [] };
|
|
9431
|
-
await
|
|
9329
|
+
await fs20.ensureDir(outputDir);
|
|
9432
9330
|
const serverJs = generateMockServerJs(dsl, port);
|
|
9433
|
-
const serverPath =
|
|
9434
|
-
await
|
|
9331
|
+
const serverPath = path19.join(outputDir, "server.js");
|
|
9332
|
+
await fs20.writeFile(serverPath, serverJs, "utf-8");
|
|
9435
9333
|
result.files.push({
|
|
9436
|
-
path:
|
|
9334
|
+
path: path19.relative(projectDir, serverPath),
|
|
9437
9335
|
description: `Express mock server \u2014 run with: node mock/server.js`
|
|
9438
9336
|
});
|
|
9439
9337
|
const mockReadme = `# Mock Server
|
|
@@ -9463,52 +9361,52 @@ ${dsl.endpoints.map(
|
|
|
9463
9361
|
|
|
9464
9362
|
Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
|
|
9465
9363
|
`;
|
|
9466
|
-
const readmePath =
|
|
9467
|
-
await
|
|
9364
|
+
const readmePath = path19.join(outputDir, "README.md");
|
|
9365
|
+
await fs20.writeFile(readmePath, mockReadme, "utf-8");
|
|
9468
9366
|
result.files.push({
|
|
9469
|
-
path:
|
|
9367
|
+
path: path19.relative(projectDir, readmePath),
|
|
9470
9368
|
description: "Mock server usage guide"
|
|
9471
9369
|
});
|
|
9472
9370
|
if (opts.proxy) {
|
|
9473
9371
|
const { content, filename } = generateProxyConfig(dsl, port, projectDir);
|
|
9474
|
-
const proxyPath =
|
|
9475
|
-
await
|
|
9476
|
-
await
|
|
9372
|
+
const proxyPath = path19.join(projectDir, filename);
|
|
9373
|
+
await fs20.ensureDir(path19.dirname(proxyPath));
|
|
9374
|
+
await fs20.writeFile(proxyPath, content, "utf-8");
|
|
9477
9375
|
result.files.push({
|
|
9478
9376
|
path: filename,
|
|
9479
9377
|
description: "Proxy config snippet \u2014 copy instructions into your framework config"
|
|
9480
9378
|
});
|
|
9481
9379
|
}
|
|
9482
9380
|
if (opts.msw) {
|
|
9483
|
-
const mswDir =
|
|
9484
|
-
await
|
|
9381
|
+
const mswDir = path19.join(projectDir, "src", "mocks");
|
|
9382
|
+
await fs20.ensureDir(mswDir);
|
|
9485
9383
|
const handlersContent = generateMswHandlers(dsl);
|
|
9486
|
-
const handlersPath =
|
|
9487
|
-
await
|
|
9384
|
+
const handlersPath = path19.join(mswDir, "handlers.ts");
|
|
9385
|
+
await fs20.writeFile(handlersPath, handlersContent, "utf-8");
|
|
9488
9386
|
result.files.push({
|
|
9489
|
-
path:
|
|
9387
|
+
path: path19.relative(projectDir, handlersPath),
|
|
9490
9388
|
description: "MSW request handlers"
|
|
9491
9389
|
});
|
|
9492
9390
|
const browserContent = generateMswBrowser();
|
|
9493
|
-
const browserPath =
|
|
9494
|
-
await
|
|
9391
|
+
const browserPath = path19.join(mswDir, "browser.ts");
|
|
9392
|
+
await fs20.writeFile(browserPath, browserContent, "utf-8");
|
|
9495
9393
|
result.files.push({
|
|
9496
|
-
path:
|
|
9394
|
+
path: path19.relative(projectDir, browserPath),
|
|
9497
9395
|
description: "MSW browser worker setup"
|
|
9498
9396
|
});
|
|
9499
9397
|
}
|
|
9500
9398
|
return result;
|
|
9501
9399
|
}
|
|
9502
9400
|
async function findLatestDslFile(projectDir) {
|
|
9503
|
-
const specDir =
|
|
9504
|
-
if (!await
|
|
9401
|
+
const specDir = path19.join(projectDir, ".ai-spec");
|
|
9402
|
+
if (!await fs20.pathExists(specDir)) return null;
|
|
9505
9403
|
const allFiles = [];
|
|
9506
9404
|
async function scan(dir) {
|
|
9507
|
-
const entries = await
|
|
9405
|
+
const entries = await fs20.readdir(dir);
|
|
9508
9406
|
for (const entry of entries) {
|
|
9509
|
-
const abs =
|
|
9510
|
-
const
|
|
9511
|
-
if (
|
|
9407
|
+
const abs = path19.join(dir, entry);
|
|
9408
|
+
const stat3 = await fs20.stat(abs);
|
|
9409
|
+
if (stat3.isDirectory()) {
|
|
9512
9410
|
await scan(abs);
|
|
9513
9411
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9514
9412
|
allFiles.push(abs);
|
|
@@ -9518,16 +9416,16 @@ async function findLatestDslFile(projectDir) {
|
|
|
9518
9416
|
await scan(specDir);
|
|
9519
9417
|
if (allFiles.length === 0) return null;
|
|
9520
9418
|
const withMtimes = await Promise.all(
|
|
9521
|
-
allFiles.map(async (f) => ({ f, mtime: (await
|
|
9419
|
+
allFiles.map(async (f) => ({ f, mtime: (await fs20.stat(f)).mtime }))
|
|
9522
9420
|
);
|
|
9523
9421
|
withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
9524
9422
|
return withMtimes[0].f;
|
|
9525
9423
|
}
|
|
9526
9424
|
|
|
9527
9425
|
// core/spec-updater.ts
|
|
9528
|
-
var
|
|
9529
|
-
var
|
|
9530
|
-
var
|
|
9426
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
9427
|
+
var path20 = __toESM(require("path"));
|
|
9428
|
+
var fs21 = __toESM(require("fs-extra"));
|
|
9531
9429
|
|
|
9532
9430
|
// prompts/update.prompt.ts
|
|
9533
9431
|
var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
|
|
@@ -9673,8 +9571,8 @@ var SpecUpdater = class {
|
|
|
9673
9571
|
* Returns all .md spec files sorted newest-first.
|
|
9674
9572
|
*/
|
|
9675
9573
|
static async findLatestSpec(specsDir) {
|
|
9676
|
-
if (!await
|
|
9677
|
-
const files = await
|
|
9574
|
+
if (!await fs21.pathExists(specsDir)) return null;
|
|
9575
|
+
const files = await fs21.readdir(specsDir);
|
|
9678
9576
|
const pattern = /^feature-(.+)-v(\d+)\.md$/;
|
|
9679
9577
|
let latest = null;
|
|
9680
9578
|
for (const file of files) {
|
|
@@ -9682,8 +9580,8 @@ var SpecUpdater = class {
|
|
|
9682
9580
|
if (!m) continue;
|
|
9683
9581
|
const version = parseInt(m[2], 10);
|
|
9684
9582
|
if (!latest || version > latest.version) {
|
|
9685
|
-
const filePath =
|
|
9686
|
-
const content = await
|
|
9583
|
+
const filePath = path20.join(specsDir, file);
|
|
9584
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
9687
9585
|
latest = { filePath, version, slug: m[1], content };
|
|
9688
9586
|
}
|
|
9689
9587
|
}
|
|
@@ -9694,16 +9592,16 @@ var SpecUpdater = class {
|
|
|
9694
9592
|
* Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
|
|
9695
9593
|
*/
|
|
9696
9594
|
async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
|
|
9697
|
-
const existingSpec = await
|
|
9595
|
+
const existingSpec = await fs21.readFile(existingSpecPath, "utf-8");
|
|
9698
9596
|
let existingDsl = null;
|
|
9699
9597
|
const dslFile = await findLatestDslFile(projectDir);
|
|
9700
9598
|
if (dslFile) {
|
|
9701
9599
|
try {
|
|
9702
|
-
existingDsl = await
|
|
9600
|
+
existingDsl = await fs21.readJson(dslFile);
|
|
9703
9601
|
} catch {
|
|
9704
9602
|
}
|
|
9705
9603
|
}
|
|
9706
|
-
console.log(
|
|
9604
|
+
console.log(import_chalk17.default.blue(" [1/3] Generating updated spec..."));
|
|
9707
9605
|
const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
|
|
9708
9606
|
let updatedSpecContent;
|
|
9709
9607
|
try {
|
|
@@ -9712,15 +9610,15 @@ var SpecUpdater = class {
|
|
|
9712
9610
|
} catch (err) {
|
|
9713
9611
|
throw new Error(`Spec update generation failed: ${err.message}`);
|
|
9714
9612
|
}
|
|
9715
|
-
const specBasename =
|
|
9613
|
+
const specBasename = path20.basename(existingSpecPath);
|
|
9716
9614
|
const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
|
|
9717
9615
|
const slug = slugMatch ? slugMatch[1] : "feature";
|
|
9718
|
-
const specsDir =
|
|
9616
|
+
const specsDir = path20.dirname(existingSpecPath);
|
|
9719
9617
|
const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
|
|
9720
|
-
await
|
|
9721
|
-
await
|
|
9722
|
-
console.log(
|
|
9723
|
-
console.log(
|
|
9618
|
+
await fs21.ensureDir(specsDir);
|
|
9619
|
+
await fs21.writeFile(newSpecPath, updatedSpecContent, "utf-8");
|
|
9620
|
+
console.log(import_chalk17.default.green(` \u2714 New spec written: ${path20.relative(projectDir, newSpecPath)}`));
|
|
9621
|
+
console.log(import_chalk17.default.blue(" [2/3] Updating DSL..."));
|
|
9724
9622
|
let updatedDsl = null;
|
|
9725
9623
|
let newDslPath = null;
|
|
9726
9624
|
if (existingDsl) {
|
|
@@ -9732,7 +9630,7 @@ var SpecUpdater = class {
|
|
|
9732
9630
|
updatedDsl = parsed;
|
|
9733
9631
|
}
|
|
9734
9632
|
} catch {
|
|
9735
|
-
console.log(
|
|
9633
|
+
console.log(import_chalk17.default.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
|
|
9736
9634
|
}
|
|
9737
9635
|
}
|
|
9738
9636
|
if (!updatedDsl) {
|
|
@@ -9741,15 +9639,15 @@ var SpecUpdater = class {
|
|
|
9741
9639
|
}
|
|
9742
9640
|
if (updatedDsl) {
|
|
9743
9641
|
const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
|
|
9744
|
-
await
|
|
9642
|
+
await fs21.writeJson(dslPath, updatedDsl, { spaces: 2 });
|
|
9745
9643
|
newDslPath = dslPath;
|
|
9746
|
-
console.log(
|
|
9644
|
+
console.log(import_chalk17.default.green(` \u2714 DSL updated: ${path20.relative(projectDir, dslPath)}`));
|
|
9747
9645
|
} else {
|
|
9748
|
-
console.log(
|
|
9646
|
+
console.log(import_chalk17.default.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
|
|
9749
9647
|
}
|
|
9750
9648
|
let affectedFiles = [];
|
|
9751
9649
|
if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
|
|
9752
|
-
console.log(
|
|
9650
|
+
console.log(import_chalk17.default.blue(" [3/3] Identifying affected files..."));
|
|
9753
9651
|
const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
|
|
9754
9652
|
const affectedPrompt = buildAffectedFilesPrompt(
|
|
9755
9653
|
changeRequest,
|
|
@@ -9760,9 +9658,9 @@ var SpecUpdater = class {
|
|
|
9760
9658
|
try {
|
|
9761
9659
|
const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
|
|
9762
9660
|
affectedFiles = parseAffectedFiles(affectedRaw);
|
|
9763
|
-
console.log(
|
|
9661
|
+
console.log(import_chalk17.default.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
|
|
9764
9662
|
} catch {
|
|
9765
|
-
console.log(
|
|
9663
|
+
console.log(import_chalk17.default.gray(" Could not identify affected files \u2014 use manual selection."));
|
|
9766
9664
|
}
|
|
9767
9665
|
}
|
|
9768
9666
|
return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
|
|
@@ -9770,8 +9668,8 @@ var SpecUpdater = class {
|
|
|
9770
9668
|
};
|
|
9771
9669
|
|
|
9772
9670
|
// core/openapi-exporter.ts
|
|
9773
|
-
var
|
|
9774
|
-
var
|
|
9671
|
+
var path21 = __toESM(require("path"));
|
|
9672
|
+
var fs22 = __toESM(require("fs-extra"));
|
|
9775
9673
|
function dslTypeToOASchema(typeDesc, fieldName = "") {
|
|
9776
9674
|
const t = typeDesc.toLowerCase();
|
|
9777
9675
|
if (t === "string" || t.includes("string")) {
|
|
@@ -10000,7 +9898,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
10000
9898
|
const format = opts.format ?? "yaml";
|
|
10001
9899
|
const serverUrl = opts.serverUrl ?? "http://localhost:3000";
|
|
10002
9900
|
const defaultName = `openapi.${format}`;
|
|
10003
|
-
const outputPath = opts.outputPath ?
|
|
9901
|
+
const outputPath = opts.outputPath ? path21.isAbsolute(opts.outputPath) ? opts.outputPath : path21.join(projectDir, opts.outputPath) : path21.join(projectDir, defaultName);
|
|
10004
9902
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
10005
9903
|
let content;
|
|
10006
9904
|
if (format === "json") {
|
|
@@ -10008,8 +9906,8 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
10008
9906
|
} else {
|
|
10009
9907
|
content = buildYamlDoc(doc);
|
|
10010
9908
|
}
|
|
10011
|
-
await
|
|
10012
|
-
await
|
|
9909
|
+
await fs22.ensureDir(path21.dirname(outputPath));
|
|
9910
|
+
await fs22.writeFile(outputPath, content, "utf-8");
|
|
10013
9911
|
return outputPath;
|
|
10014
9912
|
}
|
|
10015
9913
|
|
|
@@ -10017,9 +9915,9 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
10017
9915
|
dotenv.config();
|
|
10018
9916
|
var CONFIG_FILE = ".ai-spec.json";
|
|
10019
9917
|
async function loadConfig(dir) {
|
|
10020
|
-
const p =
|
|
10021
|
-
if (await
|
|
10022
|
-
return
|
|
9918
|
+
const p = path22.join(dir, CONFIG_FILE);
|
|
9919
|
+
if (await fs23.pathExists(p)) {
|
|
9920
|
+
return fs23.readJson(p);
|
|
10023
9921
|
}
|
|
10024
9922
|
return {};
|
|
10025
9923
|
}
|
|
@@ -10044,20 +9942,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
10044
9942
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
10045
9943
|
});
|
|
10046
9944
|
await saveKey(providerName, newKey.trim());
|
|
10047
|
-
console.log(
|
|
9945
|
+
console.log(import_chalk18.default.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
10048
9946
|
return newKey.trim();
|
|
10049
9947
|
}
|
|
10050
9948
|
function printBanner(opts) {
|
|
10051
|
-
console.log(
|
|
10052
|
-
console.log(
|
|
10053
|
-
console.log(
|
|
10054
|
-
console.log(
|
|
9949
|
+
console.log(import_chalk18.default.blue("\n" + "\u2500".repeat(52)));
|
|
9950
|
+
console.log(import_chalk18.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
9951
|
+
console.log(import_chalk18.default.blue("\u2500".repeat(52)));
|
|
9952
|
+
console.log(import_chalk18.default.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
10055
9953
|
console.log(
|
|
10056
|
-
|
|
9954
|
+
import_chalk18.default.gray(
|
|
10057
9955
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
10058
9956
|
)
|
|
10059
9957
|
);
|
|
10060
|
-
console.log(
|
|
9958
|
+
console.log(import_chalk18.default.blue("\u2500".repeat(52) + "\n"));
|
|
10061
9959
|
}
|
|
10062
9960
|
var program = new import_commander.Command();
|
|
10063
9961
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -10084,42 +9982,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10084
9982
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10085
9983
|
const workspaceConfig = await workspaceLoader.load();
|
|
10086
9984
|
if (workspaceConfig) {
|
|
10087
|
-
console.log(
|
|
9985
|
+
console.log(import_chalk18.default.cyan(`
|
|
10088
9986
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
10089
|
-
console.log(
|
|
9987
|
+
console.log(import_chalk18.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
10090
9988
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
10091
9989
|
if (opts.serve) {
|
|
10092
|
-
console.log(
|
|
9990
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10093
9991
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
10094
9992
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
10095
9993
|
if (!backendResult) {
|
|
10096
|
-
console.log(
|
|
9994
|
+
console.log(import_chalk18.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
10097
9995
|
} else {
|
|
10098
9996
|
const mockPort = 3001;
|
|
10099
9997
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
10100
|
-
const serverJsPath =
|
|
10101
|
-
console.log(
|
|
9998
|
+
const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9999
|
+
console.log(import_chalk18.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
10102
10000
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
10103
|
-
console.log(
|
|
10001
|
+
console.log(import_chalk18.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
10104
10002
|
if (frontendResult) {
|
|
10105
10003
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
10106
10004
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
10107
10005
|
if (proxyResult.applied) {
|
|
10108
|
-
console.log(
|
|
10109
|
-
console.log(
|
|
10006
|
+
console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
10007
|
+
console.log(import_chalk18.default.bold.cyan(`
|
|
10110
10008
|
Ready! Run your frontend dev server:`));
|
|
10111
|
-
console.log(
|
|
10112
|
-
console.log(
|
|
10113
|
-
console.log(
|
|
10009
|
+
console.log(import_chalk18.default.white(` cd ${frontendResult.repoAbsPath}`));
|
|
10010
|
+
console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
|
|
10011
|
+
console.log(import_chalk18.default.gray(`
|
|
10114
10012
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
10115
10013
|
} else {
|
|
10116
|
-
console.log(
|
|
10117
|
-
if (proxyResult.note) console.log(
|
|
10118
|
-
console.log(
|
|
10014
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
10015
|
+
if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
|
|
10016
|
+
console.log(import_chalk18.default.gray(` Mock server: http://localhost:${mockPort}`));
|
|
10119
10017
|
}
|
|
10120
10018
|
} else {
|
|
10121
|
-
console.log(
|
|
10122
|
-
console.log(
|
|
10019
|
+
console.log(import_chalk18.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
10020
|
+
console.log(import_chalk18.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
10123
10021
|
}
|
|
10124
10022
|
}
|
|
10125
10023
|
}
|
|
@@ -10140,7 +10038,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10140
10038
|
codegenModel: codegenModelName
|
|
10141
10039
|
});
|
|
10142
10040
|
const runId = generateRunId();
|
|
10143
|
-
console.log(
|
|
10041
|
+
console.log(import_chalk18.default.gray(` Run ID: ${runId}`));
|
|
10144
10042
|
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10145
10043
|
setActiveSnapshot(runSnapshot);
|
|
10146
10044
|
const runLogger = new RunLogger(currentDir, runId, {
|
|
@@ -10148,25 +10046,25 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10148
10046
|
model: specModelName
|
|
10149
10047
|
});
|
|
10150
10048
|
setActiveLogger(runLogger);
|
|
10151
|
-
console.log(
|
|
10049
|
+
console.log(import_chalk18.default.blue("[1/6] Loading project context..."));
|
|
10152
10050
|
runLogger.stageStart("context_load");
|
|
10153
10051
|
const loader = new ContextLoader(currentDir);
|
|
10154
10052
|
const context = await loader.loadProjectContext();
|
|
10155
10053
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10156
10054
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10157
|
-
console.log(
|
|
10158
|
-
console.log(
|
|
10159
|
-
console.log(
|
|
10055
|
+
console.log(import_chalk18.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10056
|
+
console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10057
|
+
console.log(import_chalk18.default.gray(` API files : ${context.apiStructure.length} files`));
|
|
10160
10058
|
if (context.schema) {
|
|
10161
|
-
console.log(
|
|
10059
|
+
console.log(import_chalk18.default.gray(` Prisma schema: found`));
|
|
10162
10060
|
}
|
|
10163
10061
|
if (context.constitution) {
|
|
10164
|
-
console.log(
|
|
10062
|
+
console.log(import_chalk18.default.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
10165
10063
|
if (context.constitution.length > 6e3) {
|
|
10166
|
-
console.log(
|
|
10064
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10167
10065
|
}
|
|
10168
10066
|
} else {
|
|
10169
|
-
console.log(
|
|
10067
|
+
console.log(import_chalk18.default.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
10170
10068
|
try {
|
|
10171
10069
|
const constitutionGen = new ConstitutionGenerator(
|
|
10172
10070
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -10174,12 +10072,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10174
10072
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
10175
10073
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
10176
10074
|
context.constitution = constitutionContent;
|
|
10177
|
-
console.log(
|
|
10075
|
+
console.log(import_chalk18.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
10178
10076
|
} catch (err) {
|
|
10179
|
-
console.log(
|
|
10077
|
+
console.log(import_chalk18.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
10180
10078
|
}
|
|
10181
10079
|
}
|
|
10182
|
-
console.log(
|
|
10080
|
+
console.log(import_chalk18.default.blue(`
|
|
10183
10081
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
10184
10082
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10185
10083
|
let initialSpec;
|
|
@@ -10189,30 +10087,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10189
10087
|
if (opts.skipTasks) {
|
|
10190
10088
|
const generator = new SpecGenerator(specProvider);
|
|
10191
10089
|
initialSpec = await generator.generateSpec(idea, context);
|
|
10192
|
-
console.log(
|
|
10090
|
+
console.log(import_chalk18.default.green(" \u2714 Spec generated."));
|
|
10193
10091
|
} else {
|
|
10194
10092
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
10195
10093
|
initialSpec = result.spec;
|
|
10196
10094
|
initialTasks = result.tasks;
|
|
10197
|
-
console.log(
|
|
10095
|
+
console.log(import_chalk18.default.green(` \u2714 Spec generated.`));
|
|
10198
10096
|
if (initialTasks.length > 0) {
|
|
10199
|
-
console.log(
|
|
10097
|
+
console.log(import_chalk18.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
10200
10098
|
} else {
|
|
10201
|
-
console.log(
|
|
10099
|
+
console.log(import_chalk18.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10202
10100
|
}
|
|
10203
10101
|
}
|
|
10204
10102
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10205
10103
|
} catch (err) {
|
|
10206
10104
|
runLogger.stageFail("spec_gen", err.message);
|
|
10207
|
-
console.error(
|
|
10105
|
+
console.error(import_chalk18.default.red(" \u2718 Spec generation failed:"), err);
|
|
10208
10106
|
process.exit(1);
|
|
10209
10107
|
}
|
|
10210
10108
|
let finalSpec;
|
|
10211
10109
|
if (opts.fast) {
|
|
10212
|
-
console.log(
|
|
10110
|
+
console.log(import_chalk18.default.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
10213
10111
|
finalSpec = initialSpec;
|
|
10214
10112
|
} else {
|
|
10215
|
-
console.log(
|
|
10113
|
+
console.log(import_chalk18.default.blue("\n[3/6] Interactive spec refinement..."));
|
|
10216
10114
|
runLogger.stageStart("spec_refine");
|
|
10217
10115
|
const refiner = new SpecRefiner(specProvider);
|
|
10218
10116
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
@@ -10223,7 +10121,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10223
10121
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
10224
10122
|
if (shouldRunAssessment) {
|
|
10225
10123
|
if (!opts.auto) {
|
|
10226
|
-
console.log(
|
|
10124
|
+
console.log(import_chalk18.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10227
10125
|
}
|
|
10228
10126
|
runLogger.stageStart("spec_assess");
|
|
10229
10127
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
@@ -10232,45 +10130,45 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10232
10130
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10233
10131
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10234
10132
|
if (opts.force) {
|
|
10235
|
-
console.log(
|
|
10133
|
+
console.log(import_chalk18.default.yellow(`
|
|
10236
10134
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10237
10135
|
} else {
|
|
10238
10136
|
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10239
|
-
console.log(
|
|
10137
|
+
console.log(import_chalk18.default.red(`
|
|
10240
10138
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10241
10139
|
if (!opts.auto) {
|
|
10242
|
-
console.log(
|
|
10140
|
+
console.log(import_chalk18.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
10243
10141
|
} else {
|
|
10244
|
-
console.log(
|
|
10142
|
+
console.log(import_chalk18.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
10245
10143
|
}
|
|
10246
|
-
console.log(
|
|
10144
|
+
console.log(import_chalk18.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
10247
10145
|
process.exit(1);
|
|
10248
10146
|
}
|
|
10249
10147
|
}
|
|
10250
10148
|
} else {
|
|
10251
10149
|
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10252
10150
|
if (!opts.auto) {
|
|
10253
|
-
console.log(
|
|
10151
|
+
console.log(import_chalk18.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10254
10152
|
}
|
|
10255
10153
|
}
|
|
10256
10154
|
}
|
|
10257
10155
|
if (!opts.auto) {
|
|
10258
|
-
console.log(
|
|
10156
|
+
console.log(import_chalk18.default.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
10259
10157
|
const specLines = finalSpec.split("\n").length;
|
|
10260
10158
|
const specWords = finalSpec.split(/\s+/).length;
|
|
10261
10159
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10262
|
-
console.log(
|
|
10263
|
-
if (taskCountHint) console.log(
|
|
10264
|
-
const previewSpecsDir =
|
|
10160
|
+
console.log(import_chalk18.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10161
|
+
if (taskCountHint) console.log(import_chalk18.default.gray(taskCountHint));
|
|
10162
|
+
const previewSpecsDir = path22.join(currentDir, "specs");
|
|
10265
10163
|
const slug = featureSlug;
|
|
10266
10164
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10267
10165
|
if (prevVersion) {
|
|
10268
|
-
console.log(
|
|
10166
|
+
console.log(import_chalk18.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
10269
10167
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
10270
|
-
console.log(
|
|
10168
|
+
console.log(import_chalk18.default.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10271
10169
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
10272
10170
|
printDiff(diff);
|
|
10273
|
-
console.log(
|
|
10171
|
+
console.log(import_chalk18.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10274
10172
|
}
|
|
10275
10173
|
const gate = await (0, import_prompts3.select)({
|
|
10276
10174
|
message: "Ready to proceed to code generation?",
|
|
@@ -10281,9 +10179,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10281
10179
|
]
|
|
10282
10180
|
});
|
|
10283
10181
|
if (gate === "view") {
|
|
10284
|
-
console.log(
|
|
10182
|
+
console.log(import_chalk18.default.cyan("\n" + "\u2500".repeat(52)));
|
|
10285
10183
|
console.log(finalSpec);
|
|
10286
|
-
console.log(
|
|
10184
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52) + "\n"));
|
|
10287
10185
|
const confirm22 = await (0, import_prompts3.select)({
|
|
10288
10186
|
message: "Proceed to code generation?",
|
|
10289
10187
|
choices: [
|
|
@@ -10292,92 +10190,92 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10292
10190
|
]
|
|
10293
10191
|
});
|
|
10294
10192
|
if (confirm22 === "abort") {
|
|
10295
|
-
console.log(
|
|
10193
|
+
console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10296
10194
|
process.exit(0);
|
|
10297
10195
|
}
|
|
10298
10196
|
} else if (gate === "abort") {
|
|
10299
|
-
console.log(
|
|
10197
|
+
console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
|
|
10300
10198
|
process.exit(0);
|
|
10301
10199
|
}
|
|
10302
|
-
console.log(
|
|
10200
|
+
console.log(import_chalk18.default.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
10303
10201
|
} else {
|
|
10304
|
-
console.log(
|
|
10202
|
+
console.log(import_chalk18.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
10305
10203
|
}
|
|
10306
10204
|
let extractedDsl = null;
|
|
10307
10205
|
if (opts.skipDsl) {
|
|
10308
|
-
console.log(
|
|
10206
|
+
console.log(import_chalk18.default.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
10309
10207
|
} else {
|
|
10310
|
-
console.log(
|
|
10311
|
-
console.log(
|
|
10208
|
+
console.log(import_chalk18.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10209
|
+
console.log(import_chalk18.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10312
10210
|
runLogger.stageStart("dsl_extract");
|
|
10313
10211
|
try {
|
|
10314
10212
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10315
|
-
if (isFrontend) console.log(
|
|
10213
|
+
if (isFrontend) console.log(import_chalk18.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10316
10214
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10317
10215
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10318
10216
|
if (extractedDsl) {
|
|
10319
10217
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10320
|
-
console.log(
|
|
10218
|
+
console.log(import_chalk18.default.green(" \u2714 DSL extracted and validated."));
|
|
10321
10219
|
} else {
|
|
10322
10220
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10323
|
-
console.log(
|
|
10221
|
+
console.log(import_chalk18.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10324
10222
|
}
|
|
10325
10223
|
} catch (err) {
|
|
10326
10224
|
runLogger.stageFail("dsl_extract", err.message);
|
|
10327
|
-
console.log(
|
|
10225
|
+
console.log(import_chalk18.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10328
10226
|
}
|
|
10329
10227
|
}
|
|
10330
10228
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
10331
10229
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
10332
10230
|
let workingDir = currentDir;
|
|
10333
10231
|
if (!skipWorktree) {
|
|
10334
|
-
console.log(
|
|
10232
|
+
console.log(import_chalk18.default.blue("\n[4/6] Setting up git worktree..."));
|
|
10335
10233
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
10336
10234
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10337
10235
|
if (worktreePath) workingDir = worktreePath;
|
|
10338
10236
|
} else {
|
|
10339
10237
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10340
|
-
console.log(
|
|
10238
|
+
console.log(import_chalk18.default.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10341
10239
|
}
|
|
10342
|
-
const specsDir =
|
|
10343
|
-
await
|
|
10240
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10241
|
+
await fs23.ensureDir(specsDir);
|
|
10344
10242
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10345
|
-
await
|
|
10346
|
-
console.log(
|
|
10347
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10243
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10244
|
+
console.log(import_chalk18.default.green(`
|
|
10245
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + import_chalk18.default.gray(` (v${specVersion})`));
|
|
10348
10246
|
let savedDslFile = null;
|
|
10349
10247
|
if (extractedDsl) {
|
|
10350
10248
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10351
10249
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
10352
|
-
console.log(
|
|
10250
|
+
console.log(import_chalk18.default.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
10353
10251
|
}
|
|
10354
10252
|
if (!opts.skipTasks) {
|
|
10355
10253
|
const taskGen = new TaskGenerator(specProvider);
|
|
10356
10254
|
let tasksToSave = initialTasks;
|
|
10357
10255
|
if (tasksToSave.length === 0) {
|
|
10358
|
-
console.log(
|
|
10256
|
+
console.log(import_chalk18.default.blue(`
|
|
10359
10257
|
Generating tasks (separate call)...`));
|
|
10360
10258
|
try {
|
|
10361
10259
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
10362
10260
|
} catch (err) {
|
|
10363
|
-
console.log(
|
|
10261
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
10364
10262
|
}
|
|
10365
10263
|
}
|
|
10366
10264
|
if (tasksToSave.length > 0) {
|
|
10367
10265
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
10368
10266
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
10369
10267
|
printTasks(sorted);
|
|
10370
|
-
console.log(
|
|
10268
|
+
console.log(import_chalk18.default.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
10371
10269
|
} else {
|
|
10372
|
-
console.log(
|
|
10270
|
+
console.log(import_chalk18.default.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
10373
10271
|
}
|
|
10374
10272
|
}
|
|
10375
|
-
console.log(
|
|
10273
|
+
console.log(import_chalk18.default.blue(`
|
|
10376
10274
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
10377
10275
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10378
10276
|
let generatedTestFiles = [];
|
|
10379
10277
|
if (opts.tdd && extractedDsl) {
|
|
10380
|
-
console.log(
|
|
10278
|
+
console.log(import_chalk18.default.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
10381
10279
|
const testGen = new TestGenerator(codegenProvider);
|
|
10382
10280
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10383
10281
|
}
|
|
@@ -10391,13 +10289,13 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10391
10289
|
});
|
|
10392
10290
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10393
10291
|
if (opts.tdd) {
|
|
10394
|
-
console.log(
|
|
10292
|
+
console.log(import_chalk18.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10395
10293
|
} else if (opts.skipTests) {
|
|
10396
|
-
console.log(
|
|
10294
|
+
console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
10397
10295
|
} else if (!extractedDsl) {
|
|
10398
|
-
console.log(
|
|
10296
|
+
console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
10399
10297
|
} else {
|
|
10400
|
-
console.log(
|
|
10298
|
+
console.log(import_chalk18.default.blue(`
|
|
10401
10299
|
[7/9] Test skeleton generation...`));
|
|
10402
10300
|
runLogger.stageStart("test_gen");
|
|
10403
10301
|
const testGen = new TestGenerator(codegenProvider);
|
|
@@ -10405,10 +10303,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10405
10303
|
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10406
10304
|
}
|
|
10407
10305
|
if (opts.skipErrorFeedback) {
|
|
10408
|
-
console.log(
|
|
10306
|
+
console.log(import_chalk18.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10409
10307
|
} else {
|
|
10410
10308
|
if (opts.tdd) {
|
|
10411
|
-
console.log(
|
|
10309
|
+
console.log(import_chalk18.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10412
10310
|
}
|
|
10413
10311
|
runLogger.stageStart("error_feedback");
|
|
10414
10312
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
@@ -10419,10 +10317,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10419
10317
|
}
|
|
10420
10318
|
let reviewResult = "";
|
|
10421
10319
|
if (!opts.skipReview) {
|
|
10422
|
-
console.log(
|
|
10320
|
+
console.log(import_chalk18.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10423
10321
|
runLogger.stageStart("review");
|
|
10424
10322
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10425
|
-
const savedSpec = await
|
|
10323
|
+
const savedSpec = await fs23.readFile(specFile, "utf-8");
|
|
10426
10324
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
10427
10325
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
10428
10326
|
} else {
|
|
@@ -10438,19 +10336,19 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10438
10336
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10439
10337
|
}
|
|
10440
10338
|
runLogger.finish();
|
|
10441
|
-
console.log(
|
|
10442
|
-
console.log(
|
|
10443
|
-
if (savedDslFile) console.log(
|
|
10339
|
+
console.log(import_chalk18.default.bold.green("\n\u2714 All done!"));
|
|
10340
|
+
console.log(import_chalk18.default.gray(` Spec : ${specFile}`));
|
|
10341
|
+
if (savedDslFile) console.log(import_chalk18.default.gray(` DSL : ${savedDslFile}`));
|
|
10444
10342
|
if (generatedTestFiles.length > 0) {
|
|
10445
|
-
console.log(
|
|
10343
|
+
console.log(import_chalk18.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
10446
10344
|
}
|
|
10447
|
-
console.log(
|
|
10345
|
+
console.log(import_chalk18.default.gray(` Working dir : ${workingDir}`));
|
|
10448
10346
|
if (workingDir !== currentDir) {
|
|
10449
|
-
console.log(
|
|
10347
|
+
console.log(import_chalk18.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10450
10348
|
}
|
|
10451
10349
|
runLogger.printSummary();
|
|
10452
10350
|
if (runSnapshot.fileCount > 0) {
|
|
10453
|
-
console.log(
|
|
10351
|
+
console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
10454
10352
|
}
|
|
10455
10353
|
});
|
|
10456
10354
|
program.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
|
|
@@ -10467,24 +10365,24 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10467
10365
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
10468
10366
|
let specContent = "";
|
|
10469
10367
|
let resolvedSpecFile;
|
|
10470
|
-
if (specFile && await
|
|
10471
|
-
specContent = await
|
|
10368
|
+
if (specFile && await fs23.pathExists(specFile)) {
|
|
10369
|
+
specContent = await fs23.readFile(specFile, "utf-8");
|
|
10472
10370
|
resolvedSpecFile = specFile;
|
|
10473
|
-
console.log(
|
|
10371
|
+
console.log(import_chalk18.default.gray(`Using spec: ${specFile}`));
|
|
10474
10372
|
} else {
|
|
10475
|
-
const specsDir =
|
|
10476
|
-
if (await
|
|
10477
|
-
const files = (await
|
|
10373
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
10374
|
+
if (await fs23.pathExists(specsDir)) {
|
|
10375
|
+
const files = (await fs23.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
10478
10376
|
if (files.length > 0) {
|
|
10479
|
-
const latest =
|
|
10480
|
-
specContent = await
|
|
10377
|
+
const latest = path22.join(specsDir, files[0]);
|
|
10378
|
+
specContent = await fs23.readFile(latest, "utf-8");
|
|
10481
10379
|
resolvedSpecFile = latest;
|
|
10482
|
-
console.log(
|
|
10380
|
+
console.log(import_chalk18.default.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10483
10381
|
}
|
|
10484
10382
|
}
|
|
10485
10383
|
}
|
|
10486
10384
|
if (!specContent) {
|
|
10487
|
-
console.log(
|
|
10385
|
+
console.log(import_chalk18.default.yellow("No spec file found. Running review without spec context."));
|
|
10488
10386
|
}
|
|
10489
10387
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10490
10388
|
await reviewer.printScoreTrend();
|
|
@@ -10511,15 +10409,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10511
10409
|
auto: opts.auto
|
|
10512
10410
|
});
|
|
10513
10411
|
if (result.written) {
|
|
10514
|
-
console.log(
|
|
10515
|
-
console.log(
|
|
10516
|
-
console.log(
|
|
10412
|
+
console.log(import_chalk18.default.blue("\n Summary:"));
|
|
10413
|
+
console.log(import_chalk18.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10414
|
+
console.log(import_chalk18.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10517
10415
|
if (result.backupPath) {
|
|
10518
|
-
console.log(
|
|
10416
|
+
console.log(import_chalk18.default.gray(` Backup: ${path22.basename(result.backupPath)}`));
|
|
10519
10417
|
}
|
|
10520
10418
|
}
|
|
10521
10419
|
} catch (err) {
|
|
10522
|
-
console.error(
|
|
10420
|
+
console.error(import_chalk18.default.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10523
10421
|
process.exit(1);
|
|
10524
10422
|
}
|
|
10525
10423
|
return;
|
|
@@ -10527,75 +10425,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10527
10425
|
if (opts.global) {
|
|
10528
10426
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10529
10427
|
if (existing && !opts.force) {
|
|
10530
|
-
console.log(
|
|
10428
|
+
console.log(import_chalk18.default.yellow(`
|
|
10531
10429
|
Global constitution already exists at: ${existing.source}`));
|
|
10532
|
-
console.log(
|
|
10430
|
+
console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
|
|
10533
10431
|
return;
|
|
10534
10432
|
}
|
|
10535
|
-
console.log(
|
|
10536
|
-
console.log(
|
|
10537
|
-
console.log(
|
|
10433
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10434
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
10435
|
+
console.log(import_chalk18.default.gray(" Scanning repos in workspace..."));
|
|
10538
10436
|
const loader = new ContextLoader(currentDir);
|
|
10539
10437
|
const ctx = await loader.loadProjectContext();
|
|
10540
10438
|
const summary = [
|
|
10541
10439
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10542
10440
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10543
10441
|
].join("\n");
|
|
10544
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10442
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
|
|
10545
10443
|
let globalConstitution;
|
|
10546
10444
|
try {
|
|
10547
10445
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10548
10446
|
} catch (err) {
|
|
10549
|
-
console.error(
|
|
10447
|
+
console.error(import_chalk18.default.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10550
10448
|
process.exit(1);
|
|
10551
10449
|
}
|
|
10552
10450
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10553
|
-
console.log(
|
|
10451
|
+
console.log(import_chalk18.default.green(`
|
|
10554
10452
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10555
|
-
console.log(
|
|
10556
|
-
console.log(
|
|
10557
|
-
console.log(
|
|
10558
|
-
console.log(
|
|
10453
|
+
console.log(import_chalk18.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
10454
|
+
console.log(import_chalk18.default.gray(" Project-level rules always override global rules.\n"));
|
|
10455
|
+
console.log(import_chalk18.default.bold(" Preview:"));
|
|
10456
|
+
console.log(import_chalk18.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10559
10457
|
if (globalConstitution.split("\n").length > 12) {
|
|
10560
|
-
console.log(
|
|
10458
|
+
console.log(import_chalk18.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10561
10459
|
}
|
|
10562
10460
|
return;
|
|
10563
10461
|
}
|
|
10564
|
-
const constitutionPath =
|
|
10565
|
-
if (!opts.force && await
|
|
10566
|
-
console.log(
|
|
10462
|
+
const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
|
|
10463
|
+
if (!opts.force && await fs23.pathExists(constitutionPath)) {
|
|
10464
|
+
console.log(import_chalk18.default.yellow(`
|
|
10567
10465
|
${CONSTITUTION_FILE} already exists.`));
|
|
10568
|
-
console.log(
|
|
10569
|
-
console.log(
|
|
10466
|
+
console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
|
|
10467
|
+
console.log(import_chalk18.default.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10570
10468
|
return;
|
|
10571
10469
|
}
|
|
10572
|
-
console.log(
|
|
10573
|
-
console.log(
|
|
10574
|
-
console.log(
|
|
10470
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10471
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
10472
|
+
console.log(import_chalk18.default.gray(" Analyzing codebase..."));
|
|
10575
10473
|
const generator = new ConstitutionGenerator(provider);
|
|
10576
10474
|
let constitution;
|
|
10577
10475
|
try {
|
|
10578
10476
|
constitution = await generator.generate(currentDir);
|
|
10579
10477
|
} catch (err) {
|
|
10580
|
-
console.error(
|
|
10478
|
+
console.error(import_chalk18.default.red(" \u2718 Failed to generate constitution:"), err);
|
|
10581
10479
|
process.exit(1);
|
|
10582
10480
|
}
|
|
10583
10481
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10584
|
-
const globalResult = await loadGlobalConstitution([
|
|
10482
|
+
const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
|
|
10585
10483
|
if (globalResult) {
|
|
10586
|
-
console.log(
|
|
10484
|
+
console.log(import_chalk18.default.cyan(`
|
|
10587
10485
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10588
|
-
console.log(
|
|
10589
|
-
console.log(
|
|
10486
|
+
console.log(import_chalk18.default.gray(" It will be merged with this project constitution at runtime."));
|
|
10487
|
+
console.log(import_chalk18.default.gray(" Project rules take priority over global rules."));
|
|
10590
10488
|
}
|
|
10591
|
-
console.log(
|
|
10489
|
+
console.log(import_chalk18.default.green(`
|
|
10592
10490
|
\u2714 Constitution saved: ${saved}`));
|
|
10593
|
-
console.log(
|
|
10594
|
-
console.log(
|
|
10595
|
-
console.log(
|
|
10596
|
-
console.log(
|
|
10491
|
+
console.log(import_chalk18.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
10492
|
+
console.log(import_chalk18.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
10493
|
+
console.log(import_chalk18.default.bold(" Preview:"));
|
|
10494
|
+
console.log(import_chalk18.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10597
10495
|
if (constitution.split("\n").length > 15) {
|
|
10598
|
-
console.log(
|
|
10496
|
+
console.log(import_chalk18.default.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10599
10497
|
}
|
|
10600
10498
|
});
|
|
10601
10499
|
program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
|
|
@@ -10603,44 +10501,44 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10603
10501
|
"Default code generation mode (claude-code|api|plan)"
|
|
10604
10502
|
).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
10605
10503
|
const currentDir = process.cwd();
|
|
10606
|
-
const configPath =
|
|
10504
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10607
10505
|
if (opts.clearKeys) {
|
|
10608
10506
|
await clearAllKeys();
|
|
10609
|
-
console.log(
|
|
10507
|
+
console.log(import_chalk18.default.green(`\u2714 All saved API keys cleared.`));
|
|
10610
10508
|
return;
|
|
10611
10509
|
}
|
|
10612
10510
|
if (opts.clearKey) {
|
|
10613
10511
|
await clearKey(opts.clearKey);
|
|
10614
|
-
console.log(
|
|
10512
|
+
console.log(import_chalk18.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10615
10513
|
return;
|
|
10616
10514
|
}
|
|
10617
10515
|
if (opts.listKeys) {
|
|
10618
|
-
const store = await
|
|
10516
|
+
const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10619
10517
|
const providers = Object.keys(store);
|
|
10620
10518
|
if (providers.length === 0) {
|
|
10621
|
-
console.log(
|
|
10519
|
+
console.log(import_chalk18.default.gray("No saved API keys."));
|
|
10622
10520
|
} else {
|
|
10623
|
-
console.log(
|
|
10521
|
+
console.log(import_chalk18.default.bold("Saved API keys:"));
|
|
10624
10522
|
for (const p of providers) {
|
|
10625
10523
|
const k2 = store[p];
|
|
10626
|
-
console.log(
|
|
10524
|
+
console.log(import_chalk18.default.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10627
10525
|
}
|
|
10628
|
-
console.log(
|
|
10526
|
+
console.log(import_chalk18.default.gray(`
|
|
10629
10527
|
File: ${KEY_STORE_FILE}`));
|
|
10630
10528
|
}
|
|
10631
10529
|
return;
|
|
10632
10530
|
}
|
|
10633
10531
|
if (opts.reset) {
|
|
10634
|
-
await
|
|
10635
|
-
console.log(
|
|
10532
|
+
await fs23.writeJson(configPath, {}, { spaces: 2 });
|
|
10533
|
+
console.log(import_chalk18.default.green(`\u2714 Config reset: ${configPath}`));
|
|
10636
10534
|
return;
|
|
10637
10535
|
}
|
|
10638
10536
|
const existing = await loadConfig(currentDir);
|
|
10639
10537
|
if (opts.show) {
|
|
10640
10538
|
if (Object.keys(existing).length === 0) {
|
|
10641
|
-
console.log(
|
|
10539
|
+
console.log(import_chalk18.default.gray("No config file found. Using built-in defaults."));
|
|
10642
10540
|
} else {
|
|
10643
|
-
console.log(
|
|
10541
|
+
console.log(import_chalk18.default.bold(`${configPath}:`));
|
|
10644
10542
|
console.log(JSON.stringify(existing, null, 2));
|
|
10645
10543
|
}
|
|
10646
10544
|
return;
|
|
@@ -10654,27 +10552,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10654
10552
|
if (opts.minSpecScore !== void 0) {
|
|
10655
10553
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10656
10554
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10657
|
-
console.error(
|
|
10555
|
+
console.error(import_chalk18.default.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10658
10556
|
process.exit(1);
|
|
10659
10557
|
}
|
|
10660
10558
|
updated.minSpecScore = score;
|
|
10661
10559
|
}
|
|
10662
|
-
await
|
|
10663
|
-
console.log(
|
|
10560
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10561
|
+
console.log(import_chalk18.default.green(`\u2714 Config saved to ${configPath}`));
|
|
10664
10562
|
console.log(JSON.stringify(updated, null, 2));
|
|
10665
10563
|
});
|
|
10666
10564
|
program.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
|
|
10667
10565
|
const currentDir = process.cwd();
|
|
10668
|
-
const configPath =
|
|
10566
|
+
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10669
10567
|
if (opts.list) {
|
|
10670
|
-
console.log(
|
|
10568
|
+
console.log(import_chalk18.default.bold("\nAvailable providers & models:\n"));
|
|
10671
10569
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10672
10570
|
console.log(
|
|
10673
|
-
` ${
|
|
10571
|
+
` ${import_chalk18.default.bold.cyan(key.padEnd(10))} ${import_chalk18.default.white(meta.displayName)}`
|
|
10674
10572
|
);
|
|
10675
|
-
console.log(
|
|
10573
|
+
console.log(import_chalk18.default.gray(` ${meta.description}`));
|
|
10676
10574
|
console.log(
|
|
10677
|
-
|
|
10575
|
+
import_chalk18.default.gray(
|
|
10678
10576
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10679
10577
|
)
|
|
10680
10578
|
);
|
|
@@ -10683,10 +10581,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10683
10581
|
return;
|
|
10684
10582
|
}
|
|
10685
10583
|
const existing = await loadConfig(currentDir);
|
|
10686
|
-
console.log(
|
|
10584
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10687
10585
|
if (Object.keys(existing).length > 0) {
|
|
10688
10586
|
console.log(
|
|
10689
|
-
|
|
10587
|
+
import_chalk18.default.gray(
|
|
10690
10588
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10691
10589
|
)
|
|
10692
10590
|
);
|
|
@@ -10704,7 +10602,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10704
10602
|
const providerKey = await (0, import_prompts3.select)({
|
|
10705
10603
|
message: `${label} \u2014 select provider:`,
|
|
10706
10604
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10707
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
10605
|
+
name: `${meta2.displayName.padEnd(22)} ${import_chalk18.default.gray(meta2.description)}`,
|
|
10708
10606
|
value: key,
|
|
10709
10607
|
short: meta2.displayName
|
|
10710
10608
|
}))
|
|
@@ -10712,7 +10610,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10712
10610
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10713
10611
|
const modelChoices = [
|
|
10714
10612
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10715
|
-
{ name:
|
|
10613
|
+
{ name: import_chalk18.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10716
10614
|
];
|
|
10717
10615
|
let chosenModel = await (0, import_prompts3.select)({
|
|
10718
10616
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10746,37 +10644,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10746
10644
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10747
10645
|
updated.codegen = "api";
|
|
10748
10646
|
console.log(
|
|
10749
|
-
|
|
10647
|
+
import_chalk18.default.yellow(
|
|
10750
10648
|
`
|
|
10751
10649
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10752
10650
|
)
|
|
10753
10651
|
);
|
|
10754
|
-
console.log(
|
|
10652
|
+
console.log(import_chalk18.default.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10755
10653
|
}
|
|
10756
10654
|
}
|
|
10757
10655
|
}
|
|
10758
|
-
console.log(
|
|
10759
|
-
console.log(
|
|
10656
|
+
console.log(import_chalk18.default.blue("\n Preview:"));
|
|
10657
|
+
console.log(import_chalk18.default.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10760
10658
|
if (updated.codegenProvider) {
|
|
10761
10659
|
console.log(
|
|
10762
|
-
|
|
10660
|
+
import_chalk18.default.gray(
|
|
10763
10661
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10764
10662
|
)
|
|
10765
10663
|
);
|
|
10766
10664
|
}
|
|
10767
10665
|
const ok = await (0, import_prompts3.confirm)({ message: "Save to .ai-spec.json?", default: true });
|
|
10768
10666
|
if (!ok) {
|
|
10769
|
-
console.log(
|
|
10667
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
10770
10668
|
return;
|
|
10771
10669
|
}
|
|
10772
|
-
await
|
|
10773
|
-
console.log(
|
|
10670
|
+
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10671
|
+
console.log(import_chalk18.default.green(`
|
|
10774
10672
|
\u2714 Saved to ${configPath}`));
|
|
10775
10673
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10776
10674
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10777
10675
|
if (envKey && !process.env[envKey]) {
|
|
10778
10676
|
console.log(
|
|
10779
|
-
|
|
10677
|
+
import_chalk18.default.yellow(
|
|
10780
10678
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10781
10679
|
)
|
|
10782
10680
|
);
|
|
@@ -10795,29 +10693,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10795
10693
|
cliOpts,
|
|
10796
10694
|
contractContextSection
|
|
10797
10695
|
} = opts;
|
|
10798
|
-
console.log(
|
|
10696
|
+
console.log(import_chalk18.default.blue(`
|
|
10799
10697
|
[${repoName}] Loading project context...`));
|
|
10800
10698
|
const loader = new ContextLoader(repoAbsPath);
|
|
10801
10699
|
let context = await loader.loadProjectContext();
|
|
10802
10700
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10803
|
-
console.log(
|
|
10804
|
-
console.log(
|
|
10701
|
+
console.log(import_chalk18.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10702
|
+
console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10805
10703
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10806
|
-
console.log(
|
|
10704
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10807
10705
|
}
|
|
10808
10706
|
if (!context.constitution) {
|
|
10809
|
-
console.log(
|
|
10707
|
+
console.log(import_chalk18.default.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10810
10708
|
try {
|
|
10811
10709
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10812
10710
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10813
10711
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10814
10712
|
context.constitution = constitutionContent;
|
|
10815
|
-
console.log(
|
|
10713
|
+
console.log(import_chalk18.default.green(` Constitution: generated`));
|
|
10816
10714
|
} catch (err) {
|
|
10817
|
-
console.log(
|
|
10715
|
+
console.log(import_chalk18.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10818
10716
|
}
|
|
10819
10717
|
} else {
|
|
10820
|
-
console.log(
|
|
10718
|
+
console.log(import_chalk18.default.green(` Constitution: found`));
|
|
10821
10719
|
}
|
|
10822
10720
|
let fullIdea = idea;
|
|
10823
10721
|
if (contractContextSection) {
|
|
@@ -10825,58 +10723,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10825
10723
|
|
|
10826
10724
|
${contractContextSection}`;
|
|
10827
10725
|
}
|
|
10828
|
-
console.log(
|
|
10726
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Generating spec...`));
|
|
10829
10727
|
let finalSpec;
|
|
10830
10728
|
try {
|
|
10831
10729
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10832
10730
|
finalSpec = result.spec;
|
|
10833
|
-
console.log(
|
|
10731
|
+
console.log(import_chalk18.default.green(` Spec generated.`));
|
|
10834
10732
|
} catch (err) {
|
|
10835
|
-
console.error(
|
|
10733
|
+
console.error(import_chalk18.default.red(` Spec generation failed: ${err.message}`));
|
|
10836
10734
|
return { dsl: null, specFile: null };
|
|
10837
10735
|
}
|
|
10838
10736
|
let extractedDsl = null;
|
|
10839
10737
|
if (!cliOpts.skipDsl) {
|
|
10840
|
-
console.log(
|
|
10738
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Extracting DSL...`));
|
|
10841
10739
|
try {
|
|
10842
10740
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10843
10741
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10844
10742
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10845
10743
|
if (extractedDsl) {
|
|
10846
|
-
console.log(
|
|
10744
|
+
console.log(import_chalk18.default.green(` DSL extracted.`));
|
|
10847
10745
|
}
|
|
10848
10746
|
} catch (err) {
|
|
10849
|
-
console.log(
|
|
10747
|
+
console.log(import_chalk18.default.yellow(` DSL extraction failed: ${err.message}`));
|
|
10850
10748
|
}
|
|
10851
10749
|
}
|
|
10852
10750
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10853
10751
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10854
10752
|
let workingDir = repoAbsPath;
|
|
10855
10753
|
if (!skipWorktreeForRepo) {
|
|
10856
|
-
console.log(
|
|
10754
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Setting up git worktree...`));
|
|
10857
10755
|
try {
|
|
10858
10756
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10859
10757
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10860
10758
|
if (worktreePath) workingDir = worktreePath;
|
|
10861
10759
|
} catch (err) {
|
|
10862
|
-
console.log(
|
|
10760
|
+
console.log(import_chalk18.default.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10863
10761
|
}
|
|
10864
10762
|
} else {
|
|
10865
|
-
console.log(
|
|
10763
|
+
console.log(import_chalk18.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10866
10764
|
}
|
|
10867
|
-
const specsDir =
|
|
10868
|
-
await
|
|
10765
|
+
const specsDir = path22.join(workingDir, "specs");
|
|
10766
|
+
await fs23.ensureDir(specsDir);
|
|
10869
10767
|
const featureSlug = slugify(idea);
|
|
10870
10768
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10871
|
-
await
|
|
10872
|
-
console.log(
|
|
10769
|
+
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10770
|
+
console.log(import_chalk18.default.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
|
|
10873
10771
|
let savedDslFile = null;
|
|
10874
10772
|
if (extractedDsl) {
|
|
10875
10773
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10876
10774
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10877
|
-
console.log(
|
|
10775
|
+
console.log(import_chalk18.default.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
|
|
10878
10776
|
}
|
|
10879
|
-
console.log(
|
|
10777
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10880
10778
|
try {
|
|
10881
10779
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10882
10780
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10884,29 +10782,29 @@ ${contractContextSection}`;
|
|
|
10884
10782
|
dslFilePath: savedDslFile ?? void 0,
|
|
10885
10783
|
repoType: detectedRepoType
|
|
10886
10784
|
});
|
|
10887
|
-
console.log(
|
|
10785
|
+
console.log(import_chalk18.default.green(` Code generation complete.`));
|
|
10888
10786
|
} catch (err) {
|
|
10889
|
-
console.log(
|
|
10787
|
+
console.log(import_chalk18.default.yellow(` Code generation failed: ${err.message}`));
|
|
10890
10788
|
}
|
|
10891
10789
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10892
|
-
console.log(
|
|
10790
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Generating test skeletons...`));
|
|
10893
10791
|
try {
|
|
10894
10792
|
const testGen = new TestGenerator(codegenProvider);
|
|
10895
10793
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10896
|
-
console.log(
|
|
10794
|
+
console.log(import_chalk18.default.green(` ${testFiles.length} test file(s) generated.`));
|
|
10897
10795
|
} catch (err) {
|
|
10898
|
-
console.log(
|
|
10796
|
+
console.log(import_chalk18.default.yellow(` Test generation failed: ${err.message}`));
|
|
10899
10797
|
}
|
|
10900
10798
|
}
|
|
10901
10799
|
if (!cliOpts.skipErrorFeedback) {
|
|
10902
10800
|
try {
|
|
10903
10801
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10904
10802
|
} catch (err) {
|
|
10905
|
-
console.log(
|
|
10803
|
+
console.log(import_chalk18.default.yellow(` Error feedback failed: ${err.message}`));
|
|
10906
10804
|
}
|
|
10907
10805
|
}
|
|
10908
10806
|
if (!cliOpts.skipReview) {
|
|
10909
|
-
console.log(
|
|
10807
|
+
console.log(import_chalk18.default.blue(` [${repoName}] Running code review...`));
|
|
10910
10808
|
try {
|
|
10911
10809
|
const reviewer = new CodeReviewer(specProvider);
|
|
10912
10810
|
const originalDir = process.cwd();
|
|
@@ -10918,9 +10816,9 @@ ${contractContextSection}`;
|
|
|
10918
10816
|
process.chdir(originalDir);
|
|
10919
10817
|
}
|
|
10920
10818
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10921
|
-
console.log(
|
|
10819
|
+
console.log(import_chalk18.default.green(` Code review complete.`));
|
|
10922
10820
|
} catch (err) {
|
|
10923
|
-
console.log(
|
|
10821
|
+
console.log(import_chalk18.default.yellow(` Code review failed: ${err.message}`));
|
|
10924
10822
|
}
|
|
10925
10823
|
}
|
|
10926
10824
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10943,7 +10841,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10943
10841
|
codegenModel: codegenModelName
|
|
10944
10842
|
});
|
|
10945
10843
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10946
|
-
console.log(
|
|
10844
|
+
console.log(import_chalk18.default.blue("\n[W1] Loading per-repo contexts..."));
|
|
10947
10845
|
const contexts = /* @__PURE__ */ new Map();
|
|
10948
10846
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10949
10847
|
for (const repo of workspace.repos) {
|
|
@@ -10955,27 +10853,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10955
10853
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10956
10854
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10957
10855
|
frontendContexts.set(repo.name, fctx);
|
|
10958
|
-
console.log(
|
|
10856
|
+
console.log(import_chalk18.default.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10959
10857
|
} else {
|
|
10960
|
-
console.log(
|
|
10858
|
+
console.log(import_chalk18.default.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10961
10859
|
}
|
|
10962
10860
|
} catch (err) {
|
|
10963
|
-
console.log(
|
|
10861
|
+
console.log(import_chalk18.default.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10964
10862
|
}
|
|
10965
10863
|
}
|
|
10966
|
-
console.log(
|
|
10864
|
+
console.log(import_chalk18.default.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10967
10865
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10968
10866
|
let decomposition;
|
|
10969
10867
|
try {
|
|
10970
10868
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10971
|
-
console.log(
|
|
10972
|
-
console.log(
|
|
10869
|
+
console.log(import_chalk18.default.green(` Summary: ${decomposition.summary}`));
|
|
10870
|
+
console.log(import_chalk18.default.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10973
10871
|
if (decomposition.coordinationNotes) {
|
|
10974
|
-
console.log(
|
|
10872
|
+
console.log(import_chalk18.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10975
10873
|
}
|
|
10976
10874
|
} catch (err) {
|
|
10977
|
-
console.error(
|
|
10978
|
-
console.log(
|
|
10875
|
+
console.error(import_chalk18.default.red(` Decomposition failed: ${err.message}`));
|
|
10876
|
+
console.log(import_chalk18.default.yellow(" Falling back to running all repos independently."));
|
|
10979
10877
|
decomposition = {
|
|
10980
10878
|
originalRequirement: idea,
|
|
10981
10879
|
summary: idea,
|
|
@@ -10991,11 +10889,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10991
10889
|
};
|
|
10992
10890
|
}
|
|
10993
10891
|
if (!opts.auto) {
|
|
10994
|
-
console.log(
|
|
10995
|
-
console.log(
|
|
10892
|
+
console.log(import_chalk18.default.cyan("\n[W3] Decomposition Preview:"));
|
|
10893
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
|
|
10996
10894
|
for (const r of decomposition.repos) {
|
|
10997
|
-
console.log(
|
|
10998
|
-
console.log(
|
|
10895
|
+
console.log(import_chalk18.default.bold(` ${r.repoName} (${r.role})`));
|
|
10896
|
+
console.log(import_chalk18.default.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
10999
10897
|
if (r.uxDecisions) {
|
|
11000
10898
|
const ux = r.uxDecisions;
|
|
11001
10899
|
const uxSummary = [
|
|
@@ -11004,13 +10902,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11004
10902
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
11005
10903
|
ux.errorRollback ? "rollback" : ""
|
|
11006
10904
|
].filter(Boolean).join(", ");
|
|
11007
|
-
if (uxSummary) console.log(
|
|
10905
|
+
if (uxSummary) console.log(import_chalk18.default.cyan(` UX: ${uxSummary}`));
|
|
11008
10906
|
}
|
|
11009
10907
|
if (r.dependsOnRepos.length > 0) {
|
|
11010
|
-
console.log(
|
|
10908
|
+
console.log(import_chalk18.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
11011
10909
|
}
|
|
11012
10910
|
}
|
|
11013
|
-
console.log(
|
|
10911
|
+
console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
|
|
11014
10912
|
const gate = await (0, import_prompts3.select)({
|
|
11015
10913
|
message: "Proceed with multi-repo pipeline?",
|
|
11016
10914
|
choices: [
|
|
@@ -11019,24 +10917,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11019
10917
|
]
|
|
11020
10918
|
});
|
|
11021
10919
|
if (gate === "abort") {
|
|
11022
|
-
console.log(
|
|
10920
|
+
console.log(import_chalk18.default.yellow(" Aborted."));
|
|
11023
10921
|
process.exit(0);
|
|
11024
10922
|
}
|
|
11025
10923
|
}
|
|
11026
10924
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
11027
10925
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
11028
|
-
console.log(
|
|
10926
|
+
console.log(import_chalk18.default.blue(`
|
|
11029
10927
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
11030
10928
|
const results = [];
|
|
11031
10929
|
for (const repoReq of sortedRepoRequirements) {
|
|
11032
10930
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
11033
10931
|
if (!repoConfig) {
|
|
11034
|
-
console.log(
|
|
10932
|
+
console.log(import_chalk18.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
11035
10933
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
11036
10934
|
continue;
|
|
11037
10935
|
}
|
|
11038
10936
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
11039
|
-
console.log(
|
|
10937
|
+
console.log(import_chalk18.default.bold.blue(`
|
|
11040
10938
|
\u2500\u2500 ${repoReq.repoName} (${repoReq.role}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
11041
10939
|
let contractContextSection;
|
|
11042
10940
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -11044,7 +10942,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11044
10942
|
for (const depName of repoReq.dependsOnRepos) {
|
|
11045
10943
|
const depDsl = contractDsls.get(depName);
|
|
11046
10944
|
if (depDsl) {
|
|
11047
|
-
console.log(
|
|
10945
|
+
console.log(import_chalk18.default.gray(` Using API contract from: ${depName}`));
|
|
11048
10946
|
const contract = buildFrontendApiContract(depDsl);
|
|
11049
10947
|
contractParts.push(buildContractContextSection(contract));
|
|
11050
10948
|
}
|
|
@@ -11064,7 +10962,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11064
10962
|
frontendContext: frontendCtx
|
|
11065
10963
|
});
|
|
11066
10964
|
contractContextSection = void 0;
|
|
11067
|
-
console.log(
|
|
10965
|
+
console.log(import_chalk18.default.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
11068
10966
|
}
|
|
11069
10967
|
try {
|
|
11070
10968
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -11081,22 +10979,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11081
10979
|
});
|
|
11082
10980
|
if (repoReq.isContractProvider && dsl) {
|
|
11083
10981
|
contractDsls.set(repoReq.repoName, dsl);
|
|
11084
|
-
console.log(
|
|
10982
|
+
console.log(import_chalk18.default.green(` Contract stored for downstream repos.`));
|
|
11085
10983
|
}
|
|
11086
10984
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
11087
|
-
console.log(
|
|
10985
|
+
console.log(import_chalk18.default.green(` \u2714 ${repoReq.repoName} complete`));
|
|
11088
10986
|
} catch (err) {
|
|
11089
|
-
console.error(
|
|
10987
|
+
console.error(import_chalk18.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
11090
10988
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
11091
10989
|
}
|
|
11092
10990
|
}
|
|
11093
|
-
console.log(
|
|
11094
|
-
console.log(
|
|
11095
|
-
console.log(
|
|
10991
|
+
console.log(import_chalk18.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
10992
|
+
console.log(import_chalk18.default.gray(` Workspace: ${workspace.name}`));
|
|
10993
|
+
console.log(import_chalk18.default.gray(` Requirement: ${idea}`));
|
|
11096
10994
|
console.log();
|
|
11097
10995
|
for (const r of results) {
|
|
11098
|
-
const icon = r.status === "success" ?
|
|
11099
|
-
const specInfo = r.specFile ?
|
|
10996
|
+
const icon = r.status === "success" ? import_chalk18.default.green("\u2714") : r.status === "failed" ? import_chalk18.default.red("\u2718") : import_chalk18.default.gray("\u2212");
|
|
10997
|
+
const specInfo = r.specFile ? import_chalk18.default.gray(` \u2192 ${r.specFile}`) : "";
|
|
11100
10998
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
11101
10999
|
}
|
|
11102
11000
|
return results;
|
|
@@ -11104,18 +11002,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
11104
11002
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
11105
11003
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
11106
11004
|
const currentDir = process.cwd();
|
|
11107
|
-
const configPath =
|
|
11108
|
-
if (await
|
|
11005
|
+
const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
11006
|
+
if (await fs23.pathExists(configPath)) {
|
|
11109
11007
|
const overwrite = await (0, import_prompts3.confirm)({
|
|
11110
11008
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
11111
11009
|
default: false
|
|
11112
11010
|
});
|
|
11113
11011
|
if (!overwrite) {
|
|
11114
|
-
console.log(
|
|
11012
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
11115
11013
|
return;
|
|
11116
11014
|
}
|
|
11117
11015
|
}
|
|
11118
|
-
console.log(
|
|
11016
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11119
11017
|
const workspaceName = await (0, import_prompts3.input)({
|
|
11120
11018
|
message: "Workspace name:",
|
|
11121
11019
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -11129,11 +11027,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11129
11027
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11130
11028
|
const detected = await workspaceLoader.autoDetect();
|
|
11131
11029
|
if (detected.length === 0) {
|
|
11132
|
-
console.log(
|
|
11030
|
+
console.log(import_chalk18.default.yellow(" No recognizable repos found in sibling directories."));
|
|
11133
11031
|
} else {
|
|
11134
|
-
console.log(
|
|
11032
|
+
console.log(import_chalk18.default.cyan("\n Detected repos:"));
|
|
11135
11033
|
for (const r of detected) {
|
|
11136
|
-
console.log(
|
|
11034
|
+
console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11137
11035
|
}
|
|
11138
11036
|
const keepAll = await (0, import_prompts3.confirm)({
|
|
11139
11037
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -11150,7 +11048,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11150
11048
|
if (keep) repos.push(r);
|
|
11151
11049
|
}
|
|
11152
11050
|
}
|
|
11153
|
-
console.log(
|
|
11051
|
+
console.log(import_chalk18.default.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
11154
11052
|
}
|
|
11155
11053
|
}
|
|
11156
11054
|
const repoTypeChoices = [
|
|
@@ -11172,7 +11070,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11172
11070
|
default: repos.length === 0
|
|
11173
11071
|
});
|
|
11174
11072
|
while (addMore) {
|
|
11175
|
-
console.log(
|
|
11073
|
+
console.log(import_chalk18.default.cyan(`
|
|
11176
11074
|
Adding repo #${repos.length + 1}`));
|
|
11177
11075
|
const repoName = await (0, import_prompts3.input)({
|
|
11178
11076
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -11186,16 +11084,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11186
11084
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
11187
11085
|
default: `./${repoName}`
|
|
11188
11086
|
});
|
|
11189
|
-
const absPath =
|
|
11087
|
+
const absPath = path22.resolve(currentDir, repoPath);
|
|
11190
11088
|
let detectedType = "unknown";
|
|
11191
11089
|
let detectedRole = "shared";
|
|
11192
|
-
if (await
|
|
11090
|
+
if (await fs23.pathExists(absPath)) {
|
|
11193
11091
|
const { type, role } = await detectRepoType(absPath);
|
|
11194
11092
|
detectedType = type;
|
|
11195
11093
|
detectedRole = role;
|
|
11196
|
-
console.log(
|
|
11094
|
+
console.log(import_chalk18.default.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
11197
11095
|
} else {
|
|
11198
|
-
console.log(
|
|
11096
|
+
console.log(import_chalk18.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
11199
11097
|
}
|
|
11200
11098
|
const repoType = await (0, import_prompts3.select)({
|
|
11201
11099
|
message: `Repo type for "${repoName}":`,
|
|
@@ -11218,53 +11116,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11218
11116
|
type: repoType,
|
|
11219
11117
|
role: repoRole
|
|
11220
11118
|
});
|
|
11221
|
-
console.log(
|
|
11119
|
+
console.log(import_chalk18.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
11222
11120
|
addMore = await (0, import_prompts3.confirm)({
|
|
11223
11121
|
message: "Add another repo?",
|
|
11224
11122
|
default: false
|
|
11225
11123
|
});
|
|
11226
11124
|
}
|
|
11227
11125
|
const workspaceConfig = { name: workspaceName, repos };
|
|
11228
|
-
console.log(
|
|
11229
|
-
console.log(
|
|
11126
|
+
console.log(import_chalk18.default.cyan("\n Workspace summary:"));
|
|
11127
|
+
console.log(import_chalk18.default.gray(` Name: ${workspaceName}`));
|
|
11230
11128
|
for (const r of repos) {
|
|
11231
|
-
console.log(
|
|
11129
|
+
console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11232
11130
|
}
|
|
11233
11131
|
const ok = await (0, import_prompts3.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
11234
11132
|
if (!ok) {
|
|
11235
|
-
console.log(
|
|
11133
|
+
console.log(import_chalk18.default.gray(" Cancelled."));
|
|
11236
11134
|
return;
|
|
11237
11135
|
}
|
|
11238
11136
|
const loader = new WorkspaceLoader(currentDir);
|
|
11239
11137
|
const saved = await loader.save(workspaceConfig);
|
|
11240
|
-
console.log(
|
|
11138
|
+
console.log(import_chalk18.default.green(`
|
|
11241
11139
|
\u2714 Workspace saved: ${saved}`));
|
|
11242
|
-
console.log(
|
|
11140
|
+
console.log(import_chalk18.default.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
11243
11141
|
});
|
|
11244
11142
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
11245
11143
|
const currentDir = process.cwd();
|
|
11246
11144
|
const loader = new WorkspaceLoader(currentDir);
|
|
11247
11145
|
const config2 = await loader.load();
|
|
11248
11146
|
if (!config2) {
|
|
11249
|
-
console.log(
|
|
11250
|
-
console.log(
|
|
11147
|
+
console.log(import_chalk18.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11148
|
+
console.log(import_chalk18.default.gray(" Run `ai-spec workspace init` to create one."));
|
|
11251
11149
|
return;
|
|
11252
11150
|
}
|
|
11253
|
-
console.log(
|
|
11151
|
+
console.log(import_chalk18.default.bold(`
|
|
11254
11152
|
Workspace: ${config2.name}`));
|
|
11255
|
-
console.log(
|
|
11256
|
-
console.log(
|
|
11153
|
+
console.log(import_chalk18.default.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11154
|
+
console.log(import_chalk18.default.gray(` Repos (${config2.repos.length}):
|
|
11257
11155
|
`));
|
|
11258
11156
|
for (const repo of config2.repos) {
|
|
11259
11157
|
const absPath = loader.resolveAbsPath(repo);
|
|
11260
|
-
const exists = await
|
|
11261
|
-
const status = exists ?
|
|
11158
|
+
const exists = await fs23.pathExists(absPath);
|
|
11159
|
+
const status = exists ? import_chalk18.default.green("found") : import_chalk18.default.red("not found");
|
|
11262
11160
|
console.log(
|
|
11263
|
-
` ${
|
|
11161
|
+
` ${import_chalk18.default.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
11264
11162
|
);
|
|
11265
|
-
console.log(
|
|
11163
|
+
console.log(import_chalk18.default.gray(` path: ${absPath}`));
|
|
11266
11164
|
if (repo.constitution) {
|
|
11267
|
-
console.log(
|
|
11165
|
+
console.log(import_chalk18.default.green(` constitution: found`));
|
|
11268
11166
|
}
|
|
11269
11167
|
}
|
|
11270
11168
|
});
|
|
@@ -11281,30 +11179,30 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11281
11179
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
11282
11180
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
11283
11181
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11284
|
-
console.log(
|
|
11285
|
-
console.log(
|
|
11182
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11183
|
+
console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
11286
11184
|
const updateRunId = generateRunId();
|
|
11287
11185
|
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11288
11186
|
setActiveSnapshot(updateSnapshot);
|
|
11289
11187
|
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11290
11188
|
setActiveLogger(updateLogger);
|
|
11291
|
-
console.log(
|
|
11189
|
+
console.log(import_chalk18.default.gray(` Run ID: ${updateRunId}`));
|
|
11292
11190
|
let specPath = opts.spec ?? null;
|
|
11293
11191
|
if (!specPath) {
|
|
11294
|
-
const specsDir =
|
|
11192
|
+
const specsDir = path22.join(currentDir, "specs");
|
|
11295
11193
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11296
11194
|
if (!latest) {
|
|
11297
|
-
console.error(
|
|
11195
|
+
console.error(import_chalk18.default.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11298
11196
|
process.exit(1);
|
|
11299
11197
|
}
|
|
11300
11198
|
specPath = latest.filePath;
|
|
11301
|
-
console.log(
|
|
11199
|
+
console.log(import_chalk18.default.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11302
11200
|
}
|
|
11303
|
-
console.log(
|
|
11201
|
+
console.log(import_chalk18.default.gray(" Loading project context..."));
|
|
11304
11202
|
const loader = new ContextLoader(currentDir);
|
|
11305
11203
|
const context = await loader.loadProjectContext();
|
|
11306
11204
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
11307
|
-
console.log(
|
|
11205
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11308
11206
|
}
|
|
11309
11207
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
11310
11208
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -11316,19 +11214,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11316
11214
|
repoType
|
|
11317
11215
|
});
|
|
11318
11216
|
} catch (err) {
|
|
11319
|
-
console.error(
|
|
11217
|
+
console.error(import_chalk18.default.red(` Update failed: ${err.message}`));
|
|
11320
11218
|
process.exit(1);
|
|
11321
11219
|
}
|
|
11322
|
-
console.log(
|
|
11323
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11220
|
+
console.log(import_chalk18.default.green(`
|
|
11221
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
|
|
11324
11222
|
if (result.newDslPath) {
|
|
11325
|
-
console.log(
|
|
11223
|
+
console.log(import_chalk18.default.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
|
|
11326
11224
|
}
|
|
11327
11225
|
if (result.affectedFiles.length > 0) {
|
|
11328
|
-
console.log(
|
|
11226
|
+
console.log(import_chalk18.default.cyan("\n Affected files:"));
|
|
11329
11227
|
for (const f of result.affectedFiles) {
|
|
11330
|
-
const icon = f.action === "create" ?
|
|
11331
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11228
|
+
const icon = f.action === "create" ? import_chalk18.default.green("+") : import_chalk18.default.yellow("~");
|
|
11229
|
+
console.log(` ${icon} ${f.file}: ${import_chalk18.default.gray(f.description)}`);
|
|
11332
11230
|
}
|
|
11333
11231
|
}
|
|
11334
11232
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -11336,9 +11234,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11336
11234
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
11337
11235
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
11338
11236
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11339
|
-
console.log(
|
|
11237
|
+
console.log(import_chalk18.default.blue("\n Regenerating affected files..."));
|
|
11340
11238
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11341
|
-
const specContent = await
|
|
11239
|
+
const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
|
|
11342
11240
|
const constitutionSection = context.constitution ? `
|
|
11343
11241
|
=== Project Constitution (MUST follow) ===
|
|
11344
11242
|
${context.constitution}
|
|
@@ -11349,10 +11247,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
|
11349
11247
|
` : "";
|
|
11350
11248
|
updateLogger.stageStart("update_codegen");
|
|
11351
11249
|
for (const affected of result.affectedFiles) {
|
|
11352
|
-
const fullPath =
|
|
11250
|
+
const fullPath = path22.join(currentDir, affected.file);
|
|
11353
11251
|
let existing = "";
|
|
11354
11252
|
try {
|
|
11355
|
-
existing = await
|
|
11253
|
+
existing = await fs23.readFile(fullPath, "utf-8");
|
|
11356
11254
|
} catch {
|
|
11357
11255
|
}
|
|
11358
11256
|
const codePrompt = `Apply this change to the file.
|
|
@@ -11366,23 +11264,23 @@ ${specContent}
|
|
|
11366
11264
|
${constitutionSection}${dslSection}
|
|
11367
11265
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
11368
11266
|
${existing || "Create from scratch."}`;
|
|
11369
|
-
process.stdout.write(` ${existing ?
|
|
11267
|
+
process.stdout.write(` ${existing ? import_chalk18.default.yellow("~") : import_chalk18.default.green("+")} ${affected.file}... `);
|
|
11370
11268
|
try {
|
|
11371
11269
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11372
11270
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11373
11271
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11374
|
-
await
|
|
11272
|
+
await fs23.ensureDir(path22.dirname(fullPath));
|
|
11375
11273
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11376
|
-
await
|
|
11274
|
+
await fs23.writeFile(fullPath, content, "utf-8");
|
|
11377
11275
|
updateLogger.fileWritten(affected.file);
|
|
11378
|
-
console.log(
|
|
11276
|
+
console.log(import_chalk18.default.green("\u2714"));
|
|
11379
11277
|
} catch (err) {
|
|
11380
11278
|
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11381
|
-
console.log(
|
|
11279
|
+
console.log(import_chalk18.default.red(`\u2718 ${err.message}`));
|
|
11382
11280
|
}
|
|
11383
11281
|
}
|
|
11384
11282
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11385
|
-
const updatedSpecContent = await
|
|
11283
|
+
const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11386
11284
|
if (updatedSpecContent) {
|
|
11387
11285
|
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11388
11286
|
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
@@ -11394,13 +11292,13 @@ ${existing || "Create from scratch."}`;
|
|
|
11394
11292
|
updateLogger.finish();
|
|
11395
11293
|
updateLogger.printSummary();
|
|
11396
11294
|
if (updateSnapshot.fileCount > 0) {
|
|
11397
|
-
console.log(
|
|
11295
|
+
console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11398
11296
|
}
|
|
11399
11297
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11400
|
-
console.log(
|
|
11401
|
-
console.log(
|
|
11402
|
-
console.log(
|
|
11403
|
-
console.log(
|
|
11298
|
+
console.log(import_chalk18.default.blue("\n Next steps:"));
|
|
11299
|
+
console.log(import_chalk18.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11300
|
+
console.log(import_chalk18.default.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11301
|
+
console.log(import_chalk18.default.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
11404
11302
|
}
|
|
11405
11303
|
});
|
|
11406
11304
|
program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)").option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)").option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml").option("--output <path>", "Output file path (default: openapi.yaml)").option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").action(async (opts) => {
|
|
@@ -11409,19 +11307,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11409
11307
|
if (!dslPath) {
|
|
11410
11308
|
dslPath = await findLatestDslFile(currentDir);
|
|
11411
11309
|
if (!dslPath) {
|
|
11412
|
-
console.error(
|
|
11310
|
+
console.error(import_chalk18.default.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11413
11311
|
process.exit(1);
|
|
11414
11312
|
}
|
|
11415
|
-
console.log(
|
|
11313
|
+
console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11416
11314
|
}
|
|
11417
11315
|
let dsl;
|
|
11418
11316
|
try {
|
|
11419
|
-
dsl = await
|
|
11317
|
+
dsl = await fs23.readJson(dslPath);
|
|
11420
11318
|
} catch (err) {
|
|
11421
|
-
console.error(
|
|
11319
|
+
console.error(import_chalk18.default.red(` Failed to read DSL: ${err.message}`));
|
|
11422
11320
|
process.exit(1);
|
|
11423
11321
|
}
|
|
11424
|
-
console.log(
|
|
11322
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11425
11323
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
11426
11324
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
11427
11325
|
try {
|
|
@@ -11430,31 +11328,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11430
11328
|
serverUrl,
|
|
11431
11329
|
outputPath: opts.output
|
|
11432
11330
|
});
|
|
11433
|
-
const rel =
|
|
11434
|
-
console.log(
|
|
11435
|
-
console.log(
|
|
11436
|
-
console.log(
|
|
11437
|
-
console.log(
|
|
11438
|
-
console.log(
|
|
11439
|
-
console.log(
|
|
11440
|
-
console.log(
|
|
11441
|
-
console.log(
|
|
11331
|
+
const rel = path22.relative(currentDir, outputPath);
|
|
11332
|
+
console.log(import_chalk18.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11333
|
+
console.log(import_chalk18.default.gray(` Feature : ${dsl.feature.title}`));
|
|
11334
|
+
console.log(import_chalk18.default.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11335
|
+
console.log(import_chalk18.default.gray(` Models : ${dsl.models.length}`));
|
|
11336
|
+
console.log(import_chalk18.default.gray(` Server : ${serverUrl}`));
|
|
11337
|
+
console.log(import_chalk18.default.blue("\n Next steps:"));
|
|
11338
|
+
console.log(import_chalk18.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11339
|
+
console.log(import_chalk18.default.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
11442
11340
|
} catch (err) {
|
|
11443
|
-
console.error(
|
|
11341
|
+
console.error(import_chalk18.default.red(` Export failed: ${err.message}`));
|
|
11444
11342
|
process.exit(1);
|
|
11445
11343
|
}
|
|
11446
11344
|
});
|
|
11447
11345
|
program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
|
|
11448
11346
|
const currentDir = process.cwd();
|
|
11449
11347
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11450
|
-
console.log(
|
|
11348
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11451
11349
|
if (opts.restore) {
|
|
11452
|
-
const frontendDir = opts.frontend ?
|
|
11350
|
+
const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
|
|
11453
11351
|
const r = await restoreMockProxy(frontendDir);
|
|
11454
11352
|
if (r.restored) {
|
|
11455
|
-
console.log(
|
|
11353
|
+
console.log(import_chalk18.default.green(" \u2714 Proxy restored and mock server stopped."));
|
|
11456
11354
|
} else {
|
|
11457
|
-
console.log(
|
|
11355
|
+
console.log(import_chalk18.default.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
11458
11356
|
}
|
|
11459
11357
|
return;
|
|
11460
11358
|
}
|
|
@@ -11462,32 +11360,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11462
11360
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11463
11361
|
const workspaceConfig = await workspaceLoader.load();
|
|
11464
11362
|
if (!workspaceConfig) {
|
|
11465
|
-
console.error(
|
|
11363
|
+
console.error(import_chalk18.default.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
11466
11364
|
process.exit(1);
|
|
11467
11365
|
}
|
|
11468
11366
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
11469
11367
|
if (backendRepos.length === 0) {
|
|
11470
|
-
console.log(
|
|
11368
|
+
console.log(import_chalk18.default.yellow(" No backend repos found in workspace."));
|
|
11471
11369
|
return;
|
|
11472
11370
|
}
|
|
11473
11371
|
for (const repo of backendRepos) {
|
|
11474
11372
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
11475
|
-
console.log(
|
|
11373
|
+
console.log(import_chalk18.default.cyan(`
|
|
11476
11374
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
11477
11375
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
11478
11376
|
if (!dslFile) {
|
|
11479
|
-
console.log(
|
|
11377
|
+
console.log(import_chalk18.default.yellow(` No DSL file found \u2014 skipping.`));
|
|
11480
11378
|
continue;
|
|
11481
11379
|
}
|
|
11482
|
-
const dsl2 = await
|
|
11380
|
+
const dsl2 = await fs23.readJson(dslFile);
|
|
11483
11381
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
11484
11382
|
port,
|
|
11485
11383
|
msw: opts.msw,
|
|
11486
11384
|
proxy: opts.proxy
|
|
11487
11385
|
});
|
|
11488
11386
|
for (const f of result2.files) {
|
|
11489
|
-
console.log(
|
|
11490
|
-
console.log(
|
|
11387
|
+
console.log(import_chalk18.default.green(` \u2714 ${f.path}`));
|
|
11388
|
+
console.log(import_chalk18.default.gray(` ${f.description}`));
|
|
11491
11389
|
}
|
|
11492
11390
|
}
|
|
11493
11391
|
return;
|
|
@@ -11497,19 +11395,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11497
11395
|
dslPath = await findLatestDslFile(currentDir);
|
|
11498
11396
|
if (!dslPath) {
|
|
11499
11397
|
console.error(
|
|
11500
|
-
|
|
11398
|
+
import_chalk18.default.red(
|
|
11501
11399
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
11502
11400
|
)
|
|
11503
11401
|
);
|
|
11504
11402
|
process.exit(1);
|
|
11505
11403
|
}
|
|
11506
|
-
console.log(
|
|
11404
|
+
console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11507
11405
|
}
|
|
11508
11406
|
let dsl;
|
|
11509
11407
|
try {
|
|
11510
|
-
dsl = await
|
|
11408
|
+
dsl = await fs23.readJson(dslPath);
|
|
11511
11409
|
} catch (err) {
|
|
11512
|
-
console.error(
|
|
11410
|
+
console.error(import_chalk18.default.red(` Failed to read DSL file: ${err.message}`));
|
|
11513
11411
|
process.exit(1);
|
|
11514
11412
|
}
|
|
11515
11413
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11517,58 +11415,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11517
11415
|
msw: opts.msw,
|
|
11518
11416
|
proxy: opts.proxy
|
|
11519
11417
|
});
|
|
11520
|
-
console.log(
|
|
11418
|
+
console.log(import_chalk18.default.green(`
|
|
11521
11419
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11522
11420
|
for (const f of result.files) {
|
|
11523
|
-
console.log(
|
|
11524
|
-
console.log(
|
|
11421
|
+
console.log(import_chalk18.default.green(` ${f.path}`));
|
|
11422
|
+
console.log(import_chalk18.default.gray(` ${f.description}`));
|
|
11525
11423
|
}
|
|
11526
11424
|
if (opts.serve) {
|
|
11527
|
-
const serverJsPath =
|
|
11528
|
-
if (!await
|
|
11529
|
-
console.error(
|
|
11425
|
+
const serverJsPath = path22.join(currentDir, "mock", "server.js");
|
|
11426
|
+
if (!await fs23.pathExists(serverJsPath)) {
|
|
11427
|
+
console.error(import_chalk18.default.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11530
11428
|
process.exit(1);
|
|
11531
11429
|
}
|
|
11532
11430
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11533
|
-
console.log(
|
|
11431
|
+
console.log(import_chalk18.default.green(`
|
|
11534
11432
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11535
11433
|
if (opts.frontend) {
|
|
11536
|
-
const frontendDir =
|
|
11434
|
+
const frontendDir = path22.resolve(opts.frontend);
|
|
11537
11435
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11538
11436
|
await saveMockServerPid(frontendDir, pid);
|
|
11539
11437
|
if (proxyResult.applied) {
|
|
11540
|
-
console.log(
|
|
11541
|
-
console.log(
|
|
11438
|
+
console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11439
|
+
console.log(import_chalk18.default.bold.cyan(`
|
|
11542
11440
|
Ready! Open a new terminal and run:`));
|
|
11543
|
-
console.log(
|
|
11544
|
-
console.log(
|
|
11545
|
-
console.log(
|
|
11441
|
+
console.log(import_chalk18.default.white(` cd ${frontendDir}`));
|
|
11442
|
+
console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
|
|
11443
|
+
console.log(import_chalk18.default.gray(`
|
|
11546
11444
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11547
11445
|
} else {
|
|
11548
|
-
console.log(
|
|
11549
|
-
if (proxyResult.note) console.log(
|
|
11446
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11447
|
+
if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
|
|
11550
11448
|
}
|
|
11551
11449
|
} else {
|
|
11552
|
-
console.log(
|
|
11553
|
-
console.log(
|
|
11450
|
+
console.log(import_chalk18.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11451
|
+
console.log(import_chalk18.default.gray(` Mock server: http://localhost:${port}`));
|
|
11554
11452
|
}
|
|
11555
11453
|
return;
|
|
11556
11454
|
}
|
|
11557
|
-
console.log(
|
|
11558
|
-
console.log(
|
|
11559
|
-
console.log(
|
|
11560
|
-
console.log(
|
|
11561
|
-
console.log(
|
|
11562
|
-
console.log(
|
|
11563
|
-
console.log(
|
|
11564
|
-
console.log(
|
|
11455
|
+
console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11456
|
+
console.log(import_chalk18.default.white(` 1. Install express (if not already):`));
|
|
11457
|
+
console.log(import_chalk18.default.gray(` npm install --save-dev express`));
|
|
11458
|
+
console.log(import_chalk18.default.white(` 2. Start mock server:`));
|
|
11459
|
+
console.log(import_chalk18.default.gray(` node mock/server.js`));
|
|
11460
|
+
console.log(import_chalk18.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
11461
|
+
console.log(import_chalk18.default.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
11462
|
+
console.log(import_chalk18.default.gray(` http://localhost:${port}`));
|
|
11565
11463
|
if (opts.proxy) {
|
|
11566
|
-
console.log(
|
|
11464
|
+
console.log(import_chalk18.default.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11567
11465
|
}
|
|
11568
11466
|
if (opts.msw) {
|
|
11569
|
-
console.log(
|
|
11570
|
-
console.log(
|
|
11571
|
-
console.log(
|
|
11467
|
+
console.log(import_chalk18.default.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
11468
|
+
console.log(import_chalk18.default.gray(` import { worker } from './mocks/browser';`));
|
|
11469
|
+
console.log(import_chalk18.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11572
11470
|
}
|
|
11573
11471
|
});
|
|
11574
11472
|
program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
@@ -11584,34 +11482,26 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11584
11482
|
}
|
|
11585
11483
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11586
11484
|
if (result.appended) {
|
|
11587
|
-
console.log(
|
|
11485
|
+
console.log(import_chalk18.default.green(`
|
|
11588
11486
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11589
|
-
console.log(
|
|
11487
|
+
console.log(import_chalk18.default.gray(` File: .ai-spec-constitution.md`));
|
|
11590
11488
|
} else {
|
|
11591
|
-
console.log(
|
|
11489
|
+
console.log(import_chalk18.default.yellow(`
|
|
11592
11490
|
\u26A0 Not appended: ${result.reason}`));
|
|
11593
11491
|
}
|
|
11594
11492
|
});
|
|
11595
11493
|
program.command("restore").description("Restore files modified by a previous run").argument("<runId>", "Run ID shown at the end of a create / generate run").action(async (runId) => {
|
|
11596
11494
|
const currentDir = process.cwd();
|
|
11597
11495
|
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11598
|
-
console.log(
|
|
11496
|
+
console.log(import_chalk18.default.blue(`Restoring run: ${runId}...`));
|
|
11599
11497
|
const restored = await snapshot.restore();
|
|
11600
11498
|
if (restored.length === 0) {
|
|
11601
|
-
console.log(
|
|
11499
|
+
console.log(import_chalk18.default.yellow(" No backup found for this run ID."));
|
|
11602
11500
|
} else {
|
|
11603
|
-
restored.forEach((f) => console.log(
|
|
11604
|
-
console.log(
|
|
11501
|
+
restored.forEach((f) => console.log(import_chalk18.default.green(` \u2714 restored: ${f}`)));
|
|
11502
|
+
console.log(import_chalk18.default.bold.green(`
|
|
11605
11503
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11606
11504
|
}
|
|
11607
11505
|
});
|
|
11608
|
-
|
|
11609
|
-
(async () => {
|
|
11610
|
-
const currentDir = process.cwd();
|
|
11611
|
-
const config2 = await loadConfig(currentDir);
|
|
11612
|
-
await printWelcome(currentDir, config2);
|
|
11613
|
-
})();
|
|
11614
|
-
} else {
|
|
11615
|
-
program.parse();
|
|
11616
|
-
}
|
|
11506
|
+
program.parse();
|
|
11617
11507
|
//# sourceMappingURL=index.js.map
|