@svadmin/create 0.40.2 → 0.41.0
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 +89 -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 +614 -32
- package/guidance/AGENTS.md +11 -0
- package/guidance/DESIGN.md +117 -8
- 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,406 @@ 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) => ({ id: `${surface.id}.section.${index + 1}`, title, resourceIds: surface.resourceIds })) : [{ id: `${surface.id}.content`, title: surface.name, resourceIds: surface.resourceIds }];
|
|
18927
|
+
return {
|
|
18928
|
+
id: surface.id,
|
|
18929
|
+
kind: surface.kind,
|
|
18930
|
+
title: surface.name,
|
|
18931
|
+
purpose: surface.purpose,
|
|
18932
|
+
workflowIds: surface.workflowIds,
|
|
18933
|
+
roleIds: surface.roleIds,
|
|
18934
|
+
...primaryAction === undefined ? {} : { primaryAction },
|
|
18935
|
+
secondaryActions,
|
|
18936
|
+
sections,
|
|
18937
|
+
states: surface.states
|
|
18938
|
+
};
|
|
18939
|
+
}
|
|
18940
|
+
|
|
18941
|
+
// ../core/src/product-review.ts
|
|
18942
|
+
function has2(set, value) {
|
|
18943
|
+
return set.has(value);
|
|
18944
|
+
}
|
|
18945
|
+
function addIssue(issues, issue) {
|
|
18946
|
+
issues.push(issue);
|
|
18947
|
+
}
|
|
18948
|
+
function reviewProductPlan(plan) {
|
|
18949
|
+
const issues = [];
|
|
18950
|
+
const roleIds = new Set(plan.roles.map((role) => role.id));
|
|
18951
|
+
const workflowIds = new Set(plan.workflows.map((workflow) => workflow.id));
|
|
18952
|
+
const resourceIds = new Set(plan.resources.map((resource) => resource.id));
|
|
18953
|
+
const surfaceIds = new Set(plan.surfaces.map((surface) => surface.id));
|
|
18954
|
+
const actionIds = new Set(plan.actions.map((action) => action.id));
|
|
18955
|
+
for (const workflow of plan.workflows) {
|
|
18956
|
+
if (!workflow.actorRoleIds.length)
|
|
18957
|
+
addIssue(issues, { code: "workflow.missing-actor", severity: "error", message: `Workflow "${workflow.name}" has no actor role.`, path: `workflows.${workflow.id}.actorRoleIds` });
|
|
18958
|
+
for (const roleId of workflow.actorRoleIds)
|
|
18959
|
+
if (!has2(roleIds, roleId))
|
|
18960
|
+
addIssue(issues, { code: "workflow.unknown-role", severity: "error", message: `Workflow "${workflow.name}" references unknown role "${roleId}".`, path: `workflows.${workflow.id}.actorRoleIds` });
|
|
18961
|
+
if (!workflow.goal.trim())
|
|
18962
|
+
addIssue(issues, { code: "workflow.missing-goal", severity: "error", message: `Workflow "${workflow.name}" has no user goal.`, path: `workflows.${workflow.id}.goal` });
|
|
18963
|
+
}
|
|
18964
|
+
for (const resource of plan.resources) {
|
|
18965
|
+
if (!resource.workflowIds.length && !resource.internal)
|
|
18966
|
+
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." });
|
|
18967
|
+
for (const workflowId of resource.workflowIds)
|
|
18968
|
+
if (!has2(workflowIds, workflowId))
|
|
18969
|
+
addIssue(issues, { code: "resource.unknown-workflow", severity: "error", message: `Resource "${resource.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.workflowIds` });
|
|
18970
|
+
}
|
|
18971
|
+
for (const surface of plan.surfaces) {
|
|
18972
|
+
reviewSurface(surface, { workflowIds, actionIds, resourceIds }, issues);
|
|
18973
|
+
}
|
|
18974
|
+
const menuById = new Map(plan.menu.map((item) => [item.id, item]));
|
|
18975
|
+
for (const item of plan.menu) {
|
|
18976
|
+
if (item.parentId !== undefined && !menuById.has(item.parentId)) {
|
|
18977
|
+
addIssue(issues, { code: "menu.unknown-parent", severity: "error", message: `Menu item "${item.label}" references unknown parent "${item.parentId}".`, path: `menu.${item.id}.parentId` });
|
|
18978
|
+
}
|
|
18979
|
+
}
|
|
18980
|
+
const visiting = new Set;
|
|
18981
|
+
const visited = new Set;
|
|
18982
|
+
const visitMenu = (id) => {
|
|
18983
|
+
if (visiting.has(id)) {
|
|
18984
|
+
addIssue(issues, { code: "menu.cycle", severity: "error", message: `Menu hierarchy contains a cycle at "${id}".`, path: `menu.${id}.parentId` });
|
|
18985
|
+
return;
|
|
18986
|
+
}
|
|
18987
|
+
if (visited.has(id))
|
|
18988
|
+
return;
|
|
18989
|
+
visiting.add(id);
|
|
18990
|
+
const parentId = menuById.get(id)?.parentId;
|
|
18991
|
+
if (parentId !== undefined && menuById.has(parentId))
|
|
18992
|
+
visitMenu(parentId);
|
|
18993
|
+
visiting.delete(id);
|
|
18994
|
+
visited.add(id);
|
|
18995
|
+
};
|
|
18996
|
+
for (const item of plan.menu)
|
|
18997
|
+
visitMenu(item.id);
|
|
18998
|
+
for (const item of plan.menu) {
|
|
18999
|
+
if (!item.workflowIds.length && !item.resourceId && !item.surfaceId)
|
|
19000
|
+
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." });
|
|
19001
|
+
if (item.surfaceId && !has2(surfaceIds, item.surfaceId))
|
|
19002
|
+
addIssue(issues, { code: "menu.unknown-surface", severity: "error", message: `Menu item "${item.label}" references unknown surface.`, path: `menu.${item.id}.surfaceId` });
|
|
19003
|
+
if (item.resourceId && !has2(resourceIds, item.resourceId))
|
|
19004
|
+
addIssue(issues, { code: "menu.unknown-resource", severity: "error", message: `Menu item "${item.label}" references unknown resource.`, path: `menu.${item.id}.resourceId` });
|
|
19005
|
+
if (!item.roleIds.length && item.placement !== "hidden")
|
|
19006
|
+
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." });
|
|
19007
|
+
}
|
|
19008
|
+
for (const action of plan.actions) {
|
|
19009
|
+
if (!has2(workflowIds, action.workflowId))
|
|
19010
|
+
addIssue(issues, { code: "action.unknown-workflow", severity: "error", message: `Action "${action.label}" references unknown workflow.`, path: `actions.${action.id}.workflowId` });
|
|
19011
|
+
if (!action.roleIds.length)
|
|
19012
|
+
addIssue(issues, { code: "action.missing-roles", severity: "warning", message: `Action "${action.label}" has no role visibility contract.`, path: `actions.${action.id}.roleIds` });
|
|
19013
|
+
}
|
|
19014
|
+
const served = new Set;
|
|
19015
|
+
plan.surfaces.forEach((surface) => surface.workflowIds.forEach((id) => served.add(id)));
|
|
19016
|
+
plan.menu.forEach((item) => item.workflowIds.forEach((id) => served.add(id)));
|
|
19017
|
+
const unserved = plan.workflows.filter((workflow) => !served.has(workflow.id));
|
|
19018
|
+
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." }));
|
|
19019
|
+
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;
|
|
19020
|
+
const orphanResourceCount = plan.resources.filter((resource) => resource.workflowIds.length === 0 && !resource.internal).length;
|
|
19021
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
19022
|
+
return {
|
|
19023
|
+
ok: errors.length === 0,
|
|
19024
|
+
issues,
|
|
19025
|
+
metrics: {
|
|
19026
|
+
resourceCount: plan.resources.length,
|
|
19027
|
+
workflowCount: plan.workflows.length,
|
|
19028
|
+
surfaceCount: plan.surfaces.length,
|
|
19029
|
+
menuNodeCount: plan.menu.length,
|
|
19030
|
+
orphanResourceCount,
|
|
19031
|
+
unservedWorkflowCount: unserved.length,
|
|
19032
|
+
roleVisibilityGaps
|
|
19033
|
+
}
|
|
19034
|
+
};
|
|
19035
|
+
}
|
|
19036
|
+
function reviewSurface(surface, refs, issues) {
|
|
19037
|
+
if (!surface.purpose.trim())
|
|
19038
|
+
addIssue(issues, { code: "surface.missing-purpose", severity: "error", message: `Surface "${surface.name}" has no purpose.`, path: `surfaces.${surface.id}.purpose` });
|
|
19039
|
+
if (!surface.workflowIds.length)
|
|
19040
|
+
addIssue(issues, { code: "surface.no-workflow", severity: "warning", message: `Surface "${surface.name}" is not attached to a workflow.`, path: `surfaces.${surface.id}.workflowIds` });
|
|
19041
|
+
surface.workflowIds.forEach((id) => {
|
|
19042
|
+
if (!refs.workflowIds.has(id))
|
|
19043
|
+
addIssue(issues, { code: "surface.unknown-workflow", severity: "error", message: `Surface "${surface.name}" references unknown workflow "${id}".`, path: `surfaces.${surface.id}.workflowIds` });
|
|
19044
|
+
});
|
|
19045
|
+
surface.resourceIds.forEach((id) => {
|
|
19046
|
+
if (!refs.resourceIds.has(id))
|
|
19047
|
+
addIssue(issues, { code: "surface.unknown-resource", severity: "error", message: `Surface "${surface.name}" references unknown resource "${id}".`, path: `surfaces.${surface.id}.resourceIds` });
|
|
19048
|
+
});
|
|
19049
|
+
surface.actionIds.forEach((id) => {
|
|
19050
|
+
if (!refs.actionIds.has(id))
|
|
19051
|
+
addIssue(issues, { code: "surface.unknown-action", severity: "error", message: `Surface "${surface.name}" references unknown action "${id}".`, path: `surfaces.${surface.id}.actionIds` });
|
|
19052
|
+
});
|
|
19053
|
+
if (surface.primaryActionId && !refs.actionIds.has(surface.primaryActionId))
|
|
19054
|
+
addIssue(issues, { code: "surface.unknown-primary-action", severity: "error", message: `Surface "${surface.name}" references unknown primary action.`, path: `surfaces.${surface.id}.primaryActionId` });
|
|
19055
|
+
if (!surface.states.length)
|
|
19056
|
+
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." });
|
|
19057
|
+
if (surface.sections.length > 6)
|
|
19058
|
+
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." });
|
|
19059
|
+
if (surface.actionIds.length > 4)
|
|
19060
|
+
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." });
|
|
19061
|
+
}
|
|
19062
|
+
|
|
19063
|
+
// ../core/src/product-pipeline.ts
|
|
19064
|
+
function runProductPipeline(input) {
|
|
19065
|
+
const navigation = buildProductNavigation(input.plan, input.navigation);
|
|
19066
|
+
const visibleSurfaceIds = input.surfaceIds === undefined ? undefined : new Set(input.surfaceIds);
|
|
19067
|
+
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));
|
|
19068
|
+
return { navigation, pages, productReview: reviewProductPlan(input.plan) };
|
|
19069
|
+
}
|
|
19070
|
+
function formatProductPreview(output) {
|
|
19071
|
+
const lines = ["Navigation:"];
|
|
19072
|
+
for (const node of output.navigation) {
|
|
19073
|
+
if (!node.visible)
|
|
19074
|
+
continue;
|
|
19075
|
+
lines.push(`- ${node.label}`);
|
|
19076
|
+
for (const child of node.children)
|
|
19077
|
+
if (child.visible)
|
|
19078
|
+
lines.push(` - ${child.label}`);
|
|
19079
|
+
}
|
|
19080
|
+
lines.push("", "Pages:");
|
|
19081
|
+
output.pages.forEach((page) => lines.push(`- ${page.title}: ${page.purpose}`));
|
|
19082
|
+
lines.push("", `Product review: ${output.productReview.ok ? "pass" : "fail"}`);
|
|
19083
|
+
return lines.join(`
|
|
19084
|
+
`);
|
|
19085
|
+
}
|
|
19086
|
+
|
|
19087
|
+
// src/vibe-command.ts
|
|
18688
19088
|
var PRESETS = ["operations", "enterprise", "collaboration"];
|
|
18689
19089
|
function parseVibeArguments(args) {
|
|
18690
19090
|
const [command, ...rest] = args;
|
|
@@ -18692,6 +19092,31 @@ function parseVibeArguments(args) {
|
|
|
18692
19092
|
return { command };
|
|
18693
19093
|
if (command === "catalog" && rest.length === 0)
|
|
18694
19094
|
return { command };
|
|
19095
|
+
if (command === "plan" || command === "review") {
|
|
19096
|
+
const positional = [];
|
|
19097
|
+
const roleIds = [];
|
|
19098
|
+
for (let i = 0;i < rest.length; i++) {
|
|
19099
|
+
const arg = rest[i];
|
|
19100
|
+
if (arg === undefined)
|
|
19101
|
+
continue;
|
|
19102
|
+
if (arg === "--role") {
|
|
19103
|
+
const roleId = rest[++i];
|
|
19104
|
+
if (!roleId?.trim())
|
|
19105
|
+
throw new Error(`Missing value for --role`);
|
|
19106
|
+
roleIds.push(roleId);
|
|
19107
|
+
} else if (arg.startsWith("-")) {
|
|
19108
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
19109
|
+
} else {
|
|
19110
|
+
positional.push(arg);
|
|
19111
|
+
}
|
|
19112
|
+
}
|
|
19113
|
+
if (positional.length !== 1)
|
|
19114
|
+
throw new Error(`vibe ${command} requires exactly one product plan JSON file`);
|
|
19115
|
+
const planFile = positional[0];
|
|
19116
|
+
if (planFile === undefined)
|
|
19117
|
+
throw new Error("Missing product plan JSON file");
|
|
19118
|
+
return { command, planFile: path6.resolve(planFile), roleIds };
|
|
19119
|
+
}
|
|
18695
19120
|
if (command === "catalog" && rest.length === 2 && rest[0] === "--query" && rest[1]?.trim()) {
|
|
18696
19121
|
return { command, query: rest[1] };
|
|
18697
19122
|
}
|
|
@@ -18699,9 +19124,11 @@ function parseVibeArguments(args) {
|
|
|
18699
19124
|
return { command, page: rest[0] };
|
|
18700
19125
|
}
|
|
18701
19126
|
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]");
|
|
19127
|
+
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
19128
|
let preset = "enterprise";
|
|
18704
19129
|
let write = false;
|
|
19130
|
+
let productPlanFile;
|
|
19131
|
+
const roleIds = [];
|
|
18705
19132
|
const positional = [];
|
|
18706
19133
|
for (let i = 0;i < rest.length; i++) {
|
|
18707
19134
|
const arg = rest[i];
|
|
@@ -18714,6 +19141,16 @@ function parseVibeArguments(args) {
|
|
|
18714
19141
|
if (!PRESETS.some((p) => p === value))
|
|
18715
19142
|
throw new Error(`Unknown design preset: ${value ?? "(missing)"}`);
|
|
18716
19143
|
preset = value;
|
|
19144
|
+
} else if (arg === "--product-plan" || arg === "--plan") {
|
|
19145
|
+
const value = rest[++i];
|
|
19146
|
+
if (!value?.trim())
|
|
19147
|
+
throw new Error(`Missing value for ${arg}`);
|
|
19148
|
+
productPlanFile = path6.resolve(value);
|
|
19149
|
+
} else if (arg === "--role") {
|
|
19150
|
+
const value = rest[++i];
|
|
19151
|
+
if (!value?.trim())
|
|
19152
|
+
throw new Error("Missing value for --role");
|
|
19153
|
+
roleIds.push(value);
|
|
18717
19154
|
} else if (arg.startsWith("-"))
|
|
18718
19155
|
throw new Error(`Unknown option: ${arg}`);
|
|
18719
19156
|
else
|
|
@@ -18724,7 +19161,7 @@ function parseVibeArguments(args) {
|
|
|
18724
19161
|
const directory = positional[0];
|
|
18725
19162
|
if (directory === undefined)
|
|
18726
19163
|
throw new Error("Missing project directory");
|
|
18727
|
-
return { command, directory: path6.resolve(directory), preset, write };
|
|
19164
|
+
return { command, directory: path6.resolve(directory), preset, write, productPlanFile, roleIds };
|
|
18728
19165
|
}
|
|
18729
19166
|
function readTree(directory, prefix = "") {
|
|
18730
19167
|
const files = {};
|
|
@@ -18740,8 +19177,119 @@ function readTree(directory, prefix = "") {
|
|
|
18740
19177
|
}
|
|
18741
19178
|
return files;
|
|
18742
19179
|
}
|
|
18743
|
-
function
|
|
18744
|
-
|
|
19180
|
+
function productRoute(surface) {
|
|
19181
|
+
return surface.route ?? "/workflows/" + surface.id;
|
|
19182
|
+
}
|
|
19183
|
+
function buildProductGeneratedFiles(plan, selectedRoleIds) {
|
|
19184
|
+
const allRoleIds = plan.roles.map((role) => role.id);
|
|
19185
|
+
const roleIds = selectedRoleIds.length ? selectedRoleIds : allRoleIds;
|
|
19186
|
+
const planJson = JSON.stringify(plan, null, 2);
|
|
19187
|
+
const summary = [
|
|
19188
|
+
"# " + plan.brief.name,
|
|
19189
|
+
"",
|
|
19190
|
+
plan.brief.description,
|
|
19191
|
+
"",
|
|
19192
|
+
`## Primary outcome
|
|
19193
|
+
` + plan.brief.primaryOutcome,
|
|
19194
|
+
`## Audience
|
|
19195
|
+
` + plan.brief.primaryAudience,
|
|
19196
|
+
`## Roles
|
|
19197
|
+
` + (plan.roles.map((role) => "- **" + role.name + "** (" + role.id + "):" + (role.description ?? "未提供描述")).join(`
|
|
19198
|
+
`) || "- 未定义角色"),
|
|
19199
|
+
`## Workflows
|
|
19200
|
+
` + (plan.workflows.map((workflow) => "- **" + workflow.name + "**:" + workflow.goal + "(" + workflow.priority + " / " + workflow.frequency + ")").join(`
|
|
19201
|
+
`) || "- 未定义工作流"),
|
|
19202
|
+
`## Surfaces
|
|
19203
|
+
` + (plan.surfaces.map((surface) => "- **" + surface.name + "**:" + productRoute(surface) + "," + surface.purpose).join(`
|
|
19204
|
+
`) || "- 未定义页面"),
|
|
19205
|
+
`## Menu
|
|
19206
|
+
` + (plan.menu.map((item) => "- " + item.label + "(" + item.placement + ")").join(`
|
|
19207
|
+
`) || "- 未定义菜单入口"),
|
|
19208
|
+
"## Generation notes",
|
|
19209
|
+
"- Generated resources are read-only metadata until an explicit contract is added.",
|
|
19210
|
+
"- Product actions are metadata and do not imply backend mutations.",
|
|
19211
|
+
"- Initial visible roles: " + (roleIds.join(", ") || "all") + "."
|
|
19212
|
+
].join(`
|
|
19213
|
+
|
|
19214
|
+
`) + `
|
|
19215
|
+
`;
|
|
19216
|
+
const resourcesSource = [
|
|
19217
|
+
"import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';",
|
|
19218
|
+
"import { productPlan } from './product-plan';",
|
|
19219
|
+
"",
|
|
19220
|
+
"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']);",
|
|
19221
|
+
"function toFieldType(type: string): FieldDefinition['type'] {",
|
|
19222
|
+
" return (supportedFieldTypes.has(type) ? type : 'text') as FieldDefinition['type'];",
|
|
19223
|
+
"}",
|
|
19224
|
+
"function toField(field: (typeof productPlan.resources)[number]['fields'][number]): FieldDefinition {",
|
|
19225
|
+
" const hidden = field.sensitive === true || field.disclosure === 'hidden';",
|
|
19226
|
+
" return { key: field.id, label: field.label, type: toFieldType(field.type), required: field.required, showInList: !hidden, showInShow: !hidden, showInForm: !hidden, showInCreate: !hidden, showInEdit: !hidden };",
|
|
19227
|
+
"}",
|
|
19228
|
+
"export const productResources: ResourceDefinition[] = productPlan.resources.map(resource => ({",
|
|
19229
|
+
" name: resource.runtimeName ?? resource.name, label: resource.label,",
|
|
19230
|
+
" icon: resource.menuPlacement === 'settings' ? 'settings' : undefined,",
|
|
19231
|
+
" fields: resource.fields.map(toField), canCreate: false, canEdit: false, canDelete: false, canShow: true,",
|
|
19232
|
+
" showInMenu: resource.internal !== true && resource.menuPlacement !== 'hidden' && resource.menuPlacement !== 'context' && resource.menuPlacement !== 'command',",
|
|
19233
|
+
"}));",
|
|
19234
|
+
""
|
|
19235
|
+
].join(`
|
|
19236
|
+
`);
|
|
19237
|
+
const generatedPlan = [
|
|
19238
|
+
"import { parseProductPlan, type ProductPlan } from '@svadmin/core/product-plan';",
|
|
19239
|
+
"",
|
|
19240
|
+
"export const productPlan: ProductPlan = parseProductPlan(" + planJson + ");",
|
|
19241
|
+
""
|
|
19242
|
+
].join(`
|
|
19243
|
+
`);
|
|
19244
|
+
const generatedNavigation = [
|
|
19245
|
+
"import type { MenuItem } from '@svadmin/core';",
|
|
19246
|
+
"import { buildProductMenuItems } from '@svadmin/core/product-navigation';",
|
|
19247
|
+
"import { productPlan } from './product-plan';",
|
|
19248
|
+
"",
|
|
19249
|
+
"export const ALL_PRODUCT_ROLE_IDS = " + JSON.stringify(allRoleIds) + " as const;",
|
|
19250
|
+
"export const INITIAL_PRODUCT_ROLE_IDS = " + JSON.stringify(roleIds) + " as const;",
|
|
19251
|
+
"export function createProductMenu(roleIds: readonly string[] = INITIAL_PRODUCT_ROLE_IDS, featureFlags: Readonly<Record<string, boolean>> = {}): MenuItem[] {",
|
|
19252
|
+
" return buildProductMenuItems(productPlan, { roleIds, featureFlags });",
|
|
19253
|
+
"}",
|
|
19254
|
+
"export const productMenu = createProductMenu();",
|
|
19255
|
+
""
|
|
19256
|
+
].join(`
|
|
19257
|
+
`);
|
|
19258
|
+
const generatedPages = [
|
|
19259
|
+
"import { runProductPipeline } from '@svadmin/core/product-pipeline';",
|
|
19260
|
+
"import { productPlan } from './product-plan';",
|
|
19261
|
+
"import { INITIAL_PRODUCT_ROLE_IDS } from './navigation';",
|
|
19262
|
+
"",
|
|
19263
|
+
"export const productPipeline = runProductPipeline({ plan: productPlan, navigation: { roleIds: [...INITIAL_PRODUCT_ROLE_IDS] } });",
|
|
19264
|
+
"export const productPagePlans = productPipeline.pages;",
|
|
19265
|
+
"export const productSurfaceRoutes = Object.fromEntries(productPlan.surfaces.map(surface => [surface.route ?? '/workflows/' + surface.id, surface.id])) as Record<string, string>;",
|
|
19266
|
+
""
|
|
19267
|
+
].join(`
|
|
19268
|
+
`);
|
|
19269
|
+
const generatedRoutes = [
|
|
19270
|
+
"import type { AdminPageRoute } from '@svadmin/ui';",
|
|
19271
|
+
"import ProductSurfacePage from '../ProductSurfacePage.svelte';",
|
|
19272
|
+
"import { productSurfaceRoutes } from './pages';",
|
|
19273
|
+
"",
|
|
19274
|
+
"export const productPageRoutes: readonly AdminPageRoute[] = Object.keys(productSurfaceRoutes).map(path => ({ path, component: ProductSurfacePage }));",
|
|
19275
|
+
""
|
|
19276
|
+
].join(`
|
|
19277
|
+
`);
|
|
19278
|
+
return {
|
|
19279
|
+
"svadmin.product.json": planJson + `
|
|
19280
|
+
`,
|
|
19281
|
+
"PRODUCT.md": summary,
|
|
19282
|
+
"src/generated/product-plan.ts": generatedPlan,
|
|
19283
|
+
"src/generated/resources.ts": resourcesSource,
|
|
19284
|
+
"src/generated/navigation.ts": generatedNavigation,
|
|
19285
|
+
"src/generated/pages.ts": generatedPages,
|
|
19286
|
+
"src/generated/page-routes.ts": generatedRoutes,
|
|
19287
|
+
"src/generated/README.md": `Generated product files. Derived from svadmin.product.json; regenerate instead of editing by hand.
|
|
19288
|
+
`
|
|
19289
|
+
};
|
|
19290
|
+
}
|
|
19291
|
+
function planVibeProject(packageRoot, manifest, preset, options = {}) {
|
|
19292
|
+
const blueprint = path6.join(packageRoot, options.productPlan ? "blueprints/product-workspace" : "blueprints/customer-workspace");
|
|
18745
19293
|
const files = readTree(blueprint);
|
|
18746
19294
|
const template = path6.join(packageRoot, "template");
|
|
18747
19295
|
for (const name of ["index.html", "svelte.config.js", "tsconfig.json", "vite.config.ts", "src/main.ts", "src/vite-env.d.ts"]) {
|
|
@@ -18752,15 +19300,19 @@ function planVibeProject(packageRoot, manifest, preset) {
|
|
|
18752
19300
|
}
|
|
18753
19301
|
files["DESIGN.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/DESIGN.md"), "utf8");
|
|
18754
19302
|
files["AGENTS.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/AGENTS.md"), "utf8");
|
|
19303
|
+
files["PRODUCT.md"] = fs6.readFileSync(path6.join(packageRoot, "guidance/PRODUCT.md"), "utf8");
|
|
19304
|
+
if (options.productPlan)
|
|
19305
|
+
Object.assign(files, buildProductGeneratedFiles(options.productPlan, options.roleIds ?? []));
|
|
18755
19306
|
const skill = files["vibe-skill.md"];
|
|
18756
19307
|
if (typeof skill !== "string")
|
|
18757
19308
|
throw new Error("The shipped vibe skill is missing");
|
|
18758
19309
|
files[".agents/skills/svadmin-vibe/SKILL.md"] = skill;
|
|
18759
19310
|
delete files["vibe-skill.md"];
|
|
18760
|
-
|
|
19311
|
+
if (!options.productPlan)
|
|
19312
|
+
files["src/design-selection.ts"] = `export const initialPreset = '${preset}' as const;
|
|
18761
19313
|
`;
|
|
18762
19314
|
const pkg = createProjectPackageJson(manifest, {
|
|
18763
|
-
projectName: "svadmin-customer-workspace",
|
|
19315
|
+
projectName: options.productPlan ? "svadmin-product-workspace" : "svadmin-customer-workspace",
|
|
18764
19316
|
dataProvider: "none",
|
|
18765
19317
|
authProvider: "none"
|
|
18766
19318
|
});
|
|
@@ -18777,31 +19329,36 @@ function planVibeProject(packageRoot, manifest, preset) {
|
|
|
18777
19329
|
authProvider: "none",
|
|
18778
19330
|
...manifest.dependencies["@svadmin/core"] === undefined ? {} : { coreVersionRange: manifest.dependencies["@svadmin/core"] }
|
|
18779
19331
|
});
|
|
18780
|
-
|
|
18781
|
-
name: resource.name,
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
19332
|
+
if (options.productPlan) {
|
|
19333
|
+
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"] }));
|
|
19334
|
+
ai.routes = options.productPlan.surfaces.map((surface) => ({ resource: surface.resourceIds[0] ?? "", path: productRoute(surface), operations: ["list", "show"] }));
|
|
19335
|
+
} else {
|
|
19336
|
+
ai.resources = resources.map((resource) => ({
|
|
19337
|
+
name: resource.name,
|
|
19338
|
+
label: resource.label ?? resource.name,
|
|
19339
|
+
fields: (resource.fields ?? []).map((field) => ({
|
|
19340
|
+
key: field.key,
|
|
19341
|
+
label: field.label,
|
|
19342
|
+
type: field.type,
|
|
19343
|
+
required: field.required ?? false
|
|
19344
|
+
})),
|
|
19345
|
+
operations: uiOnlyResources.includes(resource.name) ? [] : [
|
|
19346
|
+
"list",
|
|
19347
|
+
...resource.canShow === false ? [] : ["show"],
|
|
19348
|
+
...resource.canCreate === false ? [] : ["create"],
|
|
19349
|
+
...resource.canEdit === false ? [] : ["edit"]
|
|
19350
|
+
]
|
|
19351
|
+
}));
|
|
19352
|
+
ai.routes = ai.resources.map((resource) => ({
|
|
19353
|
+
resource: resource.name,
|
|
19354
|
+
path: `/${resource.name}`,
|
|
19355
|
+
operations: uiOnlyResources.includes(resource.name) ? ["list"] : resource.operations
|
|
19356
|
+
}));
|
|
19357
|
+
}
|
|
18801
19358
|
ai.providers.data = { choice: "none", package: null, capabilities: ["in-memory-demo", "data", "filter", "sort", "pagination"] };
|
|
18802
19359
|
ai.project.commands = pkg.scripts;
|
|
18803
19360
|
ai.project.testCommand = "bun run test:ui";
|
|
18804
|
-
ai.guidance = [...ai.guidance, "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
|
|
19361
|
+
ai.guidance = [...ai.guidance, "PRODUCT.md", "svadmin.vibe.json", "ARCHITECTURE.md", ".agents/skills/svadmin-vibe/SKILL.md"];
|
|
18805
19362
|
files["svadmin.ai.json"] = `${JSON.stringify(ai, null, 2)}
|
|
18806
19363
|
`;
|
|
18807
19364
|
files["svadmin.schema.json"] = `${JSON.stringify(buildAdminSchemaJson(), null, 2)}
|
|
@@ -18850,10 +19407,35 @@ async function vibeCommand(args, packageRoot, manifest) {
|
|
|
18850
19407
|
console.log(JSON.stringify(inspectVibePage(packageRoot, options.page), null, 2));
|
|
18851
19408
|
return;
|
|
18852
19409
|
}
|
|
19410
|
+
if (options.command === "review" || options.command === "plan") {
|
|
19411
|
+
const input = JSON.parse(fs6.readFileSync(options.planFile, "utf8"));
|
|
19412
|
+
const plan = parseProductPlan(input);
|
|
19413
|
+
if (options.command === "review") {
|
|
19414
|
+
console.log(JSON.stringify(reviewProductPlan(plan), null, 2));
|
|
19415
|
+
return;
|
|
19416
|
+
}
|
|
19417
|
+
const roleIds = options.roleIds.length ? options.roleIds : plan.roles.map((role) => role.id);
|
|
19418
|
+
console.log(formatProductPreview(runProductPipeline({ plan, navigation: { roleIds } })));
|
|
19419
|
+
return;
|
|
19420
|
+
}
|
|
19421
|
+
if (options.command !== "init")
|
|
19422
|
+
throw new Error("Unsupported vibe command");
|
|
18853
19423
|
if (fs6.existsSync(options.directory))
|
|
18854
19424
|
throw new Error("The target already exists. Choose a new directory; existing projects are never overwritten.");
|
|
18855
|
-
|
|
18856
|
-
|
|
19425
|
+
let productPlan;
|
|
19426
|
+
if (options.productPlanFile) {
|
|
19427
|
+
const input = JSON.parse(fs6.readFileSync(options.productPlanFile, "utf8"));
|
|
19428
|
+
productPlan = parseProductPlan(input);
|
|
19429
|
+
const report = reviewProductPlan(productPlan);
|
|
19430
|
+
if (!report.ok)
|
|
19431
|
+
throw new Error(`Product plan review failed:
|
|
19432
|
+
` + JSON.stringify(report, null, 2));
|
|
19433
|
+
if (report.issues.length)
|
|
19434
|
+
console.warn("Product plan review warnings: " + report.issues.length);
|
|
19435
|
+
}
|
|
19436
|
+
const productOptions = { roleIds: options.roleIds, ...productPlan === undefined ? {} : { productPlan } };
|
|
19437
|
+
const files = planVibeProject(packageRoot, manifest, options.preset, productOptions);
|
|
19438
|
+
console.log(`svadmin vibe: ${productPlan ? "product-workspace" : "customer-workspace"} / ${options.preset}
|
|
18857
19439
|
${options.directory}`);
|
|
18858
19440
|
for (const file of Object.keys(files).sort())
|
|
18859
19441
|
console.log(` add ${file}`);
|
|
@@ -18991,7 +19573,7 @@ function upgrade(args) {
|
|
|
18991
19573
|
const upgradeExecution = commandArguments.write ? writeProjectPackageJsonUpgrade(packagePath, scaffoldManifest, new Date) : planProjectPackageFileUpgrade(packagePath, scaffoldManifest);
|
|
18992
19574
|
printUpgradeExecution(upgradeExecution, commandArguments.projectDirectory, packagePath);
|
|
18993
19575
|
}
|
|
18994
|
-
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md"];
|
|
19576
|
+
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md", "PRODUCT.md"];
|
|
18995
19577
|
function missingGuidanceFiles(projectDirectory) {
|
|
18996
19578
|
return GUIDANCE_FILES.filter((fileName) => !fs7.existsSync(path7.join(projectDirectory, fileName)));
|
|
18997
19579
|
}
|