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