@svadmin/create 0.40.3 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/blueprints/customer-workspace/vibe-skill.md +8 -1
- package/blueprints/product-workspace/ARCHITECTURE.md +18 -0
- package/blueprints/product-workspace/README.md +14 -0
- package/blueprints/product-workspace/src/App.svelte +23 -0
- package/blueprints/product-workspace/src/ProductSurfacePage.svelte +132 -0
- package/blueprints/product-workspace/src/app.css +6 -0
- package/blueprints/product-workspace/src/design.svelte.ts +7 -0
- package/blueprints/product-workspace/src/product-provider.ts +81 -0
- package/blueprints/product-workspace/src/svadmin.config.ts +29 -0
- package/blueprints/product-workspace/svadmin.vibe.json +10 -0
- package/blueprints/product-workspace/vibe-skill.md +14 -0
- package/dist/index.js +696 -32
- package/guidance/AGENTS.md +11 -0
- package/guidance/PRODUCT.md +29 -0
- package/package.json +2 -2
- package/scaffold-manifest.json +21 -21
package/dist/index.js
CHANGED
|
@@ -17670,7 +17670,7 @@ function buildAdminAiManifest(options) {
|
|
|
17670
17670
|
notes: SCAFFOLD_MIGRATION_NOTES
|
|
17671
17671
|
},
|
|
17672
17672
|
plugins: [],
|
|
17673
|
-
guidance: ["AGENTS.md", "DESIGN.md"]
|
|
17673
|
+
guidance: ["AGENTS.md", "DESIGN.md", "PRODUCT.md"]
|
|
17674
17674
|
};
|
|
17675
17675
|
}
|
|
17676
17676
|
function buildScaffoldPlatformFiles(options) {
|
|
@@ -18685,6 +18685,488 @@ var resources = [
|
|
|
18685
18685
|
|
|
18686
18686
|
// src/vibe-command.ts
|
|
18687
18687
|
init_vibe_catalog();
|
|
18688
|
+
|
|
18689
|
+
// ../core/src/product-plan.ts
|
|
18690
|
+
var PRODUCT_PLAN_SCHEMA_VERSION = "product/v1";
|
|
18691
|
+
function isRecord2(value) {
|
|
18692
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18693
|
+
}
|
|
18694
|
+
function hasStringId(value) {
|
|
18695
|
+
return isRecord2(value) && typeof value["id"] === "string" && value["id"].trim().length > 0;
|
|
18696
|
+
}
|
|
18697
|
+
var PRODUCT_DISCLOSURES = ["visible", "collapsed", "more", "context", "hidden"];
|
|
18698
|
+
var PRODUCT_PRIORITIES = ["core", "secondary", "future"];
|
|
18699
|
+
var PRODUCT_FREQUENCIES = ["high", "medium", "low", "unknown"];
|
|
18700
|
+
var PRODUCT_PLACEMENTS = ["primary", "secondary", "settings", "context", "command", "hidden"];
|
|
18701
|
+
var PRODUCT_PAGE_KINDS = ["workbench", "list", "detail", "form", "approval", "import", "settings", "custom"];
|
|
18702
|
+
var PRODUCT_STATES = ["empty", "loading", "error", "forbidden", "success"];
|
|
18703
|
+
function pushEnumIssue(issues, value, path, allowed) {
|
|
18704
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
18705
|
+
issues.push({ code: "invalid-enum", path, message: path + " must be one of " + allowed.join(", ") + "." });
|
|
18706
|
+
}
|
|
18707
|
+
}
|
|
18708
|
+
function pushProductFieldIssues(issues, value, path) {
|
|
18709
|
+
if (!Array.isArray(value)) {
|
|
18710
|
+
issues.push({ code: "missing-array", path, message: path + " must be an array." });
|
|
18711
|
+
return;
|
|
18712
|
+
}
|
|
18713
|
+
const seen = new Set;
|
|
18714
|
+
value.forEach((item, index) => {
|
|
18715
|
+
const itemPath = path + "." + index;
|
|
18716
|
+
if (!isRecord2(item)) {
|
|
18717
|
+
issues.push({ code: "invalid-field", path: itemPath, message: itemPath + " must be an object." });
|
|
18718
|
+
return;
|
|
18719
|
+
}
|
|
18720
|
+
const fieldId = item["id"];
|
|
18721
|
+
if (typeof fieldId !== "string" || fieldId.trim().length === 0) {
|
|
18722
|
+
issues.push({ code: "invalid-id", path: itemPath + ".id", message: "Every product field needs a non-empty string id." });
|
|
18723
|
+
return;
|
|
18724
|
+
}
|
|
18725
|
+
if (seen.has(fieldId)) {
|
|
18726
|
+
issues.push({ code: "duplicate-id", path: itemPath + ".id", message: "Product field id " + fieldId + " is duplicated within its resource." });
|
|
18727
|
+
}
|
|
18728
|
+
seen.add(fieldId);
|
|
18729
|
+
for (const key of ["label", "type"]) {
|
|
18730
|
+
if (typeof item[key] !== "string")
|
|
18731
|
+
issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be a string." });
|
|
18732
|
+
}
|
|
18733
|
+
for (const key of ["readableBy", "writableBy", "usedByWorkflowIds"]) {
|
|
18734
|
+
const candidate = item[key];
|
|
18735
|
+
if (!Array.isArray(candidate) || !candidate.every((entry) => typeof entry === "string")) {
|
|
18736
|
+
issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be an array of strings." });
|
|
18737
|
+
}
|
|
18738
|
+
}
|
|
18739
|
+
for (const key of ["required", "sensitive"]) {
|
|
18740
|
+
if (item[key] !== undefined && typeof item[key] !== "boolean") {
|
|
18741
|
+
issues.push({ code: "invalid-field", path: itemPath + "." + key, message: itemPath + "." + key + " must be a boolean when provided." });
|
|
18742
|
+
}
|
|
18743
|
+
}
|
|
18744
|
+
if (item["disclosure"] !== undefined)
|
|
18745
|
+
pushEnumIssue(issues, item["disclosure"], itemPath + ".disclosure", PRODUCT_DISCLOSURES);
|
|
18746
|
+
});
|
|
18747
|
+
}
|
|
18748
|
+
function pushEntityIssues(issues, value, path, seen, shape = {}) {
|
|
18749
|
+
if (!Array.isArray(value)) {
|
|
18750
|
+
issues.push({ code: "missing-array", path, message: `${path} must be an array.` });
|
|
18751
|
+
return;
|
|
18752
|
+
}
|
|
18753
|
+
value.forEach((item, index) => {
|
|
18754
|
+
const itemPath = `${path}.${index}`;
|
|
18755
|
+
if (!hasStringId(item)) {
|
|
18756
|
+
issues.push({ code: "invalid-id", path: `${itemPath}.id`, message: "Every product entity needs a non-empty string id." });
|
|
18757
|
+
return;
|
|
18758
|
+
}
|
|
18759
|
+
if (seen.has(item.id)) {
|
|
18760
|
+
issues.push({ code: "duplicate-id", path: `${itemPath}.id`, message: `Product id "${item.id}" is duplicated.` });
|
|
18761
|
+
}
|
|
18762
|
+
seen.add(item.id);
|
|
18763
|
+
const record = isRecord2(item) ? item : undefined;
|
|
18764
|
+
if (!record)
|
|
18765
|
+
return;
|
|
18766
|
+
for (const key of shape.strings ?? []) {
|
|
18767
|
+
if (typeof record[key] !== "string")
|
|
18768
|
+
issues.push({ code: "invalid-field", path: `${itemPath}.${key}`, message: `${itemPath}.${key} must be a string.` });
|
|
18769
|
+
}
|
|
18770
|
+
for (const key of shape.arrays ?? []) {
|
|
18771
|
+
const candidate = record[key];
|
|
18772
|
+
if (!Array.isArray(candidate) || !candidate.every((value) => typeof value === "string")) {
|
|
18773
|
+
issues.push({ code: "invalid-field", path: `${itemPath}.${key}`, message: `${itemPath}.${key} must be an array of strings.` });
|
|
18774
|
+
}
|
|
18775
|
+
}
|
|
18776
|
+
});
|
|
18777
|
+
}
|
|
18778
|
+
function validateProductPlan(input) {
|
|
18779
|
+
const issues = [];
|
|
18780
|
+
if (!isRecord2(input))
|
|
18781
|
+
return { ok: false, issues: [{ code: "invalid-root", path: "", message: "Product plan must be a JSON object." }] };
|
|
18782
|
+
if (input["schemaVersion"] !== PRODUCT_PLAN_SCHEMA_VERSION) {
|
|
18783
|
+
issues.push({ code: "invalid-version", path: "schemaVersion", message: `Expected ${PRODUCT_PLAN_SCHEMA_VERSION}.` });
|
|
18784
|
+
}
|
|
18785
|
+
if (!isRecord2(input["brief"])) {
|
|
18786
|
+
issues.push({ code: "invalid-root", path: "brief", message: "Product plan brief must be an object." });
|
|
18787
|
+
} else {
|
|
18788
|
+
const brief = input["brief"];
|
|
18789
|
+
for (const key of ["id", "name", "description", "primaryOutcome", "primaryAudience"]) {
|
|
18790
|
+
if (typeof brief[key] !== "string")
|
|
18791
|
+
issues.push({ code: "invalid-field", path: `brief.${key}`, message: `brief.${key} must be a string.` });
|
|
18792
|
+
}
|
|
18793
|
+
for (const key of ["constraints", "nonGoals", "assumptions"]) {
|
|
18794
|
+
const candidate = brief[key];
|
|
18795
|
+
if (!Array.isArray(candidate) || !candidate.every((value) => typeof value === "string"))
|
|
18796
|
+
issues.push({ code: "invalid-field", path: `brief.${key}`, message: `brief.${key} must be an array of strings.` });
|
|
18797
|
+
}
|
|
18798
|
+
}
|
|
18799
|
+
const shapes = {
|
|
18800
|
+
roles: { strings: ["name"], arrays: ["permissions"] },
|
|
18801
|
+
workflows: { strings: ["name", "trigger", "goal", "priority", "frequency"], arrays: ["actorRoleIds", "steps", "successCriteria", "exceptions"] },
|
|
18802
|
+
resources: { strings: ["name", "label"], arrays: ["roleIds", "workflowIds"] },
|
|
18803
|
+
actions: { strings: ["label", "workflowId"], arrays: ["roleIds"] },
|
|
18804
|
+
surfaces: { strings: ["name", "kind", "purpose"], arrays: ["workflowIds", "resourceIds", "roleIds", "actionIds", "sections", "states"] },
|
|
18805
|
+
menu: { strings: ["label", "placement"], arrays: ["workflowIds", "roleIds"] }
|
|
18806
|
+
};
|
|
18807
|
+
for (const key of ["roles", "workflows", "resources", "actions", "surfaces", "menu"]) {
|
|
18808
|
+
pushEntityIssues(issues, input[key], key, new Set, shapes[key]);
|
|
18809
|
+
}
|
|
18810
|
+
const workflows = input["workflows"];
|
|
18811
|
+
if (Array.isArray(workflows))
|
|
18812
|
+
workflows.forEach((workflow, index) => {
|
|
18813
|
+
if (!isRecord2(workflow))
|
|
18814
|
+
return;
|
|
18815
|
+
if (typeof workflow["priority"] === "string")
|
|
18816
|
+
pushEnumIssue(issues, workflow["priority"], "workflows." + index + ".priority", PRODUCT_PRIORITIES);
|
|
18817
|
+
if (typeof workflow["frequency"] === "string")
|
|
18818
|
+
pushEnumIssue(issues, workflow["frequency"], "workflows." + index + ".frequency", PRODUCT_FREQUENCIES);
|
|
18819
|
+
});
|
|
18820
|
+
const resources = input["resources"];
|
|
18821
|
+
if (Array.isArray(resources))
|
|
18822
|
+
resources.forEach((resource, index) => {
|
|
18823
|
+
if (!isRecord2(resource))
|
|
18824
|
+
return;
|
|
18825
|
+
pushProductFieldIssues(issues, resource["fields"], "resources." + index + ".fields");
|
|
18826
|
+
if (resource["menuPlacement"] !== undefined)
|
|
18827
|
+
pushEnumIssue(issues, resource["menuPlacement"], "resources." + index + ".menuPlacement", PRODUCT_PLACEMENTS);
|
|
18828
|
+
if (resource["internal"] !== undefined && typeof resource["internal"] !== "boolean") {
|
|
18829
|
+
issues.push({ code: "invalid-field", path: "resources." + index + ".internal", message: "resources." + index + ".internal must be a boolean when provided." });
|
|
18830
|
+
}
|
|
18831
|
+
});
|
|
18832
|
+
const actions = input["actions"];
|
|
18833
|
+
if (Array.isArray(actions))
|
|
18834
|
+
actions.forEach((action, index) => {
|
|
18835
|
+
if (!isRecord2(action))
|
|
18836
|
+
return;
|
|
18837
|
+
if (action["disclosure"] !== undefined)
|
|
18838
|
+
pushEnumIssue(issues, action["disclosure"], "actions." + index + ".disclosure", PRODUCT_DISCLOSURES);
|
|
18839
|
+
if (action["destructive"] !== undefined && typeof action["destructive"] !== "boolean") {
|
|
18840
|
+
issues.push({ code: "invalid-field", path: "actions." + index + ".destructive", message: "actions." + index + ".destructive must be a boolean when provided." });
|
|
18841
|
+
}
|
|
18842
|
+
});
|
|
18843
|
+
const surfaces = input["surfaces"];
|
|
18844
|
+
if (Array.isArray(surfaces))
|
|
18845
|
+
surfaces.forEach((surface, index) => {
|
|
18846
|
+
if (!isRecord2(surface))
|
|
18847
|
+
return;
|
|
18848
|
+
if (typeof surface["kind"] === "string")
|
|
18849
|
+
pushEnumIssue(issues, surface["kind"], "surfaces." + index + ".kind", PRODUCT_PAGE_KINDS);
|
|
18850
|
+
if (surface["menuPlacement"] !== undefined)
|
|
18851
|
+
pushEnumIssue(issues, surface["menuPlacement"], "surfaces." + index + ".menuPlacement", PRODUCT_PLACEMENTS);
|
|
18852
|
+
if (surface["route"] !== undefined && (typeof surface["route"] !== "string" || surface["route"].trim().length === 0)) {
|
|
18853
|
+
issues.push({ code: "invalid-field", path: "surfaces." + index + ".route", message: "surfaces." + index + ".route must be a non-empty string when provided." });
|
|
18854
|
+
}
|
|
18855
|
+
const states = surface["states"];
|
|
18856
|
+
if (Array.isArray(states))
|
|
18857
|
+
states.forEach((state, stateIndex) => {
|
|
18858
|
+
if (typeof state === "string")
|
|
18859
|
+
pushEnumIssue(issues, state, "surfaces." + index + ".states." + stateIndex, PRODUCT_STATES);
|
|
18860
|
+
});
|
|
18861
|
+
});
|
|
18862
|
+
const menu = input["menu"];
|
|
18863
|
+
if (Array.isArray(menu))
|
|
18864
|
+
menu.forEach((entry, index) => {
|
|
18865
|
+
if (!isRecord2(entry))
|
|
18866
|
+
return;
|
|
18867
|
+
if (typeof entry["placement"] === "string")
|
|
18868
|
+
pushEnumIssue(issues, entry["placement"], "menu." + index + ".placement", PRODUCT_PLACEMENTS);
|
|
18869
|
+
for (const key of ["href", "parentId", "surfaceId", "resourceId", "condition"]) {
|
|
18870
|
+
if (entry[key] !== undefined && typeof entry[key] !== "string") {
|
|
18871
|
+
issues.push({ code: "invalid-field", path: "menu." + index + "." + key, message: "menu." + index + "." + key + " must be a string when provided." });
|
|
18872
|
+
}
|
|
18873
|
+
}
|
|
18874
|
+
if (entry["order"] !== undefined && (typeof entry["order"] !== "number" || !Number.isFinite(entry["order"]))) {
|
|
18875
|
+
issues.push({ code: "invalid-field", path: "menu." + index + ".order", message: "menu." + index + ".order must be a finite number when provided." });
|
|
18876
|
+
}
|
|
18877
|
+
});
|
|
18878
|
+
return issues.length === 0 ? { ok: true, value: input } : { ok: false, issues };
|
|
18879
|
+
}
|
|
18880
|
+
|
|
18881
|
+
class ProductPlanValidationError extends Error {
|
|
18882
|
+
issues;
|
|
18883
|
+
constructor(issues) {
|
|
18884
|
+
super(`Invalid product plan (${issues.length} issue${issues.length === 1 ? "" : "s"})`);
|
|
18885
|
+
this.name = "ProductPlanValidationError";
|
|
18886
|
+
this.issues = issues;
|
|
18887
|
+
}
|
|
18888
|
+
}
|
|
18889
|
+
function parseProductPlan(input) {
|
|
18890
|
+
const result = validateProductPlan(input);
|
|
18891
|
+
if (!result.ok)
|
|
18892
|
+
throw new ProductPlanValidationError(result.issues);
|
|
18893
|
+
return result.value;
|
|
18894
|
+
}
|
|
18895
|
+
|
|
18896
|
+
// ../core/src/product-navigation.ts
|
|
18897
|
+
function canSeeByRole(item, roleIds) {
|
|
18898
|
+
return item.roleIds.length === 0 || item.roleIds.some((roleId) => roleIds.includes(roleId));
|
|
18899
|
+
}
|
|
18900
|
+
function conditionEnabled(condition, flags) {
|
|
18901
|
+
return condition === undefined ? true : flags?.[condition] === true;
|
|
18902
|
+
}
|
|
18903
|
+
function createNode(item, allItems, context, ancestry = new Set) {
|
|
18904
|
+
const visibleByRole = canSeeByRole(item, context.roleIds);
|
|
18905
|
+
const visibleByCondition = conditionEnabled(item.condition, context.featureFlags);
|
|
18906
|
+
const visible = visibleByRole && visibleByCondition && item.placement !== "hidden";
|
|
18907
|
+
const nextAncestry = new Set(ancestry);
|
|
18908
|
+
nextAncestry.add(item.id);
|
|
18909
|
+
const children = nextAncestry.has(item.id) ? allItems.filter((candidate) => candidate.parentId === item.id && !nextAncestry.has(candidate.id)).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)).map((candidate) => createNode(candidate, allItems, context, nextAncestry)) : [];
|
|
18910
|
+
return {
|
|
18911
|
+
...item,
|
|
18912
|
+
children,
|
|
18913
|
+
visible,
|
|
18914
|
+
...visible ? {} : { reason: visibleByRole ? "hidden by business condition or placement" : "current role is not allowed to see this entry" }
|
|
18915
|
+
};
|
|
18916
|
+
}
|
|
18917
|
+
function buildProductNavigation(plan, context) {
|
|
18918
|
+
return plan.menu.filter((item) => item.parentId === undefined).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)).map((item) => createNode(item, plan.menu, context));
|
|
18919
|
+
}
|
|
18920
|
+
|
|
18921
|
+
// ../core/src/product-pages.ts
|
|
18922
|
+
function buildProductPagePlan(surface, plan) {
|
|
18923
|
+
const actionById = new Map(plan.actions.map((action) => [action.id, action]));
|
|
18924
|
+
const primaryAction = surface.primaryActionId === undefined ? undefined : actionById.get(surface.primaryActionId);
|
|
18925
|
+
const secondaryActions = surface.actionIds.filter((id) => id !== surface.primaryActionId).map((id) => actionById.get(id)).filter((action) => action !== undefined);
|
|
18926
|
+
const sections = surface.sections.length ? surface.sections.map((title, index) => ({
|
|
18927
|
+
id: `${surface.id}.section.${index + 1}`,
|
|
18928
|
+
title,
|
|
18929
|
+
resourceIds: index === 0 ? surface.resourceIds : []
|
|
18930
|
+
})) : [{ id: `${surface.id}.content`, title: surface.name, resourceIds: surface.resourceIds }];
|
|
18931
|
+
return {
|
|
18932
|
+
id: surface.id,
|
|
18933
|
+
kind: surface.kind,
|
|
18934
|
+
title: surface.name,
|
|
18935
|
+
purpose: surface.purpose,
|
|
18936
|
+
workflowIds: surface.workflowIds,
|
|
18937
|
+
roleIds: surface.roleIds,
|
|
18938
|
+
...primaryAction === undefined ? {} : { primaryAction },
|
|
18939
|
+
secondaryActions,
|
|
18940
|
+
sections,
|
|
18941
|
+
states: surface.states
|
|
18942
|
+
};
|
|
18943
|
+
}
|
|
18944
|
+
|
|
18945
|
+
// ../core/src/product-review.ts
|
|
18946
|
+
function has2(set, value) {
|
|
18947
|
+
return set.has(value);
|
|
18948
|
+
}
|
|
18949
|
+
function addIssue(issues, issue) {
|
|
18950
|
+
issues.push(issue);
|
|
18951
|
+
}
|
|
18952
|
+
function reviewProductPlan(plan) {
|
|
18953
|
+
const issues = [];
|
|
18954
|
+
const roleIds = new Set(plan.roles.map((role) => role.id));
|
|
18955
|
+
const workflowIds = new Set(plan.workflows.map((workflow) => workflow.id));
|
|
18956
|
+
const resourceIds = new Set(plan.resources.map((resource) => resource.id));
|
|
18957
|
+
const surfaceIds = new Set(plan.surfaces.map((surface) => surface.id));
|
|
18958
|
+
const actionIds = new Set(plan.actions.map((action) => action.id));
|
|
18959
|
+
const runtimeNames = new Map;
|
|
18960
|
+
for (const resource of plan.resources) {
|
|
18961
|
+
const runtimeName = resource.runtimeName ?? resource.name;
|
|
18962
|
+
const previous = runtimeNames.get(runtimeName);
|
|
18963
|
+
if (previous !== undefined) {
|
|
18964
|
+
addIssue(issues, {
|
|
18965
|
+
code: "resource.duplicate-runtime-name",
|
|
18966
|
+
severity: "error",
|
|
18967
|
+
message: `Resources "${resource.label}" and "${previous}" resolve to the same runtime name "${runtimeName}".`,
|
|
18968
|
+
path: `resources.${resource.id}.runtimeName`,
|
|
18969
|
+
suggestion: "Give every generated resource route a unique runtime name."
|
|
18970
|
+
});
|
|
18971
|
+
} else {
|
|
18972
|
+
runtimeNames.set(runtimeName, resource.label);
|
|
18973
|
+
}
|
|
18974
|
+
}
|
|
18975
|
+
const surfaceRoutes = new Map;
|
|
18976
|
+
for (const surface of plan.surfaces) {
|
|
18977
|
+
const route = surface.route ?? `/workflows/${surface.id}`;
|
|
18978
|
+
const previous = surfaceRoutes.get(route);
|
|
18979
|
+
if (previous !== undefined) {
|
|
18980
|
+
addIssue(issues, {
|
|
18981
|
+
code: "surface.duplicate-route",
|
|
18982
|
+
severity: "error",
|
|
18983
|
+
message: `Surfaces "${surface.name}" and "${previous}" resolve to the same route "${route}".`,
|
|
18984
|
+
path: `surfaces.${surface.id}.route`,
|
|
18985
|
+
suggestion: "Give every generated surface a unique route."
|
|
18986
|
+
});
|
|
18987
|
+
} else {
|
|
18988
|
+
surfaceRoutes.set(route, surface.name);
|
|
18989
|
+
}
|
|
18990
|
+
}
|
|
18991
|
+
for (const workflow of plan.workflows) {
|
|
18992
|
+
if (!workflow.actorRoleIds.length)
|
|
18993
|
+
addIssue(issues, { code: "workflow.missing-actor", severity: "error", message: `Workflow "${workflow.name}" has no actor role.`, path: `workflows.${workflow.id}.actorRoleIds` });
|
|
18994
|
+
for (const roleId of workflow.actorRoleIds)
|
|
18995
|
+
if (!has2(roleIds, roleId))
|
|
18996
|
+
addIssue(issues, { code: "workflow.unknown-role", severity: "error", message: `Workflow "${workflow.name}" references unknown role "${roleId}".`, path: `workflows.${workflow.id}.actorRoleIds` });
|
|
18997
|
+
if (!workflow.goal.trim())
|
|
18998
|
+
addIssue(issues, { code: "workflow.missing-goal", severity: "error", message: `Workflow "${workflow.name}" has no user goal.`, path: `workflows.${workflow.id}.goal` });
|
|
18999
|
+
}
|
|
19000
|
+
for (const resource of plan.resources) {
|
|
19001
|
+
if (!resource.workflowIds.length && !resource.internal)
|
|
19002
|
+
addIssue(issues, { code: "resource.no-workflow", severity: "warning", message: `Resource "${resource.label}" is not attached to a workflow.`, path: `resources.${resource.id}.workflowIds`, suggestion: "Make it contextual/internal or attach it to a real task." });
|
|
19003
|
+
for (const workflowId of resource.workflowIds)
|
|
19004
|
+
if (!has2(workflowIds, workflowId))
|
|
19005
|
+
addIssue(issues, { code: "resource.unknown-workflow", severity: "error", message: `Resource "${resource.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.workflowIds` });
|
|
19006
|
+
for (const roleId of resource.roleIds)
|
|
19007
|
+
if (!has2(roleIds, roleId))
|
|
19008
|
+
addIssue(issues, { code: "resource.unknown-role", severity: "error", message: `Resource "${resource.label}" references unknown role "${roleId}".`, path: `resources.${resource.id}.roleIds` });
|
|
19009
|
+
resource.fields.forEach((field) => {
|
|
19010
|
+
for (const roleId of field.readableBy)
|
|
19011
|
+
if (!has2(roleIds, roleId))
|
|
19012
|
+
addIssue(issues, { code: "field.unknown-role", severity: "error", message: `Field "${field.label}" references unknown readable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.readableBy` });
|
|
19013
|
+
for (const roleId of field.writableBy)
|
|
19014
|
+
if (!has2(roleIds, roleId))
|
|
19015
|
+
addIssue(issues, { code: "field.unknown-role", severity: "error", message: `Field "${field.label}" references unknown writable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.writableBy` });
|
|
19016
|
+
for (const workflowId of field.usedByWorkflowIds)
|
|
19017
|
+
if (!has2(workflowIds, workflowId))
|
|
19018
|
+
addIssue(issues, { code: "field.unknown-workflow", severity: "error", message: `Field "${field.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.fields.${field.id}.usedByWorkflowIds` });
|
|
19019
|
+
});
|
|
19020
|
+
}
|
|
19021
|
+
for (const surface of plan.surfaces) {
|
|
19022
|
+
reviewSurface(surface, { workflowIds, actionIds, resourceIds, roleIds, surfaceIds }, issues);
|
|
19023
|
+
}
|
|
19024
|
+
const visitingSurfaces = new Set;
|
|
19025
|
+
const visitedSurfaces = new Set;
|
|
19026
|
+
const visitSurface = (id) => {
|
|
19027
|
+
if (visitingSurfaces.has(id)) {
|
|
19028
|
+
addIssue(issues, { code: "surface.cycle", severity: "error", message: `Surface hierarchy contains a cycle at "${id}".`, path: `surfaces.${id}.parentSurfaceId` });
|
|
19029
|
+
return;
|
|
19030
|
+
}
|
|
19031
|
+
if (visitedSurfaces.has(id))
|
|
19032
|
+
return;
|
|
19033
|
+
visitingSurfaces.add(id);
|
|
19034
|
+
const parentId = plan.surfaces.find((surface) => surface.id === id)?.parentSurfaceId;
|
|
19035
|
+
if (parentId !== undefined && surfaceIds.has(parentId))
|
|
19036
|
+
visitSurface(parentId);
|
|
19037
|
+
visitingSurfaces.delete(id);
|
|
19038
|
+
visitedSurfaces.add(id);
|
|
19039
|
+
};
|
|
19040
|
+
for (const surface of plan.surfaces)
|
|
19041
|
+
visitSurface(surface.id);
|
|
19042
|
+
const menuById = new Map(plan.menu.map((item) => [item.id, item]));
|
|
19043
|
+
for (const item of plan.menu) {
|
|
19044
|
+
if (item.parentId !== undefined && !menuById.has(item.parentId)) {
|
|
19045
|
+
addIssue(issues, { code: "menu.unknown-parent", severity: "error", message: `Menu item "${item.label}" references unknown parent "${item.parentId}".`, path: `menu.${item.id}.parentId` });
|
|
19046
|
+
}
|
|
19047
|
+
}
|
|
19048
|
+
const visiting = new Set;
|
|
19049
|
+
const visited = new Set;
|
|
19050
|
+
const visitMenu = (id) => {
|
|
19051
|
+
if (visiting.has(id)) {
|
|
19052
|
+
addIssue(issues, { code: "menu.cycle", severity: "error", message: `Menu hierarchy contains a cycle at "${id}".`, path: `menu.${id}.parentId` });
|
|
19053
|
+
return;
|
|
19054
|
+
}
|
|
19055
|
+
if (visited.has(id))
|
|
19056
|
+
return;
|
|
19057
|
+
visiting.add(id);
|
|
19058
|
+
const parentId = menuById.get(id)?.parentId;
|
|
19059
|
+
if (parentId !== undefined && menuById.has(parentId))
|
|
19060
|
+
visitMenu(parentId);
|
|
19061
|
+
visiting.delete(id);
|
|
19062
|
+
visited.add(id);
|
|
19063
|
+
};
|
|
19064
|
+
for (const item of plan.menu)
|
|
19065
|
+
visitMenu(item.id);
|
|
19066
|
+
for (const item of plan.menu) {
|
|
19067
|
+
if (!item.workflowIds.length && !item.resourceId && !item.surfaceId)
|
|
19068
|
+
addIssue(issues, { code: "menu.no-business-target", severity: "warning", message: `Menu item "${item.label}" has no workflow, resource or surface target.`, path: `menu.${item.id}`, suggestion: "Keep it only for an intentional external/system entry." });
|
|
19069
|
+
if (item.surfaceId && !has2(surfaceIds, item.surfaceId))
|
|
19070
|
+
addIssue(issues, { code: "menu.unknown-surface", severity: "error", message: `Menu item "${item.label}" references unknown surface.`, path: `menu.${item.id}.surfaceId` });
|
|
19071
|
+
if (item.resourceId && !has2(resourceIds, item.resourceId))
|
|
19072
|
+
addIssue(issues, { code: "menu.unknown-resource", severity: "error", message: `Menu item "${item.label}" references unknown resource.`, path: `menu.${item.id}.resourceId` });
|
|
19073
|
+
for (const workflowId of item.workflowIds)
|
|
19074
|
+
if (!has2(workflowIds, workflowId))
|
|
19075
|
+
addIssue(issues, { code: "menu.unknown-workflow", severity: "error", message: `Menu item "${item.label}" references unknown workflow "${workflowId}".`, path: `menu.${item.id}.workflowIds` });
|
|
19076
|
+
for (const roleId of item.roleIds)
|
|
19077
|
+
if (!has2(roleIds, roleId))
|
|
19078
|
+
addIssue(issues, { code: "menu.unknown-role", severity: "error", message: `Menu item "${item.label}" references unknown role "${roleId}".`, path: `menu.${item.id}.roleIds` });
|
|
19079
|
+
if (!item.roleIds.length && item.placement !== "hidden")
|
|
19080
|
+
addIssue(issues, { code: "menu.public-entry", severity: "info", message: `Menu item "${item.label}" is public at the UI hint layer.`, path: `menu.${item.id}.roleIds`, suggestion: "Keep server authorization independent from menu visibility." });
|
|
19081
|
+
}
|
|
19082
|
+
for (const action of plan.actions) {
|
|
19083
|
+
if (!has2(workflowIds, action.workflowId))
|
|
19084
|
+
addIssue(issues, { code: "action.unknown-workflow", severity: "error", message: `Action "${action.label}" references unknown workflow.`, path: `actions.${action.id}.workflowId` });
|
|
19085
|
+
for (const roleId of action.roleIds)
|
|
19086
|
+
if (!has2(roleIds, roleId))
|
|
19087
|
+
addIssue(issues, { code: "action.unknown-role", severity: "error", message: `Action "${action.label}" references unknown role "${roleId}".`, path: `actions.${action.id}.roleIds` });
|
|
19088
|
+
if (!action.roleIds.length)
|
|
19089
|
+
addIssue(issues, { code: "action.missing-roles", severity: "warning", message: `Action "${action.label}" has no role visibility contract.`, path: `actions.${action.id}.roleIds` });
|
|
19090
|
+
}
|
|
19091
|
+
const served = new Set;
|
|
19092
|
+
plan.surfaces.forEach((surface) => surface.workflowIds.forEach((id) => served.add(id)));
|
|
19093
|
+
plan.menu.forEach((item) => item.workflowIds.forEach((id) => served.add(id)));
|
|
19094
|
+
const unserved = plan.workflows.filter((workflow) => !served.has(workflow.id));
|
|
19095
|
+
unserved.forEach((workflow) => addIssue(issues, { code: "workflow.unserved", severity: workflow.priority === "core" ? "error" : "warning", message: `Workflow "${workflow.name}" has no page or menu entry.`, path: `workflows.${workflow.id}`, suggestion: "Add a surface or contextual entry, or mark the workflow as deferred." }));
|
|
19096
|
+
const roleVisibilityGaps = plan.workflows.filter((workflow) => workflow.actorRoleIds.length > 0 && !plan.surfaces.some((surface) => surface.workflowIds.includes(workflow.id) && (surface.roleIds.length === 0 || workflow.actorRoleIds.some((roleId) => surface.roleIds.includes(roleId))))).length;
|
|
19097
|
+
const orphanResourceCount = plan.resources.filter((resource) => resource.workflowIds.length === 0 && !resource.internal).length;
|
|
19098
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
19099
|
+
return {
|
|
19100
|
+
ok: errors.length === 0,
|
|
19101
|
+
issues,
|
|
19102
|
+
metrics: {
|
|
19103
|
+
resourceCount: plan.resources.length,
|
|
19104
|
+
workflowCount: plan.workflows.length,
|
|
19105
|
+
surfaceCount: plan.surfaces.length,
|
|
19106
|
+
menuNodeCount: plan.menu.length,
|
|
19107
|
+
orphanResourceCount,
|
|
19108
|
+
unservedWorkflowCount: unserved.length,
|
|
19109
|
+
roleVisibilityGaps
|
|
19110
|
+
}
|
|
19111
|
+
};
|
|
19112
|
+
}
|
|
19113
|
+
function reviewSurface(surface, refs, issues) {
|
|
19114
|
+
if (!surface.purpose.trim())
|
|
19115
|
+
addIssue(issues, { code: "surface.missing-purpose", severity: "error", message: `Surface "${surface.name}" has no purpose.`, path: `surfaces.${surface.id}.purpose` });
|
|
19116
|
+
for (const roleId of surface.roleIds)
|
|
19117
|
+
if (!refs.roleIds.has(roleId))
|
|
19118
|
+
addIssue(issues, { code: "surface.unknown-role", severity: "error", message: `Surface "${surface.name}" references unknown role "${roleId}".`, path: `surfaces.${surface.id}.roleIds` });
|
|
19119
|
+
if (surface.parentSurfaceId !== undefined && !refs.surfaceIds.has(surface.parentSurfaceId))
|
|
19120
|
+
addIssue(issues, { code: "surface.unknown-parent", severity: "error", message: `Surface "${surface.name}" references unknown parent surface "${surface.parentSurfaceId}".`, path: `surfaces.${surface.id}.parentSurfaceId` });
|
|
19121
|
+
if (!surface.workflowIds.length)
|
|
19122
|
+
addIssue(issues, { code: "surface.no-workflow", severity: "warning", message: `Surface "${surface.name}" is not attached to a workflow.`, path: `surfaces.${surface.id}.workflowIds` });
|
|
19123
|
+
surface.workflowIds.forEach((id) => {
|
|
19124
|
+
if (!refs.workflowIds.has(id))
|
|
19125
|
+
addIssue(issues, { code: "surface.unknown-workflow", severity: "error", message: `Surface "${surface.name}" references unknown workflow "${id}".`, path: `surfaces.${surface.id}.workflowIds` });
|
|
19126
|
+
});
|
|
19127
|
+
surface.resourceIds.forEach((id) => {
|
|
19128
|
+
if (!refs.resourceIds.has(id))
|
|
19129
|
+
addIssue(issues, { code: "surface.unknown-resource", severity: "error", message: `Surface "${surface.name}" references unknown resource "${id}".`, path: `surfaces.${surface.id}.resourceIds` });
|
|
19130
|
+
});
|
|
19131
|
+
surface.actionIds.forEach((id) => {
|
|
19132
|
+
if (!refs.actionIds.has(id))
|
|
19133
|
+
addIssue(issues, { code: "surface.unknown-action", severity: "error", message: `Surface "${surface.name}" references unknown action "${id}".`, path: `surfaces.${surface.id}.actionIds` });
|
|
19134
|
+
});
|
|
19135
|
+
if (surface.primaryActionId && !refs.actionIds.has(surface.primaryActionId))
|
|
19136
|
+
addIssue(issues, { code: "surface.unknown-primary-action", severity: "error", message: `Surface "${surface.name}" references unknown primary action.`, path: `surfaces.${surface.id}.primaryActionId` });
|
|
19137
|
+
if (!surface.states.length)
|
|
19138
|
+
addIssue(issues, { code: "surface.missing-states", severity: "warning", message: `Surface "${surface.name}" does not declare empty/loading/error/permission states.`, path: `surfaces.${surface.id}.states`, suggestion: "Declare the states the Svelte view must implement." });
|
|
19139
|
+
if (surface.sections.length > 6)
|
|
19140
|
+
addIssue(issues, { code: "surface.section-density", severity: "warning", message: `Surface "${surface.name}" contains ${surface.sections.length} sections.`, path: `surfaces.${surface.id}.sections`, suggestion: "Group related information or use progressive disclosure." });
|
|
19141
|
+
if (surface.actionIds.length > 4)
|
|
19142
|
+
addIssue(issues, { code: "surface.action-density", severity: "warning", message: `Surface "${surface.name}" exposes ${surface.actionIds.length} actions.`, path: `surfaces.${surface.id}.actionIds`, suggestion: "Keep one primary action and move secondary actions into context or More." });
|
|
19143
|
+
}
|
|
19144
|
+
|
|
19145
|
+
// ../core/src/product-pipeline.ts
|
|
19146
|
+
function runProductPipeline(input) {
|
|
19147
|
+
const navigation = buildProductNavigation(input.plan, input.navigation);
|
|
19148
|
+
const visibleSurfaceIds = input.surfaceIds === undefined ? undefined : new Set(input.surfaceIds);
|
|
19149
|
+
const pages = input.plan.surfaces.filter((surface) => visibleSurfaceIds === undefined || visibleSurfaceIds.has(surface.id)).filter((surface) => surface.roleIds.length === 0 || surface.roleIds.some((roleId) => input.navigation.roleIds.includes(roleId))).map((surface) => buildProductPagePlan(surface, input.plan));
|
|
19150
|
+
return { navigation, pages, productReview: reviewProductPlan(input.plan) };
|
|
19151
|
+
}
|
|
19152
|
+
function formatProductPreview(output) {
|
|
19153
|
+
const lines = ["Navigation:"];
|
|
19154
|
+
for (const node of output.navigation) {
|
|
19155
|
+
if (!node.visible)
|
|
19156
|
+
continue;
|
|
19157
|
+
lines.push(`- ${node.label}`);
|
|
19158
|
+
for (const child of node.children)
|
|
19159
|
+
if (child.visible)
|
|
19160
|
+
lines.push(` - ${child.label}`);
|
|
19161
|
+
}
|
|
19162
|
+
lines.push("", "Pages:");
|
|
19163
|
+
output.pages.forEach((page) => lines.push(`- ${page.title}: ${page.purpose}`));
|
|
19164
|
+
lines.push("", `Product review: ${output.productReview.ok ? "pass" : "fail"}`);
|
|
19165
|
+
return lines.join(`
|
|
19166
|
+
`);
|
|
19167
|
+
}
|
|
19168
|
+
|
|
19169
|
+
// src/vibe-command.ts
|
|
18688
19170
|
var PRESETS = ["operations", "enterprise", "collaboration"];
|
|
18689
19171
|
function parseVibeArguments(args) {
|
|
18690
19172
|
const [command, ...rest] = args;
|
|
@@ -18692,6 +19174,31 @@ function parseVibeArguments(args) {
|
|
|
18692
19174
|
return { command };
|
|
18693
19175
|
if (command === "catalog" && rest.length === 0)
|
|
18694
19176
|
return { command };
|
|
19177
|
+
if (command === "plan" || command === "review") {
|
|
19178
|
+
const positional = [];
|
|
19179
|
+
const roleIds = [];
|
|
19180
|
+
for (let i = 0;i < rest.length; i++) {
|
|
19181
|
+
const arg = rest[i];
|
|
19182
|
+
if (arg === undefined)
|
|
19183
|
+
continue;
|
|
19184
|
+
if (arg === "--role") {
|
|
19185
|
+
const roleId = rest[++i];
|
|
19186
|
+
if (!roleId?.trim())
|
|
19187
|
+
throw new Error(`Missing value for --role`);
|
|
19188
|
+
roleIds.push(roleId);
|
|
19189
|
+
} else if (arg.startsWith("-")) {
|
|
19190
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
19191
|
+
} else {
|
|
19192
|
+
positional.push(arg);
|
|
19193
|
+
}
|
|
19194
|
+
}
|
|
19195
|
+
if (positional.length !== 1)
|
|
19196
|
+
throw new Error(`vibe ${command} requires exactly one product plan JSON file`);
|
|
19197
|
+
const planFile = positional[0];
|
|
19198
|
+
if (planFile === undefined)
|
|
19199
|
+
throw new Error("Missing product plan JSON file");
|
|
19200
|
+
return { command, planFile: path6.resolve(planFile), roleIds };
|
|
19201
|
+
}
|
|
18695
19202
|
if (command === "catalog" && rest.length === 2 && rest[0] === "--query" && rest[1]?.trim()) {
|
|
18696
19203
|
return { command, query: rest[1] };
|
|
18697
19204
|
}
|
|
@@ -18699,9 +19206,11 @@ function parseVibeArguments(args) {
|
|
|
18699
19206
|
return { command, page: rest[0] };
|
|
18700
19207
|
}
|
|
18701
19208
|
if (command !== "init")
|
|
18702
|
-
throw new Error("Usage: vibe mcp | vibe catalog [--query <keywords>] | vibe inspect <page> | vibe init <directory> [--preset operations|enterprise|collaboration] [--write]");
|
|
19209
|
+
throw new Error("Usage: vibe mcp | vibe catalog [--query <keywords>] | vibe inspect <page> | vibe plan <product-plan.json> [--role <role-id>] | vibe review <product-plan.json> | vibe init <directory> [--preset operations|enterprise|collaboration] [--product-plan <file>] [--role <role-id>] [--write]");
|
|
18703
19210
|
let preset = "enterprise";
|
|
18704
19211
|
let write = false;
|
|
19212
|
+
let productPlanFile;
|
|
19213
|
+
const roleIds = [];
|
|
18705
19214
|
const positional = [];
|
|
18706
19215
|
for (let i = 0;i < rest.length; i++) {
|
|
18707
19216
|
const arg = rest[i];
|
|
@@ -18714,6 +19223,16 @@ function parseVibeArguments(args) {
|
|
|
18714
19223
|
if (!PRESETS.some((p) => p === value))
|
|
18715
19224
|
throw new Error(`Unknown design preset: ${value ?? "(missing)"}`);
|
|
18716
19225
|
preset = value;
|
|
19226
|
+
} else if (arg === "--product-plan" || arg === "--plan") {
|
|
19227
|
+
const value = rest[++i];
|
|
19228
|
+
if (!value?.trim())
|
|
19229
|
+
throw new Error(`Missing value for ${arg}`);
|
|
19230
|
+
productPlanFile = path6.resolve(value);
|
|
19231
|
+
} else if (arg === "--role") {
|
|
19232
|
+
const value = rest[++i];
|
|
19233
|
+
if (!value?.trim())
|
|
19234
|
+
throw new Error("Missing value for --role");
|
|
19235
|
+
roleIds.push(value);
|
|
18717
19236
|
} else if (arg.startsWith("-"))
|
|
18718
19237
|
throw new Error(`Unknown option: ${arg}`);
|
|
18719
19238
|
else
|
|
@@ -18724,7 +19243,7 @@ function parseVibeArguments(args) {
|
|
|
18724
19243
|
const directory = positional[0];
|
|
18725
19244
|
if (directory === undefined)
|
|
18726
19245
|
throw new Error("Missing project directory");
|
|
18727
|
-
return { command, directory: path6.resolve(directory), preset, write };
|
|
19246
|
+
return { command, directory: path6.resolve(directory), preset, write, productPlanFile, roleIds };
|
|
18728
19247
|
}
|
|
18729
19248
|
function readTree(directory, prefix = "") {
|
|
18730
19249
|
const files = {};
|
|
@@ -18740,8 +19259,119 @@ function readTree(directory, prefix = "") {
|
|
|
18740
19259
|
}
|
|
18741
19260
|
return files;
|
|
18742
19261
|
}
|
|
18743
|
-
function
|
|
18744
|
-
|
|
19262
|
+
function productRoute(surface) {
|
|
19263
|
+
return surface.route ?? "/workflows/" + surface.id;
|
|
19264
|
+
}
|
|
19265
|
+
function buildProductGeneratedFiles(plan, selectedRoleIds) {
|
|
19266
|
+
const allRoleIds = plan.roles.map((role) => role.id);
|
|
19267
|
+
const roleIds = selectedRoleIds.length ? selectedRoleIds : allRoleIds;
|
|
19268
|
+
const planJson = JSON.stringify(plan, null, 2);
|
|
19269
|
+
const summary = [
|
|
19270
|
+
"# " + plan.brief.name,
|
|
19271
|
+
"",
|
|
19272
|
+
plan.brief.description,
|
|
19273
|
+
"",
|
|
19274
|
+
`## Primary outcome
|
|
19275
|
+
` + plan.brief.primaryOutcome,
|
|
19276
|
+
`## Audience
|
|
19277
|
+
` + plan.brief.primaryAudience,
|
|
19278
|
+
`## Roles
|
|
19279
|
+
` + (plan.roles.map((role) => "- **" + role.name + "** (" + role.id + "):" + (role.description ?? "未提供描述")).join(`
|
|
19280
|
+
`) || "- 未定义角色"),
|
|
19281
|
+
`## Workflows
|
|
19282
|
+
` + (plan.workflows.map((workflow) => "- **" + workflow.name + "**:" + workflow.goal + "(" + workflow.priority + " / " + workflow.frequency + ")").join(`
|
|
19283
|
+
`) || "- 未定义工作流"),
|
|
19284
|
+
`## Surfaces
|
|
19285
|
+
` + (plan.surfaces.map((surface) => "- **" + surface.name + "**:" + productRoute(surface) + "," + surface.purpose).join(`
|
|
19286
|
+
`) || "- 未定义页面"),
|
|
19287
|
+
`## Menu
|
|
19288
|
+
` + (plan.menu.map((item) => "- " + item.label + "(" + item.placement + ")").join(`
|
|
19289
|
+
`) || "- 未定义菜单入口"),
|
|
19290
|
+
"## Generation notes",
|
|
19291
|
+
"- Generated resources are read-only metadata until an explicit contract is added.",
|
|
19292
|
+
"- Product actions are metadata and do not imply backend mutations.",
|
|
19293
|
+
"- Initial visible roles: " + (roleIds.join(", ") || "all") + "."
|
|
19294
|
+
].join(`
|
|
19295
|
+
|
|
19296
|
+
`) + `
|
|
19297
|
+
`;
|
|
19298
|
+
const resourcesSource = [
|
|
19299
|
+
"import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';",
|
|
19300
|
+
"import { productPlan } from './product-plan';",
|
|
19301
|
+
"",
|
|
19302
|
+
"const supportedFieldTypes = new Set<string>(['text','number','boolean','date','time','datetime','daterange','select','multiselect','tags','textarea','richtext','image','images','json','relation','color','url','email','phone','currency','percent','file','markdown','password','array','tree-select','treeselect','cascader','transfer','rate','rating','avatar','copy','code']);",
|
|
19303
|
+
"function toFieldType(type: string): FieldDefinition['type'] {",
|
|
19304
|
+
" return (supportedFieldTypes.has(type) ? type : 'text') as FieldDefinition['type'];",
|
|
19305
|
+
"}",
|
|
19306
|
+
"function toField(field: (typeof productPlan.resources)[number]['fields'][number]): FieldDefinition {",
|
|
19307
|
+
" const hidden = field.sensitive === true || field.disclosure === 'hidden';",
|
|
19308
|
+
" return { key: field.id, label: field.label, type: toFieldType(field.type), required: field.required, showInList: !hidden, showInShow: !hidden, showInForm: !hidden, showInCreate: !hidden, showInEdit: !hidden };",
|
|
19309
|
+
"}",
|
|
19310
|
+
"export const productResources: ResourceDefinition[] = productPlan.resources.map(resource => ({",
|
|
19311
|
+
" name: resource.runtimeName ?? resource.name, label: resource.label,",
|
|
19312
|
+
" icon: resource.menuPlacement === 'settings' ? 'settings' : undefined,",
|
|
19313
|
+
" fields: resource.fields.map(toField), canCreate: false, canEdit: false, canDelete: false, canShow: true,",
|
|
19314
|
+
" showInMenu: resource.internal !== true && resource.menuPlacement !== 'hidden' && resource.menuPlacement !== 'context' && resource.menuPlacement !== 'command',",
|
|
19315
|
+
"}));",
|
|
19316
|
+
""
|
|
19317
|
+
].join(`
|
|
19318
|
+
`);
|
|
19319
|
+
const generatedPlan = [
|
|
19320
|
+
"import { parseProductPlan, type ProductPlan } from '@svadmin/core/product-plan';",
|
|
19321
|
+
"",
|
|
19322
|
+
"export const productPlan: ProductPlan = parseProductPlan(" + planJson + ");",
|
|
19323
|
+
""
|
|
19324
|
+
].join(`
|
|
19325
|
+
`);
|
|
19326
|
+
const generatedNavigation = [
|
|
19327
|
+
"import type { MenuItem } from '@svadmin/core';",
|
|
19328
|
+
"import { buildProductMenuItems } from '@svadmin/core/product-navigation';",
|
|
19329
|
+
"import { productPlan } from './product-plan';",
|
|
19330
|
+
"",
|
|
19331
|
+
"export const ALL_PRODUCT_ROLE_IDS = " + JSON.stringify(allRoleIds) + " as const;",
|
|
19332
|
+
"export const INITIAL_PRODUCT_ROLE_IDS = " + JSON.stringify(roleIds) + " as const;",
|
|
19333
|
+
"export function createProductMenu(roleIds: readonly string[] = INITIAL_PRODUCT_ROLE_IDS, featureFlags: Readonly<Record<string, boolean>> = {}): MenuItem[] {",
|
|
19334
|
+
" return buildProductMenuItems(productPlan, { roleIds, featureFlags });",
|
|
19335
|
+
"}",
|
|
19336
|
+
"export const productMenu = createProductMenu();",
|
|
19337
|
+
""
|
|
19338
|
+
].join(`
|
|
19339
|
+
`);
|
|
19340
|
+
const generatedPages = [
|
|
19341
|
+
"import { runProductPipeline } from '@svadmin/core/product-pipeline';",
|
|
19342
|
+
"import { productPlan } from './product-plan';",
|
|
19343
|
+
"import { INITIAL_PRODUCT_ROLE_IDS } from './navigation';",
|
|
19344
|
+
"",
|
|
19345
|
+
"export const productPipeline = runProductPipeline({ plan: productPlan, navigation: { roleIds: [...INITIAL_PRODUCT_ROLE_IDS] } });",
|
|
19346
|
+
"export const productPagePlans = productPipeline.pages;",
|
|
19347
|
+
"export const productSurfaceRoutes = Object.fromEntries(productPlan.surfaces.map(surface => [surface.route ?? '/workflows/' + surface.id, surface.id])) as Record<string, string>;",
|
|
19348
|
+
""
|
|
19349
|
+
].join(`
|
|
19350
|
+
`);
|
|
19351
|
+
const generatedRoutes = [
|
|
19352
|
+
"import type { AdminPageRoute } from '@svadmin/ui';",
|
|
19353
|
+
"import ProductSurfacePage from '../ProductSurfacePage.svelte';",
|
|
19354
|
+
"import { productSurfaceRoutes } from './pages';",
|
|
19355
|
+
"",
|
|
19356
|
+
"export const productPageRoutes: readonly AdminPageRoute[] = Object.keys(productSurfaceRoutes).map(path => ({ path, component: ProductSurfacePage }));",
|
|
19357
|
+
""
|
|
19358
|
+
].join(`
|
|
19359
|
+
`);
|
|
19360
|
+
return {
|
|
19361
|
+
"svadmin.product.json": planJson + `
|
|
19362
|
+
`,
|
|
19363
|
+
"PRODUCT.md": summary,
|
|
19364
|
+
"src/generated/product-plan.ts": generatedPlan,
|
|
19365
|
+
"src/generated/resources.ts": resourcesSource,
|
|
19366
|
+
"src/generated/navigation.ts": generatedNavigation,
|
|
19367
|
+
"src/generated/pages.ts": generatedPages,
|
|
19368
|
+
"src/generated/page-routes.ts": generatedRoutes,
|
|
19369
|
+
"src/generated/README.md": `Generated product files. Derived from svadmin.product.json; regenerate instead of editing by hand.
|
|
19370
|
+
`
|
|
19371
|
+
};
|
|
19372
|
+
}
|
|
19373
|
+
function planVibeProject(packageRoot, manifest, preset, options = {}) {
|
|
19374
|
+
const blueprint = path6.join(packageRoot, options.productPlan ? "blueprints/product-workspace" : "blueprints/customer-workspace");
|
|
18745
19375
|
const files = readTree(blueprint);
|
|
18746
19376
|
const template = path6.join(packageRoot, "template");
|
|
18747
19377
|
for (const name of ["index.html", "svelte.config.js", "tsconfig.json", "vite.config.ts", "src/main.ts", "src/vite-env.d.ts"]) {
|
|
@@ -18752,15 +19382,19 @@ function planVibeProject(packageRoot, manifest, preset) {
|
|
|
18752
19382
|
}
|
|
18753
19383
|
files["DESIGN.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/DESIGN.md"), "utf8");
|
|
18754
19384
|
files["AGENTS.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/AGENTS.md"), "utf8");
|
|
19385
|
+
files["PRODUCT.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/PRODUCT.md"), "utf8");
|
|
19386
|
+
if (options.productPlan)
|
|
19387
|
+
Object.assign(files, buildProductGeneratedFiles(options.productPlan, options.roleIds ?? []));
|
|
18755
19388
|
const skill = files["vibe-skill.md"];
|
|
18756
19389
|
if (typeof skill !== "string")
|
|
18757
19390
|
throw new Error("The shipped vibe skill is missing");
|
|
18758
19391
|
files[".agents/skills/svadmin-vibe/SKILL.md"] = skill;
|
|
18759
19392
|
delete files["vibe-skill.md"];
|
|
18760
|
-
|
|
19393
|
+
if (!options.productPlan)
|
|
19394
|
+
files["src/design-selection.ts"] = `export const initialPreset = '${preset}' as const;
|
|
18761
19395
|
`;
|
|
18762
19396
|
const pkg = createProjectPackageJson(manifest, {
|
|
18763
|
-
projectName: "svadmin-customer-workspace",
|
|
19397
|
+
projectName: options.productPlan ? "svadmin-product-workspace" : "svadmin-customer-workspace",
|
|
18764
19398
|
dataProvider: "none",
|
|
18765
19399
|
authProvider: "none"
|
|
18766
19400
|
});
|
|
@@ -18777,31 +19411,36 @@ function planVibeProject(packageRoot, manifest, preset) {
|
|
|
18777
19411
|
authProvider: "none",
|
|
18778
19412
|
...manifest.dependencies["@svadmin/core"] === undefined ? {} : { coreVersionRange: manifest.dependencies["@svadmin/core"] }
|
|
18779
19413
|
});
|
|
18780
|
-
|
|
18781
|
-
name: resource.name,
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
19414
|
+
if (options.productPlan) {
|
|
19415
|
+
ai.resources = options.productPlan.resources.map((resource) => ({ name: resource.runtimeName ?? resource.name, label: resource.label, fields: resource.fields.map((field) => ({ key: field.id, label: field.label, type: field.type, required: field.required ?? false })), operations: ["list", "show"] }));
|
|
19416
|
+
ai.routes = options.productPlan.surfaces.map((surface) => ({ resource: surface.resourceIds[0] ?? "", path: productRoute(surface), operations: ["list", "show"] }));
|
|
19417
|
+
} else {
|
|
19418
|
+
ai.resources = resources.map((resource) => ({
|
|
19419
|
+
name: resource.name,
|
|
19420
|
+
label: resource.label ?? resource.name,
|
|
19421
|
+
fields: (resource.fields ?? []).map((field) => ({
|
|
19422
|
+
key: field.key,
|
|
19423
|
+
label: field.label,
|
|
19424
|
+
type: field.type,
|
|
19425
|
+
required: field.required ?? false
|
|
19426
|
+
})),
|
|
19427
|
+
operations: uiOnlyResources.includes(resource.name) ? [] : [
|
|
19428
|
+
"list",
|
|
19429
|
+
...resource.canShow === false ? [] : ["show"],
|
|
19430
|
+
...resource.canCreate === false ? [] : ["create"],
|
|
19431
|
+
...resource.canEdit === false ? [] : ["edit"]
|
|
19432
|
+
]
|
|
19433
|
+
}));
|
|
19434
|
+
ai.routes = ai.resources.map((resource) => ({
|
|
19435
|
+
resource: resource.name,
|
|
19436
|
+
path: `/${resource.name}`,
|
|
19437
|
+
operations: uiOnlyResources.includes(resource.name) ? ["list"] : resource.operations
|
|
19438
|
+
}));
|
|
19439
|
+
}
|
|
18801
19440
|
ai.providers.data = { choice: "none", package: null, capabilities: ["in-memory-demo", "data", "filter", "sort", "pagination"] };
|
|
18802
19441
|
ai.project.commands = pkg.scripts;
|
|
18803
19442
|
ai.project.testCommand = "bun run test:ui";
|
|
18804
|
-
ai.guidance = [...ai.guidance, "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
|
|
19443
|
+
ai.guidance = [...ai.guidance, "PRODUCT.md", "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
|
|
18805
19444
|
files["svadmin.ai.json"] = `${JSON.stringify(ai, null, 2)}
|
|
18806
19445
|
`;
|
|
18807
19446
|
files["svadmin.schema.json"] = `${JSON.stringify(buildAdminSchemaJson(), null, 2)}
|
|
@@ -18850,10 +19489,35 @@ async function vibeCommand(args, packageRoot, manifest) {
|
|
|
18850
19489
|
console.log(JSON.stringify(inspectVibePage(packageRoot, options.page), null, 2));
|
|
18851
19490
|
return;
|
|
18852
19491
|
}
|
|
19492
|
+
if (options.command === "review" || options.command === "plan") {
|
|
19493
|
+
const input = JSON.parse(fs6.readFileSync(options.planFile, "utf8"));
|
|
19494
|
+
const plan = parseProductPlan(input);
|
|
19495
|
+
if (options.command === "review") {
|
|
19496
|
+
console.log(JSON.stringify(reviewProductPlan(plan), null, 2));
|
|
19497
|
+
return;
|
|
19498
|
+
}
|
|
19499
|
+
const roleIds = options.roleIds.length ? options.roleIds : plan.roles.map((role) => role.id);
|
|
19500
|
+
console.log(formatProductPreview(runProductPipeline({ plan, navigation: { roleIds } })));
|
|
19501
|
+
return;
|
|
19502
|
+
}
|
|
19503
|
+
if (options.command !== "init")
|
|
19504
|
+
throw new Error("Unsupported vibe command");
|
|
18853
19505
|
if (fs6.existsSync(options.directory))
|
|
18854
19506
|
throw new Error("The target already exists. Choose a new directory; existing projects are never overwritten.");
|
|
18855
|
-
|
|
18856
|
-
|
|
19507
|
+
let productPlan;
|
|
19508
|
+
if (options.productPlanFile) {
|
|
19509
|
+
const input = JSON.parse(fs6.readFileSync(options.productPlanFile, "utf8"));
|
|
19510
|
+
productPlan = parseProductPlan(input);
|
|
19511
|
+
const report = reviewProductPlan(productPlan);
|
|
19512
|
+
if (!report.ok)
|
|
19513
|
+
throw new Error(`Product plan review failed:
|
|
19514
|
+
` + JSON.stringify(report, null, 2));
|
|
19515
|
+
if (report.issues.length)
|
|
19516
|
+
console.warn("Product plan review warnings: " + report.issues.length);
|
|
19517
|
+
}
|
|
19518
|
+
const productOptions = { roleIds: options.roleIds, ...productPlan === undefined ? {} : { productPlan } };
|
|
19519
|
+
const files = planVibeProject(packageRoot, manifest, options.preset, productOptions);
|
|
19520
|
+
console.log(`svadmin vibe: ${productPlan ? "product-workspace" : "customer-workspace"} / ${options.preset}
|
|
18857
19521
|
${options.directory}`);
|
|
18858
19522
|
for (const file of Object.keys(files).sort())
|
|
18859
19523
|
console.log(` add ${file}`);
|
|
@@ -18991,7 +19655,7 @@ function upgrade(args) {
|
|
|
18991
19655
|
const upgradeExecution = commandArguments.write ? writeProjectPackageJsonUpgrade(packagePath, scaffoldManifest, new Date) : planProjectPackageFileUpgrade(packagePath, scaffoldManifest);
|
|
18992
19656
|
printUpgradeExecution(upgradeExecution, commandArguments.projectDirectory, packagePath);
|
|
18993
19657
|
}
|
|
18994
|
-
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md"];
|
|
19658
|
+
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md", "PRODUCT.md"];
|
|
18995
19659
|
function missingGuidanceFiles(projectDirectory) {
|
|
18996
19660
|
return GUIDANCE_FILES.filter((fileName) => !fs7.existsSync(path7.join(projectDirectory, fileName)));
|
|
18997
19661
|
}
|